3 """Defines :class:`GUI` providing a wxWidgets interface to Hooke.
\r
9 wxversion.select(WX_GOOD)
\r
18 import wx.aui as aui
\r
19 import wx.lib.evtmgr as evtmgr
\r
22 # wxPropertyGrid included in wxPython >= 2.9.1, until then, see
\r
23 # http://wxpropgrid.sourceforge.net/cgi-bin/index?page=download
\r
24 # until then, we'll avoid it because of the *nix build problems.
\r
25 #import wx.propgrid as wxpg
\r
27 from matplotlib.ticker import FuncFormatter
\r
29 from ...command import CommandExit, Exit, Success, Failure, Command, Argument
\r
30 from ...config import Setting
\r
31 from ...interaction import Request, BooleanRequest, ReloadUserInterfaceConfig
\r
32 from ...ui import UserInterface, CommandMessage
\r
33 from . import menu as menu
\r
34 from . import navbar as navbar
\r
35 from . import panel as panel
\r
36 from . import prettyformat as prettyformat
\r
37 from . import statusbar as statusbar
\r
40 class HookeFrame (wx.Frame):
\r
41 """The main Hooke-interface window.
\r
45 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
\r
46 super(HookeFrame, self).__init__(*args, **kwargs)
\r
48 self.commands = commands
\r
49 self.inqueue = inqueue
\r
50 self.outqueue = outqueue
\r
51 self._perspectives = {} # {name: perspective_str}
\r
54 self.SetIcon(wx.Icon(self.gui.config['icon image'], wx.BITMAP_TYPE_ICO))
\r
56 # setup frame manager
\r
57 self._c['manager'] = aui.AuiManager()
\r
58 self._c['manager'].SetManagedWindow(self)
\r
60 # set the gradient and drag styles
\r
61 self._c['manager'].GetArtProvider().SetMetric(
\r
62 aui.AUI_DOCKART_GRADIENT_TYPE, aui.AUI_GRADIENT_NONE)
\r
63 self._c['manager'].SetFlags(
\r
64 self._c['manager'].GetFlags() ^ aui.AUI_MGR_TRANSPARENT_DRAG)
\r
66 # Min size for the frame itself isn't completely done. See
\r
67 # the end of FrameManager::Update() for the test code. For
\r
68 # now, just hard code a frame minimum size.
\r
69 self.SetMinSize(wx.Size(500, 500))
\r
71 self._setup_panels()
\r
72 self._setup_toolbars()
\r
73 self._c['manager'].Update() # commit pending changes
\r
75 # Create the menubar after the panes so that the default
\r
76 # perspective is created with all panes open
\r
77 self._c['menu bar'] = menu.HookeMenuBar(
\r
80 'close': self._on_close,
\r
81 'about': self._on_about,
\r
83 self.SetMenuBar(self._c['menu bar'])
\r
85 self._c['status bar'] = statusbar.StatusBar(
\r
87 style=wx.ST_SIZEGRIP)
\r
88 self.SetStatusBar(self._c['status bar'])
\r
90 self._update_perspectives()
\r
93 name = self.gui.config['active perspective']
\r
94 return # TODO: cleanup
\r
95 menu_item = self.GetPerspectiveMenuItem(name)
\r
96 if menu_item is not None:
\r
97 self._on_restore_perspective(menu_item)
\r
98 #TODO: config setting to remember playlists from last session
\r
99 self.playlists = self._c['playlists'].Playlists
\r
100 self._displayed_plot = None
\r
101 #load default list, if possible
\r
102 self.do_loadlist(self.GetStringFromConfig('core', 'preferences', 'playlist'))
\r
104 def _setup_panels(self):
\r
105 client_size = self.GetClientSize()
\r
106 for label,p,style in [
\r
107 # ('folders', wx.GenericDirCtrl(
\r
109 # dir=self.gui.config['folders-workdir'],
\r
111 # style=wx.DIRCTRL_SHOW_FILTERS,
\r
112 # filter=self.gui.config['folders-filters'],
\r
113 # defaultFilter=int(self.gui.config['folders-filter-index'])), 'left'), #HACK: config should convert
\r
114 # ('playlists', panel.PANELS['playlist'](
\r
116 # config=self.gui.config,
\r
118 # style=wx.WANTS_CHARS|wx.NO_BORDER,
\r
119 # # WANTS_CHARS so the panel doesn't eat the Return key.
\r
120 # size=(160, 200)), 'left'),
\r
121 # ('note', panel.note.Note(
\r
123 # style=wx.WANTS_CHARS|wx.NO_BORDER,
\r
124 # size=(160, 200)), 'left'),
\r
125 # ('notebook', Notebook(
\r
127 # pos=wx.Point(client_size.x, client_size.y),
\r
128 # size=wx.Size(430, 200),
\r
129 # style=aui.AUI_NB_DEFAULT_STYLE
\r
130 # | aui.AUI_NB_TAB_EXTERNAL_MOVE | wx.NO_BORDER), 'center'),
\r
131 ('commands', panel.PANELS['commands'](
\r
132 commands=self.commands,
\r
133 selected=self.gui.config['selected command'],
\r
135 'execute': self.execute_command,
\r
136 'select_plugin': self.select_plugin,
\r
137 'select_command': self.select_command,
\r
138 # 'selection_changed': self.panelProperties.select(self, method, command), #SelectedTreeItem = selected_item,
\r
141 style=wx.WANTS_CHARS|wx.NO_BORDER,
\r
142 # WANTS_CHARS so the panel doesn't eat the Return key.
\r
145 #('properties', panel.propertyeditor.PropertyEditor(self),'right'),
\r
146 # ('assistant', wx.TextCtrl(
\r
148 # pos=wx.Point(0, 0),
\r
149 # size=wx.Size(150, 90),
\r
150 # style=wx.NO_BORDER|wx.TE_MULTILINE), 'right'),
\r
151 # ('output', wx.TextCtrl(
\r
153 # pos=wx.Point(0, 0),
\r
154 # size=wx.Size(150, 90),
\r
155 # style=wx.NO_BORDER|wx.TE_MULTILINE), 'bottom'),
\r
156 # ('results', panel.results.Results(self), 'bottom'),
\r
158 self._add_panel(label, p, style)
\r
159 #self._c['assistant'].SetEditable(False)
\r
161 def _add_panel(self, label, panel, style):
\r
162 self._c[label] = panel
\r
163 cap_label = label.capitalize()
\r
164 info = aui.AuiPaneInfo().Name(cap_label).Caption(cap_label)
\r
165 info.PaneBorder(False).CloseButton(True).MaximizeButton(False)
\r
168 elif style == 'center':
\r
170 elif style == 'left':
\r
172 elif style == 'right':
\r
175 assert style == 'bottom', style
\r
177 self._c['manager'].AddPane(panel, info)
\r
179 def _setup_toolbars(self):
\r
180 self._c['navigation bar'] = navbar.NavBar(
\r
182 'next': self._next_curve,
\r
183 'previous': self._previous_curve,
\r
186 style=wx.TB_FLAT | wx.TB_NODIVIDER)
\r
187 self._c['manager'].AddPane(
\r
188 self._c['navigation bar'],
\r
189 aui.AuiPaneInfo().Name('Navigation').Caption('Navigation'
\r
190 ).ToolbarPane().Top().Layer(1).Row(1).LeftDockable(False
\r
191 ).RightDockable(False))
\r
193 def _bind_events(self):
\r
194 # TODO: figure out if we can use the eventManager for menu
\r
195 # ranges and events of 'self' without raising an assertion
\r
197 self.Bind(wx.EVT_ERASE_BACKGROUND, self._on_erase_background)
\r
198 self.Bind(wx.EVT_SIZE, self._on_size)
\r
199 self.Bind(wx.EVT_CLOSE, self._on_close)
\r
200 self.Bind(aui.EVT_AUI_PANE_CLOSE, self.OnPaneClose)
\r
201 self.Bind(aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self._on_notebook_page_close)
\r
203 return # TODO: cleanup
\r
204 for value in self._c['menu bar']._c['view']._c.values():
\r
205 self.Bind(wx.EVT_MENU_RANGE, self._on_view, value)
\r
207 self.Bind(wx.EVT_MENU, self._on_save_perspective,
\r
208 self._c['menu bar']._c['perspective']._c['save'])
\r
209 self.Bind(wx.EVT_MENU, self._on_delete_perspective,
\r
210 self._c['menu bar']._c['perspective']._c['delete'])
\r
212 treeCtrl = self._c['folders'].GetTreeCtrl()
\r
213 treeCtrl.Bind(wx.EVT_LEFT_DCLICK, self._on_dir_ctrl_left_double_click)
\r
215 # TODO: playlist callbacks
\r
216 return # TODO: cleanup
\r
217 evtmgr.eventManager.Register(self.OnUpdateNote, wx.EVT_BUTTON, self.panelNote.UpdateButton)
\r
219 self.panelProperties.pg.Bind(wxpg.EVT_PG_CHANGED, self.OnPropGridChanged)
\r
221 self.panelResults.results_list.OnCheckItem = self.OnResultsCheck
\r
223 def _command_by_name(self, name):
\r
224 cs = [c for c in self.commands if c.name == name]
\r
226 raise KeyError(name)
\r
228 raise Exception('Multiple commands named "%s"' % name)
\r
231 def execute_command(self, _class=None, method=None,
\r
232 command=None, args=None):
\r
233 self.inqueue.put(CommandMessage(command, args))
\r
236 msg = self.outqueue.get()
\r
237 results.append(msg)
\r
238 print type(msg), msg
\r
239 if isinstance(msg, Exit):
\r
242 elif isinstance(msg, CommandExit):
\r
243 # TODO: display command complete
\r
245 elif isinstance(msg, ReloadUserInterfaceConfig):
\r
246 self.gui.reload_config(msg.config)
\r
248 elif isinstance(msg, Request):
\r
249 h = handler.HANDLERS[msg.type]
\r
250 h.run(self, msg) # TODO: pause for response?
\r
253 self, '_postprocess_%s' % command.name.replace(' ', '_'), None)
\r
255 pp(command=command, results=results)
\r
258 def _handle_request(self, msg):
\r
259 """Repeatedly try to get a response to `msg`.
\r
262 raise NotImplementedError('_%s_request_prompt' % msg.type)
\r
263 prompt_string = prompt(msg)
\r
264 parser = getattr(self, '_%s_request_parser' % msg.type, None)
\r
266 raise NotImplementedError('_%s_request_parser' % msg.type)
\r
270 self.cmd.stdout.write(''.join([
\r
271 error.__class__.__name__, ': ', str(error), '\n']))
\r
272 self.cmd.stdout.write(prompt_string)
\r
273 value = parser(msg, self.cmd.stdin.readline())
\r
275 response = msg.response(value)
\r
277 except ValueError, error:
\r
279 self.inqueue.put(response)
\r
282 def _GetActiveFileIndex(self):
\r
283 lib.playlist.Playlist = self.GetActivePlaylist()
\r
284 #get the selected item from the tree
\r
285 selected_item = self._c['playlists']._c['tree'].GetSelection()
\r
286 #test if a playlist or a curve was double-clicked
\r
287 if self._c['playlists']._c['tree'].ItemHasChildren(selected_item):
\r
291 selected_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item)
\r
292 while selected_item.IsOk():
\r
294 selected_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item)
\r
297 def _GetPlaylistTab(self, name):
\r
298 for index, page in enumerate(self._c['notebook']._tabs._pages):
\r
299 if page.caption == name:
\r
303 def _restore_perspective(self, name):
\r
305 self.gui.config['active perspective'] = name # TODO: push to engine's Hooke
\r
306 self._c['manager'].LoadPerspective(self._perspectives[name])
\r
307 self._c['manager'].Update()
\r
308 for pane in self._c['manager'].GetAllPanes():
\r
309 if pane.name in self._c['menu bar']._c['view']._c.keys():
\r
310 pane.Check(pane.window.IsShown())
\r
312 def _SavePerspectiveToFile(self, name, perspective):
\r
313 filename = ''.join([name, '.txt'])
\r
314 filename = lh.get_file_path(filename, ['perspective'])
\r
315 perspectivesFile = open(filename, 'w')
\r
316 perspectivesFile.write(perspective)
\r
317 perspectivesFile.close()
\r
319 def select_plugin(self, _class=None, method=None, plugin=None):
\r
320 for option in config[section]:
\r
321 properties.append([option, config[section][option]])
\r
323 def select_command(self, _class, method, command):
\r
324 self.select_plugin(plugin=command.plugin)
\r
325 plugin = self.GetItemText(selected_item)
\r
326 if plugin != 'core':
\r
327 doc_string = eval('plugins.' + plugin + '.' + plugin + 'Commands.__doc__')
\r
329 doc_string = 'The module "core" contains Hooke core functionality'
\r
330 if doc_string is not None:
\r
331 self.panelAssistant.ChangeValue(doc_string)
\r
333 self.panelAssistant.ChangeValue('')
\r
334 panel.propertyeditor.PropertyEditor.Initialize(self.panelProperties, properties)
\r
335 self.gui.config['selected command'] = command
\r
337 def AddPlaylistFromFiles(self, files=[], name='Untitled'):
\r
339 playlist = lib.playlist.Playlist(self, self.drivers)
\r
341 playlist.add_curve(item)
\r
342 if playlist.count > 0:
\r
343 playlist.name = self._GetUniquePlaylistName(name)
\r
345 self.AddTayliss(playlist)
\r
347 def AppendToOutput(self, text):
\r
348 self.panelOutput.AppendText(''.join([text, '\n']))
\r
350 def AppliesPlotmanipulator(self, name):
\r
352 Returns True if the plotmanipulator 'name' is applied, False otherwise
\r
353 name does not contain 'plotmanip_', just the name of the plotmanipulator (e.g. 'flatten')
\r
355 return self.GetBoolFromConfig('core', 'plotmanipulators', name)
\r
357 def ApplyPlotmanipulators(self, plot, plot_file):
\r
359 Apply all active plotmanipulators.
\r
361 if plot is not None and plot_file is not None:
\r
362 manipulated_plot = copy.deepcopy(plot)
\r
363 for plotmanipulator in self.plotmanipulators:
\r
364 if self.GetBoolFromConfig('core', 'plotmanipulators', plotmanipulator.name):
\r
365 manipulated_plot = plotmanipulator.method(manipulated_plot, plot_file)
\r
366 return manipulated_plot
\r
368 def GetActiveFigure(self):
\r
369 playlist_name = self.GetActivePlaylistName()
\r
370 figure = self.playlists[playlist_name].figure
\r
371 if figure is not None:
\r
375 def GetActiveFile(self):
\r
376 playlist = self.GetActivePlaylist()
\r
377 if playlist is not None:
\r
378 return playlist.get_active_file()
\r
381 def GetActivePlot(self):
\r
382 playlist = self.GetActivePlaylist()
\r
383 if playlist is not None:
\r
384 return playlist.get_active_file().plot
\r
387 def GetDisplayedPlot(self):
\r
388 plot = copy.deepcopy(self.displayed_plot)
\r
390 #plot.curves = copy.deepcopy(plot.curves)
\r
393 def GetDisplayedPlotCorrected(self):
\r
394 plot = copy.deepcopy(self.displayed_plot)
\r
396 plot.curves = copy.deepcopy(plot.corrected_curves)
\r
399 def GetDisplayedPlotRaw(self):
\r
400 plot = copy.deepcopy(self.displayed_plot)
\r
402 plot.curves = copy.deepcopy(plot.raw_curves)
\r
405 def GetDockArt(self):
\r
406 return self._c['manager'].GetArtProvider()
\r
408 def GetPlotmanipulator(self, name):
\r
410 Returns a plot manipulator function from its name
\r
412 for plotmanipulator in self.plotmanipulators:
\r
413 if plotmanipulator.name == name:
\r
414 return plotmanipulator
\r
417 def GetPerspectiveMenuItem(self, name):
\r
418 if self._perspectives.has_key(name):
\r
419 perspectives_list = [key for key, value in self._perspectives.iteritems()]
\r
420 perspectives_list.sort()
\r
421 index = perspectives_list.index(name)
\r
422 perspective_Id = ID_FirstPerspective + index
\r
423 menu_item = self._c['menu bar'].FindItemById(perspective_Id)
\r
428 def HasPlotmanipulator(self, name):
\r
430 returns True if the plotmanipulator 'name' is loaded, False otherwise
\r
432 for plotmanipulator in self.plotmanipulators:
\r
433 if plotmanipulator.command == name:
\r
437 def _on_about(self, *args):
\r
438 dialog = wx.MessageDialog(
\r
440 message=self.gui._splash_text(),
\r
441 caption='About Hooke',
\r
442 style=wx.OK|wx.ICON_INFORMATION)
\r
446 def _on_close(self, *args):
\r
448 self.gui.config['main height'] = str(self.GetSize().GetHeight())
\r
449 self.gui.config['main left'] = str(self.GetPosition()[0])
\r
450 self.gui.config['main top'] = str(self.GetPosition()[1])
\r
451 self.gui.config['main width'] = str(self.GetSize().GetWidth())
\r
452 # push changes back to Hooke.config?
\r
453 self._c['manager'].UnInit()
\r
454 del self._c['manager']
\r
457 def _update_perspectives(self):
\r
458 """Add perspectives to menubar and _perspectives.
\r
460 self._perspectives = {
\r
461 'Default': self._c['manager'].SavePerspective(),
\r
463 path = self.gui.config['perspective path']
\r
464 if os.path.isdir(path):
\r
465 files = sorted(os.listdir(path))
\r
466 for fname in files:
\r
467 name, extension = os.path.splitext(fname)
\r
468 if extension != '.txt':
\r
470 fpath = os.path.join(path, fpath)
\r
471 if not os.path.isfile(fpath):
\r
474 with open(fpath, 'rU') as f:
\r
475 perspective = f.readline()
\r
477 self._perspectives[name] = perspective
\r
479 selected_perspective = self.gui.config['active perspective']
\r
480 if not self._perspectives.has_key(selected_perspective):
\r
481 self.gui.config['active perspective'] = 'Default' # TODO: push to engine's Hooke
\r
483 self._update_perspective_menu()
\r
484 self._restore_perspective(selected_perspective)
\r
486 def _update_perspective_menu(self):
\r
487 self._c['menu bar']._c['perspective'].update(
\r
488 sorted(self._perspectives.keys()),
\r
489 self.gui.config['active perspective'],
\r
490 self._on_restore_perspective)
\r
492 def _on_restore_perspective(self, event):
\r
493 name = self._c['menu bar'].FindItemById(event.GetId()).GetLabel()
\r
494 self._restore_perspective(name)
\r
496 def _on_save_perspective(self, event):
\r
497 def nameExists(name):
\r
498 menu_position = self._c['menu bar'].FindMenu('Perspective')
\r
499 menu = self._c['menu bar'].GetMenu(menu_position)
\r
500 for item in menu.GetMenuItems():
\r
501 if item.GetText() == name:
\r
507 dialog = wx.TextEntryDialog(self, 'Enter a name for the new perspective:', 'Save perspective')
\r
508 dialog.SetValue('New perspective')
\r
509 if dialog.ShowModal() != wx.ID_OK:
\r
512 name = dialog.GetValue()
\r
514 if nameExists(name):
\r
515 dialogConfirm = wx.MessageDialog(self, 'A file with this name already exists.\n\nDo you want to replace it?', 'Confirm', wx.YES_NO|wx.ICON_QUESTION|wx.CENTER)
\r
516 if dialogConfirm.ShowModal() == wx.ID_YES:
\r
521 perspective = self._c['manager'].SavePerspective()
\r
522 self._SavePerspectiveToFile(name, perspective)
\r
523 self.gui.config['active perspectives'] = name
\r
524 self._update_perspective_menu()
\r
525 # if nameExists(name):
\r
526 # #check the corresponding menu item
\r
527 # menu_item = self.GetPerspectiveMenuItem(name)
\r
528 # #replace the perspectiveStr in _pespectives
\r
529 # self._perspectives[name] = perspective
\r
531 # #because we deal with radio items, we need to do some extra work
\r
532 # #delete all menu items from the perspectives menu
\r
533 # for item in self._perspectives_menu.GetMenuItems():
\r
534 # self._perspectives_menu.DeleteItem(item)
\r
535 # #recreate the perspectives menu
\r
536 # self._perspectives_menu.Append(ID_SavePerspective, 'Save Perspective')
\r
537 # self._perspectives_menu.Append(ID_DeletePerspective, 'Delete Perspective')
\r
538 # self._perspectives_menu.AppendSeparator()
\r
539 # #convert the perspectives dictionary into a list
\r
540 # # the list contains:
\r
541 # #[0]: name of the perspective
\r
542 # #[1]: perspective
\r
543 # perspectives_list = [key for key, value in self._perspectives.iteritems()]
\r
544 # perspectives_list.append(name)
\r
545 # perspectives_list.sort()
\r
546 # #add all previous perspectives
\r
547 # for index, item in enumerate(perspectives_list):
\r
548 # menu_item = self._perspectives_menu.AppendRadioItem(ID_FirstPerspective + index, item)
\r
550 # menu_item.Check()
\r
551 # #add the new perspective to _perspectives
\r
552 # self._perspectives[name] = perspective
\r
554 def _on_delete_perspective(self, event):
\r
555 dialog = panel.selection.Selection(
\r
556 options=sorted(os.listdir(self.gui.config['perspective path'])),
\r
557 message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
\r
558 button_id=wx.ID_DELETE,
\r
559 callbacks={'button': self._on_delete_perspective},
\r
560 selection_style='multiple',
\r
562 label='Delete perspective(s)',
\r
563 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
\r
564 dialog.CenterOnScreen()
\r
567 self._update_perspective_menu()
\r
568 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
\r
569 # http://trac.wxwidgets.org/ticket/3258
\r
570 # ) that makes the radio item indicator in the menu disappear.
\r
571 # The code should be fine once this issue is fixed.
\r
573 def _on_delete_perspective(self, _class, method, options, selected):
\r
575 self._perspectives.remove(p)
\r
576 if p == self.gui.config['active perspective']:
\r
577 self.gui.config['active perspective'] = 'Default'
\r
578 path = os.path.join(self.gui.config['perspective path'],
\r
581 self._update_perspective_menu()
\r
583 def _on_dir_ctrl_left_double_click(self, event):
\r
584 file_path = self.panelFolders.GetPath()
\r
585 if os.path.isfile(file_path):
\r
586 if file_path.endswith('.hkp'):
\r
587 self.do_loadlist(file_path)
\r
590 def _on_erase_background(self, event):
\r
593 def _next_curve(self, *args):
\r
594 """Call the `next curve` command.
\r
596 results = self.execute_command(
\r
597 command=self._command_by_name('next curve'))
\r
598 if isinstance(results[-1], Success):
\r
599 self.execute_command(
\r
600 command=self._command_by_name('get curve'))
\r
602 def _previous_curve(self, *args):
\r
603 """Call the `previous curve` command.
\r
605 self.execute_command(
\r
606 command=self._command_by_name('previous curve'))
\r
607 if isinstance(results[-1], Success):
\r
608 self.execute_command(
\r
609 command=self._command_by_name('get curve'))
\r
611 def _postprocess_get_curve(self, command, results):
\r
612 """Update `self` to show the curve.
\r
614 if not isinstance(results[-1], Success):
\r
615 return # error executing 'get curve'
\r
616 assert len(results) == 2, results
\r
620 selected_item = self._c['playlists']._c['tree'].GetSelection()
\r
621 if self._c['playlists']._c['tree'].ItemHasChildren(selected_item):
\r
622 #GetFirstChild returns a tuple
\r
623 #we only need the first element
\r
624 next_item = self._c['playlists']._c['tree'].GetFirstChild(selected_item)[0]
\r
626 next_item = self._c['playlists']._c['tree'].GetNextSibling(selected_item)
\r
627 if not next_item.IsOk():
\r
628 parent_item = self._c['playlists']._c['tree'].GetItemParent(selected_item)
\r
629 #GetFirstChild returns a tuple
\r
630 #we only need the first element
\r
631 next_item = self._c['playlists']._c['tree'].GetFirstChild(parent_item)[0]
\r
632 self._c['playlists']._c['tree'].SelectItem(next_item, True)
\r
633 if not self._c['playlists']._c['tree'].ItemHasChildren(selected_item):
\r
634 playlist = self.GetActivePlaylist()
\r
635 if playlist.count > 1:
\r
637 self._c['status bar'].set_playlist(playlist)
\r
641 def _on_notebook_page_close(self, event):
\r
642 ctrl = event.GetEventObject()
\r
643 playlist_name = ctrl.GetPageText(ctrl._curpage)
\r
644 self.DeleteFromPlaylists(playlist_name)
\r
646 def OnPaneClose(self, event):
\r
649 def OnPropGridChanged (self, event):
\r
650 prop = event.GetProperty()
\r
652 item_section = self.panelProperties.SelectedTreeItem
\r
653 item_plugin = self._c['commands']._c['tree'].GetItemParent(item_section)
\r
654 plugin = self._c['commands']._c['tree'].GetItemText(item_plugin)
\r
655 config = self.gui.config[plugin]
\r
656 property_section = self._c['commands']._c['tree'].GetItemText(item_section)
\r
657 property_key = prop.GetName()
\r
658 property_value = prop.GetDisplayedString()
\r
660 config[property_section][property_key]['value'] = property_value
\r
662 def OnResultsCheck(self, index, flag):
\r
663 results = self.GetActivePlot().results
\r
664 if results.has_key(self.results_str):
\r
665 results[self.results_str].results[index].visible = flag
\r
666 results[self.results_str].update()
\r
670 def _on_size(self, event):
\r
673 def OnUpdateNote(self, event):
\r
675 Saves the note to the active file.
\r
677 active_file = self.GetActiveFile()
\r
678 active_file.note = self.panelNote.Editor.GetValue()
\r
680 def _on_view(self, event):
\r
681 menu_id = event.GetId()
\r
682 menu_item = self._c['menu bar'].FindItemById(menu_id)
\r
683 menu_label = menu_item.GetLabel()
\r
685 pane = self._c['manager'].GetPane(menu_label)
\r
686 pane.Show(not pane.IsShown())
\r
687 #if we don't do the following, the Folders pane does not resize properly on hide/show
\r
688 if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
\r
689 #folders_size = pane.GetSize()
\r
690 self.panelFolders.Fit()
\r
691 self._c['manager'].Update()
\r
693 def _clickize(self, xvector, yvector, index):
\r
695 Returns a ClickedPoint() object from an index and vectors of x, y coordinates
\r
697 point = lib.clickedpoint.ClickedPoint()
\r
698 point.index = index
\r
699 point.absolute_coords = xvector[index], yvector[index]
\r
700 point.find_graph_coords(xvector, yvector)
\r
703 def _delta(self, message='Click 2 points', block=0):
\r
705 Calculates the difference between two clicked points
\r
707 clicked_points = self._measure_N_points(N=2, message=message, block=block)
\r
709 plot = self.GetDisplayedPlotCorrected()
\r
710 curve = plot.curves[block]
\r
712 delta = lib.delta.Delta()
\r
713 delta.point1.x = clicked_points[0].graph_coords[0]
\r
714 delta.point1.y = clicked_points[0].graph_coords[1]
\r
715 delta.point2.x = clicked_points[1].graph_coords[0]
\r
716 delta.point2.y = clicked_points[1].graph_coords[1]
\r
717 delta.units.x = curve.units.x
\r
718 delta.units.y = curve.units.y
\r
722 def _measure_N_points(self, N, message='', block=0):
\r
724 General helper function for N-points measurements
\r
725 By default, measurements are done on the retraction
\r
728 dialog = wx.MessageDialog(None, message, 'Info', wx.OK)
\r
731 figure = self.GetActiveFigure()
\r
733 xvector = self.displayed_plot.curves[block].x
\r
734 yvector = self.displayed_plot.curves[block].y
\r
736 clicked_points = figure.ginput(N, timeout=-1, show_clicks=True)
\r
739 for clicked_point in clicked_points:
\r
740 point = lib.clickedpoint.ClickedPoint()
\r
741 point.absolute_coords = clicked_point[0], clicked_point[1]
\r
743 #TODO: make this optional?
\r
744 #so far, the clicked point is taken, not the corresponding data point
\r
745 point.find_graph_coords(xvector, yvector)
\r
746 point.is_line_edge = True
\r
747 point.is_marker = True
\r
748 points.append(point)
\r
751 def do_copylog(self):
\r
753 Copies all files in the current playlist that have a note to the destination folder.
\r
754 destination: select folder where you want the files to be copied
\r
755 use_LVDT_folder: when checked, the files will be copied to a folder called 'LVDT' in the destination folder (for MFP-1D files only)
\r
757 playlist = self.GetActivePlaylist()
\r
758 if playlist is not None:
\r
759 destination = self.GetStringFromConfig('core', 'copylog', 'destination')
\r
760 if not os.path.isdir(destination):
\r
761 os.makedirs(destination)
\r
762 for current_file in playlist.files:
\r
763 if current_file.note:
\r
764 shutil.copy(current_file.filename, destination)
\r
765 if current_file.driver.filetype == 'mfp1d':
\r
766 filename = current_file.filename.replace('deflection', 'LVDT', 1)
\r
767 path, name = os.path.split(filename)
\r
768 filename = os.path.join(path, 'lvdt', name)
\r
769 use_LVDT_folder = self.GetBoolFromConfig('core', 'copylog', 'use_LVDT_folder')
\r
770 if use_LVDT_folder:
\r
771 destination = os.path.join(destination, 'LVDT')
\r
772 shutil.copy(filename, destination)
\r
774 def do_plotmanipulators(self):
\r
776 Please select the plotmanipulators you would like to use
\r
777 and define the order in which they will be applied to the data.
\r
779 Click 'Execute' to apply your changes.
\r
783 def do_preferences(self):
\r
785 Please set general preferences for Hooke here.
\r
786 hide_curve_extension: hides the extension of the force curve files.
\r
787 not recommended for 'picoforce' files
\r
793 Use this command for testing purposes. You find do_test in hooke.py.
\r
797 def do_version(self):
\r
801 Prints the current version and codename, plus library version. Useful for debugging.
\r
803 self.AppendToOutput('Hooke ' + __version__ + ' (' + __codename__ + ')')
\r
804 self.AppendToOutput('Released on: ' + __releasedate__)
\r
805 self.AppendToOutput('---')
\r
806 self.AppendToOutput('Python version: ' + python_version)
\r
807 self.AppendToOutput('WxPython version: ' + wx_version)
\r
808 self.AppendToOutput('Matplotlib version: ' + mpl_version)
\r
809 self.AppendToOutput('SciPy version: ' + scipy_version)
\r
810 self.AppendToOutput('NumPy version: ' + numpy_version)
\r
811 self.AppendToOutput('ConfigObj version: ' + configobj_version)
\r
812 self.AppendToOutput('wxPropertyGrid version: ' + '.'.join([str(PROPGRID_MAJOR), str(PROPGRID_MINOR), str(PROPGRID_RELEASE)]))
\r
813 self.AppendToOutput('---')
\r
814 self.AppendToOutput('Platform: ' + str(platform.uname()))
\r
815 self.AppendToOutput('******************************')
\r
816 self.AppendToOutput('Loaded plugins')
\r
817 self.AppendToOutput('---')
\r
819 #sort the plugins into alphabetical order
\r
820 plugins_list = [key for key, value in self.plugins.iteritems()]
\r
821 plugins_list.sort()
\r
822 for plugin in plugins_list:
\r
823 self.AppendToOutput(plugin)
\r
825 def UpdateNote(self):
\r
826 #update the note for the active file
\r
827 active_file = self.GetActiveFile()
\r
828 if active_file is not None:
\r
829 self.panelNote.Editor.SetValue(active_file.note)
\r
831 def UpdatePlaylistsTreeSelection(self):
\r
832 playlist = self.GetActivePlaylist()
\r
833 if playlist is not None:
\r
834 if playlist.index >= 0:
\r
835 self._c['status bar'].set_playlist(playlist)
\r
839 def UpdatePlot(self, plot=None):
\r
841 def add_to_plot(curve, set_scale=True):
\r
842 if curve.visible and curve.x and curve.y:
\r
843 #get the index of the subplot to use as destination
\r
844 destination = (curve.destination.column - 1) * number_of_rows + curve.destination.row - 1
\r
845 #set all parameters for the plot
\r
846 axes_list[destination].set_title(curve.title)
\r
848 axes_list[destination].set_xlabel(curve.prefix.x + curve.units.x)
\r
849 axes_list[destination].set_ylabel(curve.prefix.y + curve.units.y)
\r
850 #set the formatting details for the scale
\r
851 formatter_x = lib.curve.PrefixFormatter(curve.decimals.x, curve.prefix.x, use_zero)
\r
852 formatter_y = lib.curve.PrefixFormatter(curve.decimals.y, curve.prefix.y, use_zero)
\r
853 axes_list[destination].xaxis.set_major_formatter(formatter_x)
\r
854 axes_list[destination].yaxis.set_major_formatter(formatter_y)
\r
855 if curve.style == 'plot':
\r
856 axes_list[destination].plot(curve.x, curve.y, color=curve.color, label=curve.label, lw=curve.linewidth, zorder=1)
\r
857 if curve.style == 'scatter':
\r
858 axes_list[destination].scatter(curve.x, curve.y, color=curve.color, label=curve.label, s=curve.size, zorder=2)
\r
859 #add the legend if necessary
\r
861 axes_list[destination].legend()
\r
864 active_file = self.GetActiveFile()
\r
865 if not active_file.driver:
\r
866 #the first time we identify a file, the following need to be set
\r
867 active_file.identify(self.drivers)
\r
868 for curve in active_file.plot.curves:
\r
869 curve.decimals.x = self.GetIntFromConfig('core', 'preferences', 'x_decimals')
\r
870 curve.decimals.y = self.GetIntFromConfig('core', 'preferences', 'y_decimals')
\r
871 curve.legend = self.GetBoolFromConfig('core', 'preferences', 'legend')
\r
872 curve.prefix.x = self.GetStringFromConfig('core', 'preferences', 'x_prefix')
\r
873 curve.prefix.y = self.GetStringFromConfig('core', 'preferences', 'y_prefix')
\r
874 if active_file.driver is None:
\r
875 self.AppendToOutput('Invalid file: ' + active_file.filename)
\r
877 self.displayed_plot = copy.deepcopy(active_file.plot)
\r
878 #add raw curves to plot
\r
879 self.displayed_plot.raw_curves = copy.deepcopy(self.displayed_plot.curves)
\r
880 #apply all active plotmanipulators
\r
881 self.displayed_plot = self.ApplyPlotmanipulators(self.displayed_plot, active_file)
\r
882 #add corrected curves to plot
\r
883 self.displayed_plot.corrected_curves = copy.deepcopy(self.displayed_plot.curves)
\r
886 self.displayed_plot = copy.deepcopy(plot)
\r
888 figure = self.GetActiveFigure()
\r
891 #use '0' instead of e.g. '0.00' for scales
\r
892 use_zero = self.GetBoolFromConfig('core', 'preferences', 'use_zero')
\r
893 #optionally remove the extension from the title of the plot
\r
894 hide_curve_extension = self.GetBoolFromConfig('core', 'preferences', 'hide_curve_extension')
\r
895 if hide_curve_extension:
\r
896 title = lh.remove_extension(self.displayed_plot.title)
\r
898 title = self.displayed_plot.title
\r
899 figure.suptitle(title, fontsize=14)
\r
900 #create the list of all axes necessary (rows and columns)
\r
902 number_of_columns = max([curve.destination.column for curve in self.displayed_plot.curves])
\r
903 number_of_rows = max([curve.destination.row for curve in self.displayed_plot.curves])
\r
904 for index in range(number_of_rows * number_of_columns):
\r
905 axes_list.append(figure.add_subplot(number_of_rows, number_of_columns, index + 1))
\r
907 #add all curves to the corresponding plots
\r
908 for curve in self.displayed_plot.curves:
\r
911 #make sure the titles of 'subplots' do not overlap with the axis labels of the 'main plot'
\r
912 figure.subplots_adjust(hspace=0.3)
\r
915 self.panelResults.ClearResults()
\r
916 if self.displayed_plot.results.has_key(self.results_str):
\r
917 for curve in self.displayed_plot.results[self.results_str].results:
\r
918 add_to_plot(curve, set_scale=False)
\r
919 self.panelResults.DisplayResults(self.displayed_plot.results[self.results_str])
\r
921 self.panelResults.ClearResults()
\r
923 figure.canvas.draw()
\r
925 def _on_curve_select(self, playlist, curve):
\r
926 #create the plot tab and add playlist to the dictionary
\r
927 plotPanel = panel.plot.PlotPanel(self, ID_FirstPlot + len(self.playlists))
\r
928 notebook_tab = self._c['notebook'].AddPage(plotPanel, playlist.name, True)
\r
929 #tab_index = self._c['notebook'].GetSelection()
\r
930 playlist.figure = plotPanel.get_figure()
\r
931 self.playlists[playlist.name] = playlist
\r
932 #self.playlists[playlist.name] = [playlist, figure]
\r
933 self._c['status bar'].set_playlist(playlist)
\r
938 def _on_playlist_left_doubleclick(self):
\r
939 index = self._c['notebook'].GetSelection()
\r
940 current_playlist = self._c['notebook'].GetPageText(index)
\r
941 if current_playlist != playlist_name:
\r
942 index = self._GetPlaylistTab(playlist_name)
\r
943 self._c['notebook'].SetSelection(index)
\r
944 self._c['status bar'].set_playlist(playlist)
\r
948 def _on_playlist_delete(self, playlist):
\r
949 notebook = self.Parent.plotNotebook
\r
950 index = self.Parent._GetPlaylistTab(playlist.name)
\r
951 notebook.SetSelection(index)
\r
952 notebook.DeletePage(notebook.GetSelection())
\r
953 self.Parent.DeleteFromPlaylists(playlist_name)
\r
956 class HookeApp (wx.App):
\r
957 """A :class:`wx.App` wrapper around :class:`HookeFrame`.
\r
959 Tosses up a splash screen and then loads :class:`HookeFrame` in
\r
962 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
\r
964 self.commands = commands
\r
965 self.inqueue = inqueue
\r
966 self.outqueue = outqueue
\r
967 super(HookeApp, self).__init__(*args, **kwargs)
\r
970 self.SetAppName('Hooke')
\r
971 self.SetVendorName('')
\r
972 self._setup_splash_screen()
\r
974 height = int(self.gui.config['main height']) # HACK: config should convert
\r
975 width = int(self.gui.config['main width'])
\r
976 top = int(self.gui.config['main top'])
\r
977 left = int(self.gui.config['main left'])
\r
979 # Sometimes, the ini file gets confused and sets 'left' and
\r
980 # 'top' to large negative numbers. Here we catch and fix
\r
981 # this. Keep small negative numbers, the user might want
\r
989 'frame': HookeFrame(
\r
990 self.gui, self.commands, self.inqueue, self.outqueue,
\r
991 parent=None, title='Hooke',
\r
992 pos=(left, top), size=(width, height),
\r
993 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
\r
995 self._c['frame'].Show(True)
\r
996 self.SetTopWindow(self._c['frame'])
\r
999 def _setup_splash_screen(self):
\r
1000 if self.gui.config['show splash screen']:
\r
1001 path = self.gui.config['splash screen image']
\r
1002 if os.path.isfile(path):
\r
1003 duration = int(self.gui.config['splash screen duration']) # HACK: config should decode types
\r
1005 bitmap=wx.Image(path).ConvertToBitmap(),
\r
1006 splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
\r
1007 milliseconds=duration,
\r
1010 # For some reason splashDuration and sleep do not
\r
1011 # correspond to each other at least not on Windows.
\r
1012 # Maybe it's because duration is in milliseconds and
\r
1013 # sleep in seconds. Thus we need to increase the
\r
1014 # sleep time a bit. A factor of 1.2 seems to work.
\r
1016 time.sleep(sleepFactor * duration / 1000)
\r
1019 class GUI (UserInterface):
\r
1020 """wxWindows graphical user interface.
\r
1022 def __init__(self):
\r
1023 super(GUI, self).__init__(name='gui')
\r
1025 def default_settings(self):
\r
1026 """Return a list of :class:`hooke.config.Setting`\s for any
\r
1027 configurable UI settings.
\r
1029 The suggested section setting is::
\r
1031 Setting(section=self.setting_section, help=self.__doc__)
\r
1034 Setting(section=self.setting_section, help=self.__doc__),
\r
1035 Setting(section=self.setting_section, option='icon image',
\r
1036 value=os.path.join('doc', 'img', 'microscope.ico'),
\r
1037 help='Path to the hooke icon image.'),
\r
1038 Setting(section=self.setting_section, option='show splash screen',
\r
1040 help='Enable/disable the splash screen'),
\r
1041 Setting(section=self.setting_section, option='splash screen image',
\r
1042 value=os.path.join('doc', 'img', 'hooke.jpg'),
\r
1043 help='Path to the Hooke splash screen image.'),
\r
1044 Setting(section=self.setting_section, option='splash screen duration',
\r
1046 help='Duration of the splash screen in milliseconds.'),
\r
1047 Setting(section=self.setting_section, option='perspective path',
\r
1048 value=os.path.join('resources', 'gui', 'perspective'),
\r
1049 help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.
\r
1050 Setting(section=self.setting_section, option='hide extensions',
\r
1052 help='Hide file extensions when displaying names.'),
\r
1053 Setting(section=self.setting_section, option='folders-workdir',
\r
1055 help='This should probably go...'),
\r
1056 Setting(section=self.setting_section, option='folders-filters',
\r
1058 help='This should probably go...'),
\r
1059 Setting(section=self.setting_section, option='active perspective',
\r
1061 help='Name of active perspective file (or "Default").'),
\r
1062 Setting(section=self.setting_section, option='folders-filter-index',
\r
1064 help='This should probably go...'),
\r
1065 Setting(section=self.setting_section, option='main height',
\r
1067 help='Height of main window in pixels.'),
\r
1068 Setting(section=self.setting_section, option='main width',
\r
1070 help='Width of main window in pixels.'),
\r
1071 Setting(section=self.setting_section, option='main top',
\r
1073 help='Pixels from screen top to top of main window.'),
\r
1074 Setting(section=self.setting_section, option='main left',
\r
1076 help='Pixels from screen left to left of main window.'),
\r
1077 Setting(section=self.setting_section, option='selected command',
\r
1078 value='load playlist',
\r
1079 help='Name of the initially selected command.'),
\r
1082 def _app(self, commands, ui_to_command_queue, command_to_ui_queue):
\r
1086 app = HookeApp(gui=self,
\r
1087 commands=commands,
\r
1088 inqueue=ui_to_command_queue,
\r
1089 outqueue=command_to_ui_queue,
\r
1090 redirect=redirect)
\r
1093 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
\r
1094 app = self._app(commands, ui_to_command_queue, command_to_ui_queue)
\r