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
82 'view_panel': self._on_panel_visibility,
\r
83 'save_perspective': self._on_save_perspective,
\r
84 'delete_perspective': self._on_delete_perspective,
\r
86 self.SetMenuBar(self._c['menu bar'])
\r
88 self._c['status bar'] = statusbar.StatusBar(
\r
90 style=wx.ST_SIZEGRIP)
\r
91 self.SetStatusBar(self._c['status bar'])
\r
93 self._update_perspectives()
\r
96 name = self.gui.config['active perspective']
\r
97 return # TODO: cleanup
\r
98 menu_item = self.GetPerspectiveMenuItem(name)
\r
99 if menu_item is not None:
\r
100 self._on_restore_perspective(menu_item)
\r
101 #TODO: config setting to remember playlists from last session
\r
102 self.playlists = self._c['playlists'].Playlists
\r
103 self._displayed_plot = None
\r
104 #load default list, if possible
\r
105 self.do_loadlist(self.GetStringFromConfig('core', 'preferences', 'playlist'))
\r
107 def _setup_panels(self):
\r
108 client_size = self.GetClientSize()
\r
109 for label,p,style in [
\r
110 # ('folders', wx.GenericDirCtrl(
\r
112 # dir=self.gui.config['folders-workdir'],
\r
114 # style=wx.DIRCTRL_SHOW_FILTERS,
\r
115 # filter=self.gui.config['folders-filters'],
\r
116 # defaultFilter=int(self.gui.config['folders-filter-index'])), 'left'), #HACK: config should convert
\r
117 # ('playlists', panel.PANELS['playlist'](
\r
119 # config=self.gui.config,
\r
121 # style=wx.WANTS_CHARS|wx.NO_BORDER,
\r
122 # # WANTS_CHARS so the panel doesn't eat the Return key.
\r
123 # size=(160, 200)), 'left'),
\r
124 # ('note', panel.note.Note(
\r
126 # style=wx.WANTS_CHARS|wx.NO_BORDER,
\r
127 # size=(160, 200)), 'left'),
\r
128 # ('notebook', Notebook(
\r
130 # pos=wx.Point(client_size.x, client_size.y),
\r
131 # size=wx.Size(430, 200),
\r
132 # style=aui.AUI_NB_DEFAULT_STYLE
\r
133 # | aui.AUI_NB_TAB_EXTERNAL_MOVE | wx.NO_BORDER), 'center'),
\r
134 ('commands', panel.PANELS['commands'](
\r
135 commands=self.commands,
\r
136 selected=self.gui.config['selected command'],
\r
138 'execute': self.execute_command,
\r
139 'select_plugin': self.select_plugin,
\r
140 'select_command': self.select_command,
\r
141 # 'selection_changed': self.panelProperties.select(self, method, command), #SelectedTreeItem = selected_item,
\r
144 style=wx.WANTS_CHARS|wx.NO_BORDER,
\r
145 # WANTS_CHARS so the panel doesn't eat the Return key.
\r
148 #('properties', panel.propertyeditor.PropertyEditor(self),'right'),
\r
149 # ('assistant', wx.TextCtrl(
\r
151 # pos=wx.Point(0, 0),
\r
152 # size=wx.Size(150, 90),
\r
153 # style=wx.NO_BORDER|wx.TE_MULTILINE), 'right'),
\r
154 # ('output', wx.TextCtrl(
\r
156 # pos=wx.Point(0, 0),
\r
157 # size=wx.Size(150, 90),
\r
158 # style=wx.NO_BORDER|wx.TE_MULTILINE), 'bottom'),
\r
159 # ('results', panel.results.Results(self), 'bottom'),
\r
161 self._add_panel(label, p, style)
\r
162 #self._c['assistant'].SetEditable(False)
\r
164 def _add_panel(self, label, panel, style):
\r
165 self._c[label] = panel
\r
166 cap_label = label.capitalize()
\r
167 info = aui.AuiPaneInfo().Name(cap_label).Caption(cap_label)
\r
168 info.PaneBorder(False).CloseButton(True).MaximizeButton(False)
\r
171 elif style == 'center':
\r
173 elif style == 'left':
\r
175 elif style == 'right':
\r
178 assert style == 'bottom', style
\r
180 self._c['manager'].AddPane(panel, info)
\r
182 def _setup_toolbars(self):
\r
183 self._c['navigation bar'] = navbar.NavBar(
\r
185 'next': self._next_curve,
\r
186 'previous': self._previous_curve,
\r
189 style=wx.TB_FLAT | wx.TB_NODIVIDER)
\r
190 self._c['manager'].AddPane(
\r
191 self._c['navigation bar'],
\r
192 aui.AuiPaneInfo().Name('Navigation').Caption('Navigation'
\r
193 ).ToolbarPane().Top().Layer(1).Row(1).LeftDockable(False
\r
194 ).RightDockable(False))
\r
196 def _bind_events(self):
\r
197 # TODO: figure out if we can use the eventManager for menu
\r
198 # ranges and events of 'self' without raising an assertion
\r
200 self.Bind(wx.EVT_ERASE_BACKGROUND, self._on_erase_background)
\r
201 self.Bind(wx.EVT_SIZE, self._on_size)
\r
202 self.Bind(wx.EVT_CLOSE, self._on_close)
\r
203 self.Bind(aui.EVT_AUI_PANE_CLOSE, self.OnPaneClose)
\r
204 self.Bind(aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self._on_notebook_page_close)
\r
206 return # TODO: cleanup
\r
207 for value in self._c['menu bar']._c['view']._c.values():
\r
208 self.Bind(wx.EVT_MENU_RANGE, self._on_view, value)
\r
210 self.Bind(wx.EVT_MENU, self._on_save_perspective,
\r
211 self._c['menu bar']._c['perspective']._c['save'])
\r
212 self.Bind(wx.EVT_MENU, self._on_delete_perspective,
\r
213 self._c['menu bar']._c['perspective']._c['delete'])
\r
215 treeCtrl = self._c['folders'].GetTreeCtrl()
\r
216 treeCtrl.Bind(wx.EVT_LEFT_DCLICK, self._on_dir_ctrl_left_double_click)
\r
218 # TODO: playlist callbacks
\r
219 return # TODO: cleanup
\r
220 evtmgr.eventManager.Register(self.OnUpdateNote, wx.EVT_BUTTON, self.panelNote.UpdateButton)
\r
222 self.panelProperties.pg.Bind(wxpg.EVT_PG_CHANGED, self.OnPropGridChanged)
\r
224 self.panelResults.results_list.OnCheckItem = self.OnResultsCheck
\r
226 def _command_by_name(self, name):
\r
227 cs = [c for c in self.commands if c.name == name]
\r
229 raise KeyError(name)
\r
231 raise Exception('Multiple commands named "%s"' % name)
\r
234 def execute_command(self, _class=None, method=None,
\r
235 command=None, args=None):
\r
236 self.inqueue.put(CommandMessage(command, args))
\r
239 msg = self.outqueue.get()
\r
240 results.append(msg)
\r
241 print type(msg), msg
\r
242 if isinstance(msg, Exit):
\r
245 elif isinstance(msg, CommandExit):
\r
246 # TODO: display command complete
\r
248 elif isinstance(msg, ReloadUserInterfaceConfig):
\r
249 self.gui.reload_config(msg.config)
\r
251 elif isinstance(msg, Request):
\r
252 h = handler.HANDLERS[msg.type]
\r
253 h.run(self, msg) # TODO: pause for response?
\r
256 self, '_postprocess_%s' % command.name.replace(' ', '_'), None)
\r
258 pp(command=command, results=results)
\r
261 def _handle_request(self, msg):
\r
262 """Repeatedly try to get a response to `msg`.
\r
265 raise NotImplementedError('_%s_request_prompt' % msg.type)
\r
266 prompt_string = prompt(msg)
\r
267 parser = getattr(self, '_%s_request_parser' % msg.type, None)
\r
269 raise NotImplementedError('_%s_request_parser' % msg.type)
\r
273 self.cmd.stdout.write(''.join([
\r
274 error.__class__.__name__, ': ', str(error), '\n']))
\r
275 self.cmd.stdout.write(prompt_string)
\r
276 value = parser(msg, self.cmd.stdin.readline())
\r
278 response = msg.response(value)
\r
280 except ValueError, error:
\r
282 self.inqueue.put(response)
\r
285 def _GetActiveFileIndex(self):
\r
286 lib.playlist.Playlist = self.GetActivePlaylist()
\r
287 #get the selected item from the tree
\r
288 selected_item = self._c['playlists']._c['tree'].GetSelection()
\r
289 #test if a playlist or a curve was double-clicked
\r
290 if self._c['playlists']._c['tree'].ItemHasChildren(selected_item):
\r
294 selected_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item)
\r
295 while selected_item.IsOk():
\r
297 selected_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item)
\r
300 def _GetPlaylistTab(self, name):
\r
301 for index, page in enumerate(self._c['notebook']._tabs._pages):
\r
302 if page.caption == name:
\r
306 def _restore_perspective(self, name):
\r
308 self.gui.config['active perspective'] = name # TODO: push to engine's Hooke
\r
309 self._c['manager'].LoadPerspective(self._perspectives[name])
\r
310 self._c['manager'].Update()
\r
311 for pane in self._c['manager'].GetAllPanes():
\r
312 if pane.name in self._c['menu bar']._c['view']._c.keys():
\r
313 pane.Check(pane.window.IsShown())
\r
315 def _SavePerspectiveToFile(self, name, perspective):
\r
316 filename = ''.join([name, '.txt'])
\r
317 filename = lh.get_file_path(filename, ['perspective'])
\r
318 perspectivesFile = open(filename, 'w')
\r
319 perspectivesFile.write(perspective)
\r
320 perspectivesFile.close()
\r
322 def select_plugin(self, _class=None, method=None, plugin=None):
\r
323 for option in config[section]:
\r
324 properties.append([option, config[section][option]])
\r
326 def select_command(self, _class, method, command):
\r
327 self.select_plugin(plugin=command.plugin)
\r
328 plugin = self.GetItemText(selected_item)
\r
329 if plugin != 'core':
\r
330 doc_string = eval('plugins.' + plugin + '.' + plugin + 'Commands.__doc__')
\r
332 doc_string = 'The module "core" contains Hooke core functionality'
\r
333 if doc_string is not None:
\r
334 self.panelAssistant.ChangeValue(doc_string)
\r
336 self.panelAssistant.ChangeValue('')
\r
337 panel.propertyeditor.PropertyEditor.Initialize(self.panelProperties, properties)
\r
338 self.gui.config['selected command'] = command
\r
340 def AddPlaylistFromFiles(self, files=[], name='Untitled'):
\r
342 playlist = lib.playlist.Playlist(self, self.drivers)
\r
344 playlist.add_curve(item)
\r
345 if playlist.count > 0:
\r
346 playlist.name = self._GetUniquePlaylistName(name)
\r
348 self.AddTayliss(playlist)
\r
350 def AppendToOutput(self, text):
\r
351 self.panelOutput.AppendText(''.join([text, '\n']))
\r
353 def AppliesPlotmanipulator(self, name):
\r
355 Returns True if the plotmanipulator 'name' is applied, False otherwise
\r
356 name does not contain 'plotmanip_', just the name of the plotmanipulator (e.g. 'flatten')
\r
358 return self.GetBoolFromConfig('core', 'plotmanipulators', name)
\r
360 def ApplyPlotmanipulators(self, plot, plot_file):
\r
362 Apply all active plotmanipulators.
\r
364 if plot is not None and plot_file is not None:
\r
365 manipulated_plot = copy.deepcopy(plot)
\r
366 for plotmanipulator in self.plotmanipulators:
\r
367 if self.GetBoolFromConfig('core', 'plotmanipulators', plotmanipulator.name):
\r
368 manipulated_plot = plotmanipulator.method(manipulated_plot, plot_file)
\r
369 return manipulated_plot
\r
371 def GetActiveFigure(self):
\r
372 playlist_name = self.GetActivePlaylistName()
\r
373 figure = self.playlists[playlist_name].figure
\r
374 if figure is not None:
\r
378 def GetActiveFile(self):
\r
379 playlist = self.GetActivePlaylist()
\r
380 if playlist is not None:
\r
381 return playlist.get_active_file()
\r
384 def GetActivePlot(self):
\r
385 playlist = self.GetActivePlaylist()
\r
386 if playlist is not None:
\r
387 return playlist.get_active_file().plot
\r
390 def GetDisplayedPlot(self):
\r
391 plot = copy.deepcopy(self.displayed_plot)
\r
393 #plot.curves = copy.deepcopy(plot.curves)
\r
396 def GetDisplayedPlotCorrected(self):
\r
397 plot = copy.deepcopy(self.displayed_plot)
\r
399 plot.curves = copy.deepcopy(plot.corrected_curves)
\r
402 def GetDisplayedPlotRaw(self):
\r
403 plot = copy.deepcopy(self.displayed_plot)
\r
405 plot.curves = copy.deepcopy(plot.raw_curves)
\r
408 def GetDockArt(self):
\r
409 return self._c['manager'].GetArtProvider()
\r
411 def GetPlotmanipulator(self, name):
\r
413 Returns a plot manipulator function from its name
\r
415 for plotmanipulator in self.plotmanipulators:
\r
416 if plotmanipulator.name == name:
\r
417 return plotmanipulator
\r
420 def GetPerspectiveMenuItem(self, name):
\r
421 if self._perspectives.has_key(name):
\r
422 perspectives_list = [key for key, value in self._perspectives.iteritems()]
\r
423 perspectives_list.sort()
\r
424 index = perspectives_list.index(name)
\r
425 perspective_Id = ID_FirstPerspective + index
\r
426 menu_item = self._c['menu bar'].FindItemById(perspective_Id)
\r
431 def HasPlotmanipulator(self, name):
\r
433 returns True if the plotmanipulator 'name' is loaded, False otherwise
\r
435 for plotmanipulator in self.plotmanipulators:
\r
436 if plotmanipulator.command == name:
\r
440 def _on_about(self, *args):
\r
441 dialog = wx.MessageDialog(
\r
443 message=self.gui._splash_text(),
\r
444 caption='About Hooke',
\r
445 style=wx.OK|wx.ICON_INFORMATION)
\r
449 def _on_close(self, *args):
\r
451 self.gui.config['main height'] = str(self.GetSize().GetHeight())
\r
452 self.gui.config['main left'] = str(self.GetPosition()[0])
\r
453 self.gui.config['main top'] = str(self.GetPosition()[1])
\r
454 self.gui.config['main width'] = str(self.GetSize().GetWidth())
\r
455 # push changes back to Hooke.config?
\r
456 self._c['manager'].UnInit()
\r
457 del self._c['manager']
\r
460 def _update_perspectives(self):
\r
461 """Add perspectives to menubar and _perspectives.
\r
463 self._perspectives = {
\r
464 'Default': self._c['manager'].SavePerspective(),
\r
466 path = self.gui.config['perspective path']
\r
467 if os.path.isdir(path):
\r
468 files = sorted(os.listdir(path))
\r
469 for fname in files:
\r
470 name, extension = os.path.splitext(fname)
\r
471 if extension != '.txt':
\r
473 fpath = os.path.join(path, fpath)
\r
474 if not os.path.isfile(fpath):
\r
477 with open(fpath, 'rU') as f:
\r
478 perspective = f.readline()
\r
480 self._perspectives[name] = perspective
\r
482 selected_perspective = self.gui.config['active perspective']
\r
483 if not self._perspectives.has_key(selected_perspective):
\r
484 self.gui.config['active perspective'] = 'Default' # TODO: push to engine's Hooke
\r
486 self._update_perspective_menu()
\r
487 self._restore_perspective(selected_perspective)
\r
489 def _update_perspective_menu(self):
\r
490 self._c['menu bar']._c['perspective'].update(
\r
491 sorted(self._perspectives.keys()),
\r
492 self.gui.config['active perspective'],
\r
493 self._on_restore_perspective)
\r
495 def _on_restore_perspective(self, event):
\r
496 name = self._c['menu bar'].FindItemById(event.GetId()).GetLabel()
\r
497 self._restore_perspective(name)
\r
499 def _on_save_perspective(self, event):
\r
500 def nameExists(name):
\r
501 menu_position = self._c['menu bar'].FindMenu('Perspective')
\r
502 menu = self._c['menu bar'].GetMenu(menu_position)
\r
503 for item in menu.GetMenuItems():
\r
504 if item.GetText() == name:
\r
510 dialog = wx.TextEntryDialog(self, 'Enter a name for the new perspective:', 'Save perspective')
\r
511 dialog.SetValue('New perspective')
\r
512 if dialog.ShowModal() != wx.ID_OK:
\r
515 name = dialog.GetValue()
\r
517 if nameExists(name):
\r
518 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
519 if dialogConfirm.ShowModal() == wx.ID_YES:
\r
524 perspective = self._c['manager'].SavePerspective()
\r
525 self._SavePerspectiveToFile(name, perspective)
\r
526 self.gui.config['active perspectives'] = name
\r
527 self._update_perspective_menu()
\r
528 # if nameExists(name):
\r
529 # #check the corresponding menu item
\r
530 # menu_item = self.GetPerspectiveMenuItem(name)
\r
531 # #replace the perspectiveStr in _pespectives
\r
532 # self._perspectives[name] = perspective
\r
534 # #because we deal with radio items, we need to do some extra work
\r
535 # #delete all menu items from the perspectives menu
\r
536 # for item in self._perspectives_menu.GetMenuItems():
\r
537 # self._perspectives_menu.DeleteItem(item)
\r
538 # #recreate the perspectives menu
\r
539 # self._perspectives_menu.Append(ID_SavePerspective, 'Save Perspective')
\r
540 # self._perspectives_menu.Append(ID_DeletePerspective, 'Delete Perspective')
\r
541 # self._perspectives_menu.AppendSeparator()
\r
542 # #convert the perspectives dictionary into a list
\r
543 # # the list contains:
\r
544 # #[0]: name of the perspective
\r
545 # #[1]: perspective
\r
546 # perspectives_list = [key for key, value in self._perspectives.iteritems()]
\r
547 # perspectives_list.append(name)
\r
548 # perspectives_list.sort()
\r
549 # #add all previous perspectives
\r
550 # for index, item in enumerate(perspectives_list):
\r
551 # menu_item = self._perspectives_menu.AppendRadioItem(ID_FirstPerspective + index, item)
\r
553 # menu_item.Check()
\r
554 # #add the new perspective to _perspectives
\r
555 # self._perspectives[name] = perspective
\r
557 def _on_delete_perspective(self, event):
\r
558 dialog = panel.selection.Selection(
\r
559 options=sorted(os.listdir(self.gui.config['perspective path'])),
\r
560 message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
\r
561 button_id=wx.ID_DELETE,
\r
562 callbacks={'button': self._on_delete_perspective},
\r
563 selection_style='multiple',
\r
565 label='Delete perspective(s)',
\r
566 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
\r
567 dialog.CenterOnScreen()
\r
570 self._update_perspective_menu()
\r
571 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
\r
572 # http://trac.wxwidgets.org/ticket/3258
\r
573 # ) that makes the radio item indicator in the menu disappear.
\r
574 # The code should be fine once this issue is fixed.
\r
576 def _on_delete_perspective(self, _class, method, options, selected):
\r
578 self._perspectives.remove(p)
\r
579 if p == self.gui.config['active perspective']:
\r
580 self.gui.config['active perspective'] = 'Default'
\r
581 path = os.path.join(self.gui.config['perspective path'],
\r
584 self._update_perspective_menu()
\r
586 def _on_dir_ctrl_left_double_click(self, event):
\r
587 file_path = self.panelFolders.GetPath()
\r
588 if os.path.isfile(file_path):
\r
589 if file_path.endswith('.hkp'):
\r
590 self.do_loadlist(file_path)
\r
593 def _on_erase_background(self, event):
\r
596 def _next_curve(self, *args):
\r
597 """Call the `next curve` command.
\r
599 results = self.execute_command(
\r
600 command=self._command_by_name('next curve'))
\r
601 if isinstance(results[-1], Success):
\r
602 self.execute_command(
\r
603 command=self._command_by_name('get curve'))
\r
605 def _previous_curve(self, *args):
\r
606 """Call the `previous curve` command.
\r
608 self.execute_command(
\r
609 command=self._command_by_name('previous curve'))
\r
610 if isinstance(results[-1], Success):
\r
611 self.execute_command(
\r
612 command=self._command_by_name('get curve'))
\r
614 def _postprocess_get_curve(self, command, results):
\r
615 """Update `self` to show the curve.
\r
617 if not isinstance(results[-1], Success):
\r
618 return # error executing 'get curve'
\r
619 assert len(results) == 2, results
\r
623 selected_item = self._c['playlists']._c['tree'].GetSelection()
\r
624 if self._c['playlists']._c['tree'].ItemHasChildren(selected_item):
\r
625 #GetFirstChild returns a tuple
\r
626 #we only need the first element
\r
627 next_item = self._c['playlists']._c['tree'].GetFirstChild(selected_item)[0]
\r
629 next_item = self._c['playlists']._c['tree'].GetNextSibling(selected_item)
\r
630 if not next_item.IsOk():
\r
631 parent_item = self._c['playlists']._c['tree'].GetItemParent(selected_item)
\r
632 #GetFirstChild returns a tuple
\r
633 #we only need the first element
\r
634 next_item = self._c['playlists']._c['tree'].GetFirstChild(parent_item)[0]
\r
635 self._c['playlists']._c['tree'].SelectItem(next_item, True)
\r
636 if not self._c['playlists']._c['tree'].ItemHasChildren(selected_item):
\r
637 playlist = self.GetActivePlaylist()
\r
638 if playlist.count > 1:
\r
640 self._c['status bar'].set_playlist(playlist)
\r
644 def _on_notebook_page_close(self, event):
\r
645 ctrl = event.GetEventObject()
\r
646 playlist_name = ctrl.GetPageText(ctrl._curpage)
\r
647 self.DeleteFromPlaylists(playlist_name)
\r
649 def OnPaneClose(self, event):
\r
652 def OnPropGridChanged (self, event):
\r
653 prop = event.GetProperty()
\r
655 item_section = self.panelProperties.SelectedTreeItem
\r
656 item_plugin = self._c['commands']._c['tree'].GetItemParent(item_section)
\r
657 plugin = self._c['commands']._c['tree'].GetItemText(item_plugin)
\r
658 config = self.gui.config[plugin]
\r
659 property_section = self._c['commands']._c['tree'].GetItemText(item_section)
\r
660 property_key = prop.GetName()
\r
661 property_value = prop.GetDisplayedString()
\r
663 config[property_section][property_key]['value'] = property_value
\r
665 def OnResultsCheck(self, index, flag):
\r
666 results = self.GetActivePlot().results
\r
667 if results.has_key(self.results_str):
\r
668 results[self.results_str].results[index].visible = flag
\r
669 results[self.results_str].update()
\r
673 def _on_size(self, event):
\r
676 def OnUpdateNote(self, event):
\r
678 Saves the note to the active file.
\r
680 active_file = self.GetActiveFile()
\r
681 active_file.note = self.panelNote.Editor.GetValue()
\r
683 def _on_panel_visibility(self, _class, method, panel_name, visible):
\r
684 pane = self._c['manager'].GetPane(panel_name)
\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