3 """Defines :class:`GUI` providing a wxWindows 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 ... import version
\r
30 from ...command import CommandExit, Exit, Command, Argument, StoreValue
\r
31 from ...config import Setting
\r
32 from ...interaction import Request, BooleanRequest, ReloadUserInterfaceConfig
\r
33 from ...ui import UserInterface, CommandMessage
\r
34 from . import menu as menu
\r
35 from . import navbar as navbar
\r
36 from . import panel as panel
\r
37 from . import prettyformat as prettyformat
\r
38 from . import statusbar as statusbar
\r
41 class HookeFrame (wx.Frame):
\r
42 """The main Hooke-interface window.
\r
46 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
\r
47 super(HookeFrame, self).__init__(*args, **kwargs)
\r
49 self.commands = commands
\r
50 self.inqueue = inqueue
\r
51 self.outqueue = outqueue
\r
52 self._perspectives = {} # {name: perspective_str}
\r
55 self.SetIcon(wx.Icon(self.gui.config['icon image'], wx.BITMAP_TYPE_ICO))
\r
57 # setup frame manager
\r
58 self._c['manager'] = aui.AuiManager()
\r
59 self._c['manager'].SetManagedWindow(self)
\r
61 # set the gradient and drag styles
\r
62 self._c['manager'].GetArtProvider().SetMetric(
\r
63 aui.AUI_DOCKART_GRADIENT_TYPE, aui.AUI_GRADIENT_NONE)
\r
64 self._c['manager'].SetFlags(
\r
65 self._c['manager'].GetFlags() ^ aui.AUI_MGR_TRANSPARENT_DRAG)
\r
67 # Min size for the frame itself isn't completely done. See
\r
68 # the end of FrameManager::Update() for the test code. For
\r
69 # now, just hard code a frame minimum size.
\r
70 self.SetMinSize(wx.Size(500, 500))
\r
72 self._setup_panels()
\r
73 self._setup_toolbars()
\r
74 self._c['manager'].Update() # commit pending changes
\r
76 # Create the menubar after the panes so that the default
\r
77 # perspective is created with all panes open
\r
78 self._c['menu bar'] = menu.MenuBar(
\r
81 self.SetMenuBar(self._c['menu bar'])
\r
83 self._c['status bar'] = statubar.StatusBar(
\r
85 style=wx.ST_SIZEGRIP)
\r
87 self._update_perspectives()
\r
90 name = self.gui.config['active perspective']
\r
91 return # TODO: cleanup
\r
92 menu_item = self.GetPerspectiveMenuItem(name)
\r
93 if menu_item is not None:
\r
94 self._on_restore_perspective(menu_item)
\r
95 #TODO: config setting to remember playlists from last session
\r
96 self.playlists = self._c['playlists'].Playlists
\r
97 self._displayed_plot = None
\r
98 #load default list, if possible
\r
99 self.do_loadlist(self.GetStringFromConfig('core', 'preferences', 'playlist'))
\r
101 def _setup_panels(self):
\r
102 client_size = self.GetClientSize()
\r
103 for label,p,style in [
\r
104 ('folders', wx.GenericDirCtrl(
\r
106 dir=self.gui.config['folders-workdir'],
\r
108 style=wx.DIRCTRL_SHOW_FILTERS,
\r
109 filter=self.gui.config['folders-filters'],
\r
110 defaultFilter=int(self.gui.config['folders-filter-index'])), 'left'), #HACK: config should convert
\r
111 ('playlists', panel.playlist.Playlist(
\r
112 config=self.gui.config,
\r
115 style=wx.WANTS_CHARS|wx.NO_BORDER,
\r
116 # WANTS_CHARS so the panel doesn't eat the Return key.
\r
117 size=(160, 200)), 'left'),
\r
118 ('note', panel.note.Note(self), 'left'),
\r
119 ('notebook', Notebook(
\r
121 pos=wx.Point(client_size.x, client_size.y),
\r
122 size=wx.Size(430, 200),
\r
123 style=aui.AUI_NB_DEFAULT_STYLE
\r
124 | aui.AUI_NB_TAB_EXTERNAL_MOVE | wx.NO_BORDER), 'center'),
\r
125 ('commands', panel.commands.Commands(
\r
126 commands=self.commands,
\r
127 selected=self.gui.config['selected command'],
\r
129 'execute': self.execute_command,
\r
130 'select_plugin': self.select_plugin,
\r
131 'select_command': self.select_command,
\r
132 # 'selection_changed': self.panelProperties.select(self, method, command), #SelectedTreeItem = selected_item,
\r
135 style=wx.WANTS_CHARS|wx.NO_BORDER,
\r
136 # WANTS_CHARS so the panel doesn't eat the Return key.
\r
137 size=(160, 200)), 'right'),
\r
138 #('properties', panel.propertyeditor.PropertyEditor(self),'right'),
\r
139 ('assistant', wx.TextCtrl(
\r
141 pos=wx.Point(0, 0),
\r
142 size=wx.Size(150, 90),
\r
143 style=wx.NO_BORDER|wx.TE_MULTILINE), 'right'),
\r
144 ('output', wx.TextCtrl(
\r
146 pos=wx.Point(0, 0),
\r
147 size=wx.Size(150, 90),
\r
148 style=wx.NO_BORDER|wx.TE_MULTILINE), 'bottom'),
\r
149 ('results', panel.results.Results(self), 'bottom'),
\r
151 self._add_panel(label, p, style)
\r
152 self._c['assistant'].SetEditable(False)
\r
154 def _add_panel(self, label, panel, style):
\r
155 self._c[label] = panel
\r
156 cap_label = label.capitalize()
\r
157 info = aui.AuiPaneInfo().Name(cap_label).Caption(cap_label)
\r
158 if style == 'left':
\r
159 info.Left().CloseButton(True).MaximizeButton(False)
\r
160 elif style == 'center':
\r
161 info.CenterPane().PaneBorder(False)
\r
162 elif style == 'right':
\r
163 info.Right().CloseButton(True).MaximizeButton(False)
\r
165 assert style == 'bottom', style
\r
166 info.Bottom().CloseButton(True).MaximizeButton(False)
\r
167 self._c['manager'].AddPane(panel, info)
\r
169 def _setup_toolbars(self):
\r
170 self._c['navbar'] = navbar.NavBar(
\r
172 'next': self._next_curve,
\r
173 'previous': self._previous_curve,
\r
176 style=wx.TB_FLAT | wx.TB_NODIVIDER)
\r
178 self._c['manager'].AddPane(
\r
180 aui.AuiPaneInfo().Name('Navigation').Caption('Navigation'
\r
181 ).ToolbarPane().Top().Layer(1).Row(1).LeftDockable(False
\r
182 ).RightDockable(False))
\r
184 def _bind_events(self):
\r
185 # TODO: figure out if we can use the eventManager for menu
\r
186 # ranges and events of 'self' without raising an assertion
\r
188 self.Bind(wx.EVT_ERASE_BACKGROUND, self._on_erase_background)
\r
189 self.Bind(wx.EVT_SIZE, self._on_size)
\r
190 self.Bind(wx.EVT_CLOSE, self._on_close)
\r
191 self.Bind(wx.EVT_MENU, self._on_close, id=wx.ID_EXIT)
\r
192 self.Bind(wx.EVT_MENU, self._on_about, id=wx.ID_ABOUT)
\r
193 self.Bind(aui.EVT_AUI_PANE_CLOSE, self.OnPaneClose)
\r
194 self.Bind(aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self._on_notebook_page_close)
\r
196 for value in self._c['menu bar']._c['view']._c.values():
\r
197 self.Bind(wx.EVT_MENU_RANGE, self._on_view, value)
\r
199 self.Bind(wx.EVT_MENU, self._on_save_perspective,
\r
200 self._c['menu bar']._c['perspective']._c['save'])
\r
201 self.Bind(wx.EVT_MENU, self._on_delete_perspective,
\r
202 self._c['menu bar']._c['perspective']._c['delete'])
\r
204 treeCtrl = self._c['folders'].GetTreeCtrl()
\r
205 treeCtrl.Bind(wx.EVT_LEFT_DCLICK, self._on_dir_ctrl_left_double_click)
\r
207 # TODO: playlist callbacks
\r
208 return # TODO: cleanup
\r
209 evtmgr.eventManager.Register(self.OnUpdateNote, wx.EVT_BUTTON, self.panelNote.UpdateButton)
\r
211 self.panelProperties.pg.Bind(wxpg.EVT_PG_CHANGED, self.OnPropGridChanged)
\r
213 self.panelResults.results_list.OnCheckItem = self.OnResultsCheck
\r
215 def _GetActiveFileIndex(self):
\r
216 lib.playlist.Playlist = self.GetActivePlaylist()
\r
217 #get the selected item from the tree
\r
218 selected_item = self._c['playlists']._c['tree'].GetSelection()
\r
219 #test if a playlist or a curve was double-clicked
\r
220 if self._c['playlists']._c['tree'].ItemHasChildren(selected_item):
\r
224 selected_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item)
\r
225 while selected_item.IsOk():
\r
227 selected_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item)
\r
230 def _GetPlaylistTab(self, name):
\r
231 for index, page in enumerate(self._c['notebook']._tabs._pages):
\r
232 if page.caption == name:
\r
236 def _restore_perspective(self, name):
\r
238 self.gui.config['active perspective'] = name # TODO: push to engine's Hooke
\r
239 self._c['manager'].LoadPerspective(self._perspectives[name])
\r
240 self._c['manager'].Update()
\r
241 for pane in self._c['manager'].GetAllPanes():
\r
242 if pane.name in self._c['menu bar']._c['view']._c.keys():
\r
243 pane.Check(pane.window.IsShown())
\r
245 def _SavePerspectiveToFile(self, name, perspective):
\r
246 filename = ''.join([name, '.txt'])
\r
247 filename = lh.get_file_path(filename, ['perspective'])
\r
248 perspectivesFile = open(filename, 'w')
\r
249 perspectivesFile.write(perspective)
\r
250 perspectivesFile.close()
\r
252 def execute_command(self, _class, method, command, args):
\r
253 self.cmd.inqueue.put(CommandMessage(command, args))
\r
255 msg = self.cmd.outqueue.get()
\r
256 if isinstance(msg, Exit):
\r
258 elif isinstance(msg, CommandExit):
\r
259 self.cmd.stdout.write(msg.__class__.__name__+'\n')
\r
260 self.cmd.stdout.write(str(msg).rstrip()+'\n')
\r
262 elif isinstance(msg, ReloadUserInterfaceConfig):
\r
263 self.cmd.ui.reload_config(msg.config)
\r
265 elif isinstance(msg, Request):
\r
266 self._handle_request(msg)
\r
268 self.cmd.stdout.write(str(msg).rstrip()+'\n')
\r
269 #TODO: run the command
\r
270 #command = ''.join(['self.do_', item_text, '()'])
\r
271 #self.AppendToOutput(command + '\n')
\r
274 def select_plugin(self, _class, method, plugin):
\r
275 for option in config[section]:
\r
276 properties.append([option, config[section][option]])
\r
278 def select_command(self, _class, method, command):
\r
279 self.select_plugin(command.plugin)
\r
280 plugin = self.GetItemText(selected_item)
\r
281 if plugin != 'core':
\r
282 doc_string = eval('plugins.' + plugin + '.' + plugin + 'Commands.__doc__')
\r
284 doc_string = 'The module "core" contains Hooke core functionality'
\r
285 if doc_string is not None:
\r
286 self.panelAssistant.ChangeValue(doc_string)
\r
288 self.panelAssistant.ChangeValue('')
\r
289 panel.propertyeditor.PropertyEditor.Initialize(self.panelProperties, properties)
\r
290 self.gui.config['selected command'] = command
\r
292 def AddPlaylistFromFiles(self, files=[], name='Untitled'):
\r
294 playlist = lib.playlist.Playlist(self, self.drivers)
\r
296 playlist.add_curve(item)
\r
297 if playlist.count > 0:
\r
298 playlist.name = self._GetUniquePlaylistName(name)
\r
300 self.AddTayliss(playlist)
\r
302 def AppendToOutput(self, text):
\r
303 self.panelOutput.AppendText(''.join([text, '\n']))
\r
305 def AppliesPlotmanipulator(self, name):
\r
307 Returns True if the plotmanipulator 'name' is applied, False otherwise
\r
308 name does not contain 'plotmanip_', just the name of the plotmanipulator (e.g. 'flatten')
\r
310 return self.GetBoolFromConfig('core', 'plotmanipulators', name)
\r
312 def ApplyPlotmanipulators(self, plot, plot_file):
\r
314 Apply all active plotmanipulators.
\r
316 if plot is not None and plot_file is not None:
\r
317 manipulated_plot = copy.deepcopy(plot)
\r
318 for plotmanipulator in self.plotmanipulators:
\r
319 if self.GetBoolFromConfig('core', 'plotmanipulators', plotmanipulator.name):
\r
320 manipulated_plot = plotmanipulator.method(manipulated_plot, plot_file)
\r
321 return manipulated_plot
\r
323 def GetActiveFigure(self):
\r
324 playlist_name = self.GetActivePlaylistName()
\r
325 figure = self.playlists[playlist_name].figure
\r
326 if figure is not None:
\r
330 def GetActiveFile(self):
\r
331 playlist = self.GetActivePlaylist()
\r
332 if playlist is not None:
\r
333 return playlist.get_active_file()
\r
336 def GetActivePlot(self):
\r
337 playlist = self.GetActivePlaylist()
\r
338 if playlist is not None:
\r
339 return playlist.get_active_file().plot
\r
342 def GetDisplayedPlot(self):
\r
343 plot = copy.deepcopy(self.displayed_plot)
\r
345 #plot.curves = copy.deepcopy(plot.curves)
\r
348 def GetDisplayedPlotCorrected(self):
\r
349 plot = copy.deepcopy(self.displayed_plot)
\r
351 plot.curves = copy.deepcopy(plot.corrected_curves)
\r
354 def GetDisplayedPlotRaw(self):
\r
355 plot = copy.deepcopy(self.displayed_plot)
\r
357 plot.curves = copy.deepcopy(plot.raw_curves)
\r
360 def GetDockArt(self):
\r
361 return self._c['manager'].GetArtProvider()
\r
363 def GetPlotmanipulator(self, name):
\r
365 Returns a plot manipulator function from its name
\r
367 for plotmanipulator in self.plotmanipulators:
\r
368 if plotmanipulator.name == name:
\r
369 return plotmanipulator
\r
372 def GetPerspectiveMenuItem(self, name):
\r
373 if self._perspectives.has_key(name):
\r
374 perspectives_list = [key for key, value in self._perspectives.iteritems()]
\r
375 perspectives_list.sort()
\r
376 index = perspectives_list.index(name)
\r
377 perspective_Id = ID_FirstPerspective + index
\r
378 menu_item = self.MenuBar.FindItemById(perspective_Id)
\r
383 def HasPlotmanipulator(self, name):
\r
385 returns True if the plotmanipulator 'name' is loaded, False otherwise
\r
387 for plotmanipulator in self.plotmanipulators:
\r
388 if plotmanipulator.command == name:
\r
392 def _on_about(self, event):
\r
393 message = 'Hooke\n\n'+\
\r
394 'A free, open source data analysis platform\n\n'+\
\r
395 'Copyright 2006-2008 by Massimo Sandal\n'+\
\r
396 'Copyright 2010 by Dr. Rolf Schmidt\n\n'+\
\r
397 'Hooke is released under the GNU General Public License version 2.'
\r
398 dialog = wx.MessageDialog(self, message, 'About Hooke', wx.OK | wx.ICON_INFORMATION)
\r
402 def _on_close(self, event):
\r
404 self.gui.config['main height'] = str(self.GetSize().GetHeight())
\r
405 self.gui.config['main left'] = str(self.GetPosition()[0])
\r
406 self.gui.config['main top'] = str(self.GetPosition()[1])
\r
407 self.gui.config['main width'] = str(self.GetSize().GetWidth())
\r
408 # push changes back to Hooke.config?
\r
409 self._c['manager'].UnInit()
\r
410 del self._c['manager']
\r
413 def _update_perspectives(self):
\r
414 """Add perspectives to menubar and _perspectives.
\r
416 self._perspectives = {
\r
417 'Default': self._c['manager'].SavePerspective(),
\r
419 path = self.gui.config['perspective path']
\r
420 if os.path.isdir(path):
\r
421 files = sorted(os.listdir(path))
\r
422 for fname in files:
\r
423 name, extension = os.path.splitext(fname)
\r
424 if extension != '.txt':
\r
426 fpath = os.path.join(path, fpath)
\r
427 if not os.path.isfile(fpath):
\r
430 with open(fpath, 'rU') as f:
\r
431 perspective = f.readline()
\r
433 self._perspectives[name] = perspective
\r
435 selected_perspective = self.gui.config['active perspective']
\r
436 if not self._perspectives.has_key(selected_perspective):
\r
437 self.gui.config['active perspective'] = 'Default' # TODO: push to engine's Hooke
\r
439 self._update_perspective_menu()
\r
440 self._restore_perspective(selected_perspective)
\r
442 def _update_perspective_menu(self):
\r
443 self._c['menu bar']._c['perspective'].update(
\r
444 sorted(self._perspectives.keys()),
\r
445 self.gui.config['active perspective'],
\r
446 self._on_restore_perspective)
\r
448 def _on_restore_perspective(self, event):
\r
449 name = self.MenuBar.FindItemById(event.GetId()).GetLabel()
\r
450 self._restore_perspective(name)
\r
452 def _on_save_perspective(self, event):
\r
453 def nameExists(name):
\r
454 menu_position = self.MenuBar.FindMenu('Perspective')
\r
455 menu = self.MenuBar.GetMenu(menu_position)
\r
456 for item in menu.GetMenuItems():
\r
457 if item.GetText() == name:
\r
463 dialog = wx.TextEntryDialog(self, 'Enter a name for the new perspective:', 'Save perspective')
\r
464 dialog.SetValue('New perspective')
\r
465 if dialog.ShowModal() != wx.ID_OK:
\r
468 name = dialog.GetValue()
\r
470 if nameExists(name):
\r
471 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
472 if dialogConfirm.ShowModal() == wx.ID_YES:
\r
477 perspective = self._c['manager'].SavePerspective()
\r
478 self._SavePerspectiveToFile(name, perspective)
\r
479 self.gui.config['active perspectives'] = name
\r
480 self._update_perspective_menu()
\r
481 # if nameExists(name):
\r
482 # #check the corresponding menu item
\r
483 # menu_item = self.GetPerspectiveMenuItem(name)
\r
484 # #replace the perspectiveStr in _pespectives
\r
485 # self._perspectives[name] = perspective
\r
487 # #because we deal with radio items, we need to do some extra work
\r
488 # #delete all menu items from the perspectives menu
\r
489 # for item in self._perspectives_menu.GetMenuItems():
\r
490 # self._perspectives_menu.DeleteItem(item)
\r
491 # #recreate the perspectives menu
\r
492 # self._perspectives_menu.Append(ID_SavePerspective, 'Save Perspective')
\r
493 # self._perspectives_menu.Append(ID_DeletePerspective, 'Delete Perspective')
\r
494 # self._perspectives_menu.AppendSeparator()
\r
495 # #convert the perspectives dictionary into a list
\r
496 # # the list contains:
\r
497 # #[0]: name of the perspective
\r
498 # #[1]: perspective
\r
499 # perspectives_list = [key for key, value in self._perspectives.iteritems()]
\r
500 # perspectives_list.append(name)
\r
501 # perspectives_list.sort()
\r
502 # #add all previous perspectives
\r
503 # for index, item in enumerate(perspectives_list):
\r
504 # menu_item = self._perspectives_menu.AppendRadioItem(ID_FirstPerspective + index, item)
\r
506 # menu_item.Check()
\r
507 # #add the new perspective to _perspectives
\r
508 # self._perspectives[name] = perspective
\r
510 def _on_delete_perspective(self, event):
\r
511 dialog = panel.selection.Selection(
\r
512 options=sorted(os.listdir(self.gui.config['perspective path'])),
\r
513 message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
\r
514 button_id=wx.ID_DELETE,
\r
515 button_callback=self._on_delete_perspective,
\r
517 label='Delete perspective(s)',
\r
518 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
\r
519 dialog.CenterOnScreen()
\r
522 self._update_perspective_menu()
\r
523 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
\r
524 # http://trac.wxwidgets.org/ticket/3258
\r
525 # ) that makes the radio item indicator in the menu disappear.
\r
526 # The code should be fine once this issue is fixed.
\r
528 def _on_delete_perspective(self, event, items, selected_items):
\r
529 for item in selected_items:
\r
530 self._perspectives.remove(item)
\r
531 if item == self.gui.config['active perspective']:
\r
532 self.gui.config['active perspective'] = 'Default'
\r
533 path = os.path.join(self.gui.config['perspective path'],
\r
536 self._update_perspective_menu()
\r
538 def _on_dir_ctrl_left_double_click(self, event):
\r
539 file_path = self.panelFolders.GetPath()
\r
540 if os.path.isfile(file_path):
\r
541 if file_path.endswith('.hkp'):
\r
542 self.do_loadlist(file_path)
\r
545 def _on_erase_background(self, event):
\r
548 def _next_curve(self, *args):
\r
551 Go to the next curve in the playlist.
\r
552 If we are at the last curve, we come back to the first.
\r
556 selected_item = self._c['playlists']._c['tree'].GetSelection()
\r
557 if self._c['playlists']._c['tree'].ItemHasChildren(selected_item):
\r
558 #GetFirstChild returns a tuple
\r
559 #we only need the first element
\r
560 next_item = self._c['playlists']._c['tree'].GetFirstChild(selected_item)[0]
\r
562 next_item = self._c['playlists']._c['tree'].GetNextSibling(selected_item)
\r
563 if not next_item.IsOk():
\r
564 parent_item = self._c['playlists']._c['tree'].GetItemParent(selected_item)
\r
565 #GetFirstChild returns a tuple
\r
566 #we only need the first element
\r
567 next_item = self._c['playlists']._c['tree'].GetFirstChild(parent_item)[0]
\r
568 self._c['playlists']._c['tree'].SelectItem(next_item, True)
\r
569 if not self._c['playlists']._c['tree'].ItemHasChildren(selected_item):
\r
570 playlist = self.GetActivePlaylist()
\r
571 if playlist.count > 1:
\r
573 self._c['status bar'].set_playlist(playlist)
\r
577 def _previous_curve(self, *args):
\r
580 Go to the previous curve in the playlist.
\r
581 If we are at the first curve, we jump to the last.
\r
583 Syntax: previous, p
\r
585 #playlist = self.playlists[self.GetActivePlaylistName()][0]
\r
586 #select the previous curve and tell the user if we wrapped around
\r
587 #self.AppendToOutput(playlist.previous())
\r
588 selected_item = self._c['playlists']._c['tree'].GetSelection()
\r
589 if self._c['playlists']._c['tree'].ItemHasChildren(selected_item):
\r
590 previous_item = self._c['playlists']._c['tree'].GetLastChild(selected_item)
\r
592 previous_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item)
\r
593 if not previous_item.IsOk():
\r
594 parent_item = self._c['playlists']._c['tree'].GetItemParent(selected_item)
\r
595 previous_item = self._c['playlists']._c['tree'].GetLastChild(parent_item)
\r
596 self._c['playlists']._c['tree'].SelectItem(previous_item, True)
\r
597 playlist = self.GetActivePlaylist()
\r
598 if playlist.count > 1:
\r
599 playlist.previous()
\r
600 self._c['status bar'].set_playlist(playlist)
\r
604 def _on_notebook_page_close(self, event):
\r
605 ctrl = event.GetEventObject()
\r
606 playlist_name = ctrl.GetPageText(ctrl._curpage)
\r
607 self.DeleteFromPlaylists(playlist_name)
\r
609 def OnPaneClose(self, event):
\r
612 def OnPropGridChanged (self, event):
\r
613 prop = event.GetProperty()
\r
615 item_section = self.panelProperties.SelectedTreeItem
\r
616 item_plugin = self._c['commands']._c['tree'].GetItemParent(item_section)
\r
617 plugin = self._c['commands']._c['tree'].GetItemText(item_plugin)
\r
618 config = self.gui.config[plugin]
\r
619 property_section = self._c['commands']._c['tree'].GetItemText(item_section)
\r
620 property_key = prop.GetName()
\r
621 property_value = prop.GetDisplayedString()
\r
623 config[property_section][property_key]['value'] = property_value
\r
625 def OnResultsCheck(self, index, flag):
\r
626 results = self.GetActivePlot().results
\r
627 if results.has_key(self.results_str):
\r
628 results[self.results_str].results[index].visible = flag
\r
629 results[self.results_str].update()
\r
633 def _on_size(self, event):
\r
636 def OnUpdateNote(self, event):
\r
638 Saves the note to the active file.
\r
640 active_file = self.GetActiveFile()
\r
641 active_file.note = self.panelNote.Editor.GetValue()
\r
643 def _on_view(self, event):
\r
644 menu_id = event.GetId()
\r
645 menu_item = self.MenuBar.FindItemById(menu_id)
\r
646 menu_label = menu_item.GetLabel()
\r
648 pane = self._c['manager'].GetPane(menu_label)
\r
649 pane.Show(not pane.IsShown())
\r
650 #if we don't do the following, the Folders pane does not resize properly on hide/show
\r
651 if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
\r
652 #folders_size = pane.GetSize()
\r
653 self.panelFolders.Fit()
\r
654 self._c['manager'].Update()
\r
656 def _clickize(self, xvector, yvector, index):
\r
658 Returns a ClickedPoint() object from an index and vectors of x, y coordinates
\r
660 point = lib.clickedpoint.ClickedPoint()
\r
661 point.index = index
\r
662 point.absolute_coords = xvector[index], yvector[index]
\r
663 point.find_graph_coords(xvector, yvector)
\r
666 def _delta(self, message='Click 2 points', block=0):
\r
668 Calculates the difference between two clicked points
\r
670 clicked_points = self._measure_N_points(N=2, message=message, block=block)
\r
672 plot = self.GetDisplayedPlotCorrected()
\r
673 curve = plot.curves[block]
\r
675 delta = lib.delta.Delta()
\r
676 delta.point1.x = clicked_points[0].graph_coords[0]
\r
677 delta.point1.y = clicked_points[0].graph_coords[1]
\r
678 delta.point2.x = clicked_points[1].graph_coords[0]
\r
679 delta.point2.y = clicked_points[1].graph_coords[1]
\r
680 delta.units.x = curve.units.x
\r
681 delta.units.y = curve.units.y
\r
685 def _measure_N_points(self, N, message='', block=0):
\r
687 General helper function for N-points measurements
\r
688 By default, measurements are done on the retraction
\r
691 dialog = wx.MessageDialog(None, message, 'Info', wx.OK)
\r
694 figure = self.GetActiveFigure()
\r
696 xvector = self.displayed_plot.curves[block].x
\r
697 yvector = self.displayed_plot.curves[block].y
\r
699 clicked_points = figure.ginput(N, timeout=-1, show_clicks=True)
\r
702 for clicked_point in clicked_points:
\r
703 point = lib.clickedpoint.ClickedPoint()
\r
704 point.absolute_coords = clicked_point[0], clicked_point[1]
\r
706 #TODO: make this optional?
\r
707 #so far, the clicked point is taken, not the corresponding data point
\r
708 point.find_graph_coords(xvector, yvector)
\r
709 point.is_line_edge = True
\r
710 point.is_marker = True
\r
711 points.append(point)
\r
714 def do_copylog(self):
\r
716 Copies all files in the current playlist that have a note to the destination folder.
\r
717 destination: select folder where you want the files to be copied
\r
718 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
720 playlist = self.GetActivePlaylist()
\r
721 if playlist is not None:
\r
722 destination = self.GetStringFromConfig('core', 'copylog', 'destination')
\r
723 if not os.path.isdir(destination):
\r
724 os.makedirs(destination)
\r
725 for current_file in playlist.files:
\r
726 if current_file.note:
\r
727 shutil.copy(current_file.filename, destination)
\r
728 if current_file.driver.filetype == 'mfp1d':
\r
729 filename = current_file.filename.replace('deflection', 'LVDT', 1)
\r
730 path, name = os.path.split(filename)
\r
731 filename = os.path.join(path, 'lvdt', name)
\r
732 use_LVDT_folder = self.GetBoolFromConfig('core', 'copylog', 'use_LVDT_folder')
\r
733 if use_LVDT_folder:
\r
734 destination = os.path.join(destination, 'LVDT')
\r
735 shutil.copy(filename, destination)
\r
737 def do_plotmanipulators(self):
\r
739 Please select the plotmanipulators you would like to use
\r
740 and define the order in which they will be applied to the data.
\r
742 Click 'Execute' to apply your changes.
\r
746 def do_preferences(self):
\r
748 Please set general preferences for Hooke here.
\r
749 hide_curve_extension: hides the extension of the force curve files.
\r
750 not recommended for 'picoforce' files
\r
756 Use this command for testing purposes. You find do_test in hooke.py.
\r
760 def do_version(self):
\r
764 Prints the current version and codename, plus library version. Useful for debugging.
\r
766 self.AppendToOutput('Hooke ' + __version__ + ' (' + __codename__ + ')')
\r
767 self.AppendToOutput('Released on: ' + __releasedate__)
\r
768 self.AppendToOutput('---')
\r
769 self.AppendToOutput('Python version: ' + python_version)
\r
770 self.AppendToOutput('WxPython version: ' + wx_version)
\r
771 self.AppendToOutput('Matplotlib version: ' + mpl_version)
\r
772 self.AppendToOutput('SciPy version: ' + scipy_version)
\r
773 self.AppendToOutput('NumPy version: ' + numpy_version)
\r
774 self.AppendToOutput('ConfigObj version: ' + configobj_version)
\r
775 self.AppendToOutput('wxPropertyGrid version: ' + '.'.join([str(PROPGRID_MAJOR), str(PROPGRID_MINOR), str(PROPGRID_RELEASE)]))
\r
776 self.AppendToOutput('---')
\r
777 self.AppendToOutput('Platform: ' + str(platform.uname()))
\r
778 self.AppendToOutput('******************************')
\r
779 self.AppendToOutput('Loaded plugins')
\r
780 self.AppendToOutput('---')
\r
782 #sort the plugins into alphabetical order
\r
783 plugins_list = [key for key, value in self.plugins.iteritems()]
\r
784 plugins_list.sort()
\r
785 for plugin in plugins_list:
\r
786 self.AppendToOutput(plugin)
\r
788 def UpdateNote(self):
\r
789 #update the note for the active file
\r
790 active_file = self.GetActiveFile()
\r
791 if active_file is not None:
\r
792 self.panelNote.Editor.SetValue(active_file.note)
\r
794 def UpdatePlaylistsTreeSelection(self):
\r
795 playlist = self.GetActivePlaylist()
\r
796 if playlist is not None:
\r
797 if playlist.index >= 0:
\r
798 self._c['status bar'].set_playlist(playlist)
\r
802 def UpdatePlot(self, plot=None):
\r
804 def add_to_plot(curve, set_scale=True):
\r
805 if curve.visible and curve.x and curve.y:
\r
806 #get the index of the subplot to use as destination
\r
807 destination = (curve.destination.column - 1) * number_of_rows + curve.destination.row - 1
\r
808 #set all parameters for the plot
\r
809 axes_list[destination].set_title(curve.title)
\r
811 axes_list[destination].set_xlabel(curve.prefix.x + curve.units.x)
\r
812 axes_list[destination].set_ylabel(curve.prefix.y + curve.units.y)
\r
813 #set the formatting details for the scale
\r
814 formatter_x = lib.curve.PrefixFormatter(curve.decimals.x, curve.prefix.x, use_zero)
\r
815 formatter_y = lib.curve.PrefixFormatter(curve.decimals.y, curve.prefix.y, use_zero)
\r
816 axes_list[destination].xaxis.set_major_formatter(formatter_x)
\r
817 axes_list[destination].yaxis.set_major_formatter(formatter_y)
\r
818 if curve.style == 'plot':
\r
819 axes_list[destination].plot(curve.x, curve.y, color=curve.color, label=curve.label, lw=curve.linewidth, zorder=1)
\r
820 if curve.style == 'scatter':
\r
821 axes_list[destination].scatter(curve.x, curve.y, color=curve.color, label=curve.label, s=curve.size, zorder=2)
\r
822 #add the legend if necessary
\r
824 axes_list[destination].legend()
\r
827 active_file = self.GetActiveFile()
\r
828 if not active_file.driver:
\r
829 #the first time we identify a file, the following need to be set
\r
830 active_file.identify(self.drivers)
\r
831 for curve in active_file.plot.curves:
\r
832 curve.decimals.x = self.GetIntFromConfig('core', 'preferences', 'x_decimals')
\r
833 curve.decimals.y = self.GetIntFromConfig('core', 'preferences', 'y_decimals')
\r
834 curve.legend = self.GetBoolFromConfig('core', 'preferences', 'legend')
\r
835 curve.prefix.x = self.GetStringFromConfig('core', 'preferences', 'x_prefix')
\r
836 curve.prefix.y = self.GetStringFromConfig('core', 'preferences', 'y_prefix')
\r
837 if active_file.driver is None:
\r
838 self.AppendToOutput('Invalid file: ' + active_file.filename)
\r
840 self.displayed_plot = copy.deepcopy(active_file.plot)
\r
841 #add raw curves to plot
\r
842 self.displayed_plot.raw_curves = copy.deepcopy(self.displayed_plot.curves)
\r
843 #apply all active plotmanipulators
\r
844 self.displayed_plot = self.ApplyPlotmanipulators(self.displayed_plot, active_file)
\r
845 #add corrected curves to plot
\r
846 self.displayed_plot.corrected_curves = copy.deepcopy(self.displayed_plot.curves)
\r
849 self.displayed_plot = copy.deepcopy(plot)
\r
851 figure = self.GetActiveFigure()
\r
854 #use '0' instead of e.g. '0.00' for scales
\r
855 use_zero = self.GetBoolFromConfig('core', 'preferences', 'use_zero')
\r
856 #optionally remove the extension from the title of the plot
\r
857 hide_curve_extension = self.GetBoolFromConfig('core', 'preferences', 'hide_curve_extension')
\r
858 if hide_curve_extension:
\r
859 title = lh.remove_extension(self.displayed_plot.title)
\r
861 title = self.displayed_plot.title
\r
862 figure.suptitle(title, fontsize=14)
\r
863 #create the list of all axes necessary (rows and columns)
\r
865 number_of_columns = max([curve.destination.column for curve in self.displayed_plot.curves])
\r
866 number_of_rows = max([curve.destination.row for curve in self.displayed_plot.curves])
\r
867 for index in range(number_of_rows * number_of_columns):
\r
868 axes_list.append(figure.add_subplot(number_of_rows, number_of_columns, index + 1))
\r
870 #add all curves to the corresponding plots
\r
871 for curve in self.displayed_plot.curves:
\r
874 #make sure the titles of 'subplots' do not overlap with the axis labels of the 'main plot'
\r
875 figure.subplots_adjust(hspace=0.3)
\r
878 self.panelResults.ClearResults()
\r
879 if self.displayed_plot.results.has_key(self.results_str):
\r
880 for curve in self.displayed_plot.results[self.results_str].results:
\r
881 add_to_plot(curve, set_scale=False)
\r
882 self.panelResults.DisplayResults(self.displayed_plot.results[self.results_str])
\r
884 self.panelResults.ClearResults()
\r
886 figure.canvas.draw()
\r
888 def _on_curve_select(self, playlist, curve):
\r
889 #create the plot tab and add playlist to the dictionary
\r
890 plotPanel = panel.plot.PlotPanel(self, ID_FirstPlot + len(self.playlists))
\r
891 notebook_tab = self._c['notebook'].AddPage(plotPanel, playlist.name, True)
\r
892 #tab_index = self._c['notebook'].GetSelection()
\r
893 playlist.figure = plotPanel.get_figure()
\r
894 self.playlists[playlist.name] = playlist
\r
895 #self.playlists[playlist.name] = [playlist, figure]
\r
896 self._c['status bar'].set_playlist(playlist)
\r
901 def _on_playlist_left_doubleclick(self):
\r
902 index = self._c['notebook'].GetSelection()
\r
903 current_playlist = self._c['notebook'].GetPageText(index)
\r
904 if current_playlist != playlist_name:
\r
905 index = self._GetPlaylistTab(playlist_name)
\r
906 self._c['notebook'].SetSelection(index)
\r
907 self._c['status bar'].set_playlist(playlist)
\r
911 def _on_playlist_delete(self, playlist):
\r
912 notebook = self.Parent.plotNotebook
\r
913 index = self.Parent._GetPlaylistTab(playlist.name)
\r
914 notebook.SetSelection(index)
\r
915 notebook.DeletePage(notebook.GetSelection())
\r
916 self.Parent.DeleteFromPlaylists(playlist_name)
\r
919 class HookeApp (wx.App):
\r
920 """A :class:`wx.App` wrapper around :class:`HookeFrame`.
\r
922 Tosses up a splash screen and then loads :class:`HookeFrame` in
\r
925 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
\r
927 self.commands = commands
\r
928 self.inqueue = inqueue
\r
929 self.outqueue = outqueue
\r
930 super(HookeApp, self).__init__(*args, **kwargs)
\r
933 self.SetAppName('Hooke')
\r
934 self.SetVendorName('')
\r
935 self._setup_splash_screen()
\r
937 height = int(self.gui.config['main height']) # HACK: config should convert
\r
938 width = int(self.gui.config['main width'])
\r
939 top = int(self.gui.config['main top'])
\r
940 left = int(self.gui.config['main left'])
\r
942 # Sometimes, the ini file gets confused and sets 'left' and
\r
943 # 'top' to large negative numbers. Here we catch and fix
\r
944 # this. Keep small negative numbers, the user might want
\r
952 'frame': HookeFrame(
\r
953 self.gui, self.commands, self.inqueue, self.outqueue,
\r
954 parent=None, title='Hooke',
\r
955 pos=(left, top), size=(width, height),
\r
956 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
\r
958 self._c['frame'].Show(True)
\r
959 self.SetTopWindow(self._c['frame'])
\r
962 def _setup_splash_screen(self):
\r
963 if self.gui.config['show splash screen']:
\r
964 path = self.gui.config['splash screen image']
\r
965 if os.path.isfile(path):
\r
966 duration = int(self.gui.config['splash screen duration']) # HACK: config should decode types
\r
968 bitmap=wx.Image(path).ConvertToBitmap(),
\r
969 splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
\r
970 milliseconds=duration,
\r
973 # For some reason splashDuration and sleep do not
\r
974 # correspond to each other at least not on Windows.
\r
975 # Maybe it's because duration is in milliseconds and
\r
976 # sleep in seconds. Thus we need to increase the
\r
977 # sleep time a bit. A factor of 1.2 seems to work.
\r
979 time.sleep(sleepFactor * duration / 1000)
\r
982 class GUI (UserInterface):
\r
983 """wxWindows graphical user interface.
\r
985 def __init__(self):
\r
986 super(GUI, self).__init__(name='gui')
\r
988 def default_settings(self):
\r
989 """Return a list of :class:`hooke.config.Setting`\s for any
\r
990 configurable UI settings.
\r
992 The suggested section setting is::
\r
994 Setting(section=self.setting_section, help=self.__doc__)
\r
997 Setting(section=self.setting_section, help=self.__doc__),
\r
998 Setting(section=self.setting_section, option='icon image',
\r
999 value=os.path.join('doc', 'img', 'microscope.ico'),
\r
1000 help='Path to the hooke icon image.'),
\r
1001 Setting(section=self.setting_section, option='show splash screen',
\r
1003 help='Enable/disable the splash screen'),
\r
1004 Setting(section=self.setting_section, option='splash screen image',
\r
1005 value=os.path.join('doc', 'img', 'hooke.jpg'),
\r
1006 help='Path to the Hooke splash screen image.'),
\r
1007 Setting(section=self.setting_section, option='splash screen duration',
\r
1009 help='Duration of the splash screen in milliseconds.'),
\r
1010 Setting(section=self.setting_section, option='perspective path',
\r
1011 value=os.path.join('resources', 'gui', 'perspective'),
\r
1012 help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.
\r
1013 Setting(section=self.setting_section, option='hide extensions',
\r
1015 help='Hide file extensions when displaying names.'),
\r
1016 Setting(section=self.setting_section, option='folders-workdir',
\r
1018 help='This should probably go...'),
\r
1019 Setting(section=self.setting_section, option='folders-filters',
\r
1021 help='This should probably go...'),
\r
1022 Setting(section=self.setting_section, option='active perspective',
\r
1024 help='Name of active perspective file (or "Default").'),
\r
1025 Setting(section=self.setting_section, option='folders-filter-index',
\r
1027 help='This should probably go...'),
\r
1028 Setting(section=self.setting_section, option='main height',
\r
1030 help='Height of main window in pixels.'),
\r
1031 Setting(section=self.setting_section, option='main width',
\r
1033 help='Width of main window in pixels.'),
\r
1034 Setting(section=self.setting_section, option='main top',
\r
1036 help='Pixels from screen top to top of main window.'),
\r
1037 Setting(section=self.setting_section, option='main left',
\r
1039 help='Pixels from screen left to left of main window.'),
\r
1040 Setting(section=self.setting_section, option='selected command',
\r
1041 value='load playlist',
\r
1042 help='Name of the initially selected command.'),
\r
1045 def _app(self, commands, ui_to_command_queue, command_to_ui_queue):
\r
1049 app = HookeApp(gui=self,
\r
1050 commands=commands,
\r
1051 inqueue=ui_to_command_queue,
\r
1052 outqueue=command_to_ui_queue,
\r
1053 redirect=redirect)
\r
1056 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
\r
1057 app = self._app(commands, ui_to_command_queue, command_to_ui_queue)
\r