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 panel as panel
\r
35 from . import prettyformat as prettyformat
\r
38 class Notebook (aui.AuiNotebook):
\r
39 def __init__(self, *args, **kwargs):
\r
40 super(Notebook, self).__init__(*args, **kwargs)
\r
41 self.SetArtProvider(aui.AuiDefaultTabArt())
\r
42 #uncomment if we find a nice icon
\r
43 #page_bmp = wx.ArtProvider.GetBitmap(wx.ART_NORMAL_FILE, wx.ART_OTHER, wx.Size(16, 16))
\r
44 self.AddPage(self._welcome_window(), 'Welcome')
\r
46 def _welcome_window(self):
\r
47 #TODO: move into panel.welcome
\r
48 ctrl = wx.html.HtmlWindow(parent=self, size=wx.Size(400, 300))
\r
50 '<h1>Welcome to Hooke</h1>',
\r
51 '<h3>Features</h3>',
\r
53 '<li>View, annotate, measure force files</li>',
\r
54 '<li>Worm-like chain fit of force peaks</li>',
\r
55 '<li>Automatic convolution-based filtering of empty files</li>',
\r
56 '<li>Automatic fit and measurement of multiple force peaks</li>',
\r
57 '<li>Handles force-clamp force experiments (experimental)</li>',
\r
58 '<li>It is extensible through plugins and drivers</li>',
\r
60 '<p>See the <a href="%s">DocumentationIndex</a>'
\r
61 % 'http://code.google.com/p/hooke/wiki/DocumentationIndex',
\r
62 'for more information</p>',
\r
64 ctrl.SetPage('\n'.join(lines))
\r
68 class NavBar (wx.ToolBar):
\r
69 def __init__(self, *args, **kwargs):
\r
70 super(NavBar, self).__init__(*args, **kwargs)
\r
71 self.SetToolBitmapSize(wx.Size(16,16))
\r
73 'previous': self.AddLabelTool(
\r
74 id=wx.ID_PREVIEW_PREVIOUS,
\r
76 bitmap=wx.ArtProvider_GetBitmap(
\r
77 wx.ART_GO_BACK, wx.ART_OTHER, wx.Size(16, 16)),
\r
78 shortHelp='Previous curve'),
\r
79 'next': self.AddLabelTool(
\r
80 id=wx.ID_PREVIEW_NEXT,
\r
82 bitmap=wx.ArtProvider_GetBitmap(
\r
83 wx.ART_GO_FORWARD, wx.ART_OTHER, wx.Size(16, 16)),
\r
84 shortHelp='Next curve'),
\r
89 class FileMenu (wx.Menu):
\r
90 def __init__(self, *args, **kwargs):
\r
91 super(FileMenu, self).__init__(*args, **kwargs)
\r
92 self._c = {'exit': self.Append(wx.ID_EXIT)}
\r
95 class ViewMenu (wx.Menu):
\r
96 def __init__(self, *args, **kwargs):
\r
97 super(ViewMenu, self).__init__(*args, **kwargs)
\r
99 'folders': self.AppendCheckItem(id=wx.ID_ANY, text='Folders\tF5'),
\r
100 'playlist': self.AppendCheckItem(
\r
101 id=wx.ID_ANY, text='Playlists\tF6'),
\r
102 'commands': self.AppendCheckItem(
\r
103 id=wx.ID_ANY, text='Commands\tF7'),
\r
104 'assistant': self.AppendCheckItem(
\r
105 id=wx.ID_ANY, text='Assistant\tF9'),
\r
106 'properties': self.AppendCheckItem(
\r
107 id=wx.ID_ANY, text='Properties\tF8'),
\r
108 'results': self.AppendCheckItem(id=wx.ID_ANY, text='Results\tF10'),
\r
109 'output': self.AppendCheckItem(id=wx.ID_ANY, text='Output\tF11'),
\r
110 'note': self.AppendCheckItem(id=wx.ID_ANY, text='Note\tF12'),
\r
112 for item in self._c.values():
\r
116 class PerspectiveMenu (wx.Menu):
\r
117 def __init__(self, *args, **kwargs):
\r
118 super(PerspectiveMenu, self).__init__(*args, **kwargs)
\r
121 def update(self, perspectives, selected, callback):
\r
122 """Rebuild the perspectives menu.
\r
124 for item in self.GetMenuItems():
\r
126 self.DeleteItem(item)
\r
128 'save': self.Append(id=wx.ID_ANY, text='Save Perspective'),
\r
129 'delete': self.Append(id=wx.ID_ANY, text='Delete Perspective'),
\r
131 self.AppendSeparator()
\r
132 for label in perspectives:
\r
133 self._c[label] = self.AppendRadioItem(id=wx.ID_ANY, text=label)
\r
134 self.Bind(wx.EVT_MENU, callback, self._c[label])
\r
135 if label == selected:
\r
136 self._c[label].Check(True)
\r
139 class HelpMenu (wx.Menu):
\r
140 def __init__(self, *args, **kwargs):
\r
141 super(HelpMenu, self).__init__(*args, **kwargs)
\r
142 self._c = {'about':self.Append(id=wx.ID_ABOUT)}
\r
145 class MenuBar (wx.MenuBar):
\r
146 def __init__(self, *args, **kwargs):
\r
147 super(MenuBar, self).__init__(*args, **kwargs)
\r
149 'file': FileMenu(),
\r
150 'view': ViewMenu(),
\r
151 'perspective': PerspectiveMenu(),
\r
152 'help': HelpMenu(),
\r
154 self.Append(self._c['file'], 'File')
\r
155 self.Append(self._c['view'], 'View')
\r
156 self.Append(self._c['perspective'], 'Perspective')
\r
157 self.Append(self._c['help'], 'Help')
\r
160 class StatusBar (wx.StatusBar):
\r
161 def __init__(self, *args, **kwargs):
\r
162 super(StatusBar, self).__init__(*args, **kwargs)
\r
163 self.SetStatusWidths([-2, -3])
\r
164 self.SetStatusText('Ready', 0)
\r
165 self.SetStatusText(u'Welcome to Hooke (version %s)' % version(), 1)
\r
168 class HookeFrame (wx.Frame):
\r
169 def __init__(self, gui, commands, *args, **kwargs):
\r
170 super(HookeFrame, self).__init__(*args, **kwargs)
\r
172 self.commands = commands
\r
173 self._perspectives = {} # {name: perspective_str}
\r
176 self.SetIcon(wx.Icon(self.gui.config['icon image'], wx.BITMAP_TYPE_ICO))
\r
178 # setup frame manager
\r
179 self._c['manager'] = aui.AuiManager()
\r
180 self._c['manager'].SetManagedWindow(self)
\r
182 # set the gradient and drag styles
\r
183 self._c['manager'].GetArtProvider().SetMetric(
\r
184 aui.AUI_DOCKART_GRADIENT_TYPE, aui.AUI_GRADIENT_NONE)
\r
185 self._c['manager'].SetFlags(
\r
186 self._c['manager'].GetFlags() ^ aui.AUI_MGR_TRANSPARENT_DRAG)
\r
188 # Min size for the frame itself isn't completely done. See
\r
189 # the end of FrameManager::Update() for the test code. For
\r
190 # now, just hard code a frame minimum size.
\r
191 self.SetMinSize(wx.Size(500, 500))
\r
193 self._setup_panels()
\r
194 self._setup_toolbars()
\r
195 self._c['manager'].Update() # commit pending changes
\r
197 # Create the menubar after the panes so that the default
\r
198 # perspective is created with all panes open
\r
199 self._c['menu bar'] = MenuBar(
\r
201 self.SetMenuBar(self._c['menu bar'])
\r
203 self._c['status bar'] = StatusBar(self, style=wx.ST_SIZEGRIP)
\r
205 self._update_perspectives()
\r
206 self._bind_events()
\r
208 name = self.gui.config['active perspective']
\r
209 return # TODO: cleanup
\r
210 menu_item = self.GetPerspectiveMenuItem(name)
\r
211 if menu_item is not None:
\r
212 self._on_restore_perspective(menu_item)
\r
213 #TODO: config setting to remember playlists from last session
\r
214 self.playlists = self._c['playlists'].Playlists
\r
215 self._displayed_plot = None
\r
216 #load default list, if possible
\r
217 self.do_loadlist(self.GetStringFromConfig('core', 'preferences', 'playlist'))
\r
219 def _setup_panels(self):
\r
220 client_size = self.GetClientSize()
\r
221 for label,p,style in [
\r
222 ('folders', wx.GenericDirCtrl(
\r
224 dir=self.gui.config['folders-workdir'],
\r
226 style=wx.DIRCTRL_SHOW_FILTERS,
\r
227 filter=self.gui.config['folders-filters'],
\r
228 defaultFilter=int(self.gui.config['folders-filter-index'])), 'left'), #HACK: config should convert
\r
229 ('playlists', panel.playlist.Playlist(
\r
230 config=self.gui.config,
\r
233 style=wx.WANTS_CHARS|wx.NO_BORDER,
\r
234 # WANTS_CHARS so the panel doesn't eat the Return key.
\r
235 size=(160, 200)), 'left'),
\r
236 ('note', panel.note.Note(self), 'left'),
\r
237 ('notebook', Notebook(
\r
239 pos=wx.Point(client_size.x, client_size.y),
\r
240 size=wx.Size(430, 200),
\r
241 style=aui.AUI_NB_DEFAULT_STYLE
\r
242 | aui.AUI_NB_TAB_EXTERNAL_MOVE | wx.NO_BORDER), 'center'),
\r
243 ('commands', panel.commands.Commands(
\r
244 commands=self.commands,
\r
245 selected=self.gui.config['selected command'],
\r
247 'execute': self.execute_command,
\r
248 'select_plugin': self.select_plugin,
\r
249 'select_command': self.select_command,
\r
250 # 'selection_changed': self.panelProperties.select(self, method, command), #SelectedTreeItem = selected_item,
\r
253 style=wx.WANTS_CHARS|wx.NO_BORDER,
\r
254 # WANTS_CHARS so the panel doesn't eat the Return key.
\r
255 size=(160, 200)), 'right'),
\r
256 #('properties', panel.propertyeditor.PropertyEditor(self),'right'),
\r
257 ('assistant', wx.TextCtrl(
\r
259 pos=wx.Point(0, 0),
\r
260 size=wx.Size(150, 90),
\r
261 style=wx.NO_BORDER|wx.TE_MULTILINE), 'right'),
\r
262 ('output', wx.TextCtrl(
\r
264 pos=wx.Point(0, 0),
\r
265 size=wx.Size(150, 90),
\r
266 style=wx.NO_BORDER|wx.TE_MULTILINE), 'bottom'),
\r
267 ('results', panel.results.Results(self), 'bottom'),
\r
269 self._add_panel(label, p, style)
\r
270 self._c['assistant'].SetEditable(False)
\r
272 def _add_panel(self, label, panel, style):
\r
273 self._c[label] = panel
\r
274 cap_label = label.capitalize()
\r
275 info = aui.AuiPaneInfo().Name(cap_label).Caption(cap_label)
\r
276 if style == 'left':
\r
277 info.Left().CloseButton(True).MaximizeButton(False)
\r
278 elif style == 'center':
\r
279 info.CenterPane().PaneBorder(False)
\r
280 elif style == 'right':
\r
281 info.Right().CloseButton(True).MaximizeButton(False)
\r
283 assert style == 'bottom', style
\r
284 info.Bottom().CloseButton(True).MaximizeButton(False)
\r
285 self._c['manager'].AddPane(panel, info)
\r
287 def _setup_toolbars(self):
\r
288 self._c['navbar'] = NavBar(self, style=wx.TB_FLAT | wx.TB_NODIVIDER)
\r
290 self._c['manager'].AddPane(
\r
292 aui.AuiPaneInfo().Name('Navigation').Caption('Navigation'
\r
293 ).ToolbarPane().Top().Layer(1).Row(1).LeftDockable(False
\r
294 ).RightDockable(False))
\r
296 def _bind_events(self):
\r
297 # TODO: figure out if we can use the eventManager for menu
\r
298 # ranges and events of 'self' without raising an assertion
\r
300 self.Bind(wx.EVT_ERASE_BACKGROUND, self._on_erase_background)
\r
301 self.Bind(wx.EVT_SIZE, self._on_size)
\r
302 self.Bind(wx.EVT_CLOSE, self._on_close)
\r
303 self.Bind(wx.EVT_MENU, self._on_close, id=wx.ID_EXIT)
\r
304 self.Bind(wx.EVT_MENU, self._on_about, id=wx.ID_ABOUT)
\r
305 self.Bind(aui.EVT_AUI_PANE_CLOSE, self.OnPaneClose)
\r
306 self.Bind(aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self._on_notebook_page_close)
\r
308 for value in self._c['menu bar']._c['view']._c.values():
\r
309 self.Bind(wx.EVT_MENU_RANGE, self._on_view, value)
\r
311 self.Bind(wx.EVT_MENU, self._on_save_perspective,
\r
312 self._c['menu bar']._c['perspective']._c['save'])
\r
313 self.Bind(wx.EVT_MENU, self._on_delete_perspective,
\r
314 self._c['menu bar']._c['perspective']._c['delete'])
\r
316 self.Bind(wx.EVT_TOOL, self._on_next, self._c['navbar']._c['next'])
\r
317 self.Bind(wx.EVT_TOOL, self._on_previous,self._c['navbar']._c['previous'])
\r
319 treeCtrl = self._c['folders'].GetTreeCtrl()
\r
320 treeCtrl.Bind(wx.EVT_LEFT_DCLICK, self._on_dir_ctrl_left_double_click)
\r
322 # TODO: playlist callbacks
\r
323 return # TODO: cleanup
\r
324 evtmgr.eventManager.Register(self.OnUpdateNote, wx.EVT_BUTTON, self.panelNote.UpdateButton)
\r
326 self.panelProperties.pg.Bind(wxpg.EVT_PG_CHANGED, self.OnPropGridChanged)
\r
328 self.panelResults.results_list.OnCheckItem = self.OnResultsCheck
\r
330 def _GetActiveFileIndex(self):
\r
331 lib.playlist.Playlist = self.GetActivePlaylist()
\r
332 #get the selected item from the tree
\r
333 selected_item = self._c['playlists']._c['tree'].GetSelection()
\r
334 #test if a playlist or a curve was double-clicked
\r
335 if self._c['playlists']._c['tree'].ItemHasChildren(selected_item):
\r
339 selected_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item)
\r
340 while selected_item.IsOk():
\r
342 selected_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item)
\r
345 def _GetPlaylistTab(self, name):
\r
346 for index, page in enumerate(self._c['notebook']._tabs._pages):
\r
347 if page.caption == name:
\r
351 def _restore_perspective(self, name):
\r
353 self.gui.config['active perspective'] = name # TODO: push to engine's Hooke
\r
354 self._c['manager'].LoadPerspective(self._perspectives[name])
\r
355 self._c['manager'].Update()
\r
356 for pane in self._c['manager'].GetAllPanes():
\r
357 if pane.name in self._c['menu bar']._c['view']._c.keys():
\r
358 pane.Check(pane.window.IsShown())
\r
360 def _SavePerspectiveToFile(self, name, perspective):
\r
361 filename = ''.join([name, '.txt'])
\r
362 filename = lh.get_file_path(filename, ['perspective'])
\r
363 perspectivesFile = open(filename, 'w')
\r
364 perspectivesFile.write(perspective)
\r
365 perspectivesFile.close()
\r
367 def execute_command(self, _class, method, command, args):
\r
368 self.cmd.inqueue.put(CommandMessage(command, args))
\r
370 msg = self.cmd.outqueue.get()
\r
371 if isinstance(msg, Exit):
\r
373 elif isinstance(msg, CommandExit):
\r
374 self.cmd.stdout.write(msg.__class__.__name__+'\n')
\r
375 self.cmd.stdout.write(str(msg).rstrip()+'\n')
\r
377 elif isinstance(msg, ReloadUserInterfaceConfig):
\r
378 self.cmd.ui.reload_config(msg.config)
\r
380 elif isinstance(msg, Request):
\r
381 self._handle_request(msg)
\r
383 self.cmd.stdout.write(str(msg).rstrip()+'\n')
\r
384 #TODO: run the command
\r
385 #command = ''.join(['self.do_', item_text, '()'])
\r
386 #self.AppendToOutput(command + '\n')
\r
389 def select_plugin(self, _class, method, plugin):
\r
390 for option in config[section]:
\r
391 properties.append([option, config[section][option]])
\r
393 def select_command(self, _class, method, command):
\r
394 self.select_plugin(command.plugin)
\r
395 plugin = self.GetItemText(selected_item)
\r
396 if plugin != 'core':
\r
397 doc_string = eval('plugins.' + plugin + '.' + plugin + 'Commands.__doc__')
\r
399 doc_string = 'The module "core" contains Hooke core functionality'
\r
400 if doc_string is not None:
\r
401 self.panelAssistant.ChangeValue(doc_string)
\r
403 self.panelAssistant.ChangeValue('')
\r
404 panel.propertyeditor.PropertyEditor.Initialize(self.panelProperties, properties)
\r
405 self.gui.config['selected command'] = command
\r
407 def AddPlaylistFromFiles(self, files=[], name='Untitled'):
\r
409 playlist = lib.playlist.Playlist(self, self.drivers)
\r
411 playlist.add_curve(item)
\r
412 if playlist.count > 0:
\r
413 playlist.name = self._GetUniquePlaylistName(name)
\r
415 self.AddTayliss(playlist)
\r
417 def AppendToOutput(self, text):
\r
418 self.panelOutput.AppendText(''.join([text, '\n']))
\r
420 def AppliesPlotmanipulator(self, name):
\r
422 Returns True if the plotmanipulator 'name' is applied, False otherwise
\r
423 name does not contain 'plotmanip_', just the name of the plotmanipulator (e.g. 'flatten')
\r
425 return self.GetBoolFromConfig('core', 'plotmanipulators', name)
\r
427 def ApplyPlotmanipulators(self, plot, plot_file):
\r
429 Apply all active plotmanipulators.
\r
431 if plot is not None and plot_file is not None:
\r
432 manipulated_plot = copy.deepcopy(plot)
\r
433 for plotmanipulator in self.plotmanipulators:
\r
434 if self.GetBoolFromConfig('core', 'plotmanipulators', plotmanipulator.name):
\r
435 manipulated_plot = plotmanipulator.method(manipulated_plot, plot_file)
\r
436 return manipulated_plot
\r
438 def GetActiveFigure(self):
\r
439 playlist_name = self.GetActivePlaylistName()
\r
440 figure = self.playlists[playlist_name].figure
\r
441 if figure is not None:
\r
445 def GetActiveFile(self):
\r
446 playlist = self.GetActivePlaylist()
\r
447 if playlist is not None:
\r
448 return playlist.get_active_file()
\r
451 def GetActivePlot(self):
\r
452 playlist = self.GetActivePlaylist()
\r
453 if playlist is not None:
\r
454 return playlist.get_active_file().plot
\r
457 def GetDisplayedPlot(self):
\r
458 plot = copy.deepcopy(self.displayed_plot)
\r
460 #plot.curves = copy.deepcopy(plot.curves)
\r
463 def GetDisplayedPlotCorrected(self):
\r
464 plot = copy.deepcopy(self.displayed_plot)
\r
466 plot.curves = copy.deepcopy(plot.corrected_curves)
\r
469 def GetDisplayedPlotRaw(self):
\r
470 plot = copy.deepcopy(self.displayed_plot)
\r
472 plot.curves = copy.deepcopy(plot.raw_curves)
\r
475 def GetDockArt(self):
\r
476 return self._c['manager'].GetArtProvider()
\r
478 def GetPlotmanipulator(self, name):
\r
480 Returns a plot manipulator function from its name
\r
482 for plotmanipulator in self.plotmanipulators:
\r
483 if plotmanipulator.name == name:
\r
484 return plotmanipulator
\r
487 def GetPerspectiveMenuItem(self, name):
\r
488 if self._perspectives.has_key(name):
\r
489 perspectives_list = [key for key, value in self._perspectives.iteritems()]
\r
490 perspectives_list.sort()
\r
491 index = perspectives_list.index(name)
\r
492 perspective_Id = ID_FirstPerspective + index
\r
493 menu_item = self.MenuBar.FindItemById(perspective_Id)
\r
498 def HasPlotmanipulator(self, name):
\r
500 returns True if the plotmanipulator 'name' is loaded, False otherwise
\r
502 for plotmanipulator in self.plotmanipulators:
\r
503 if plotmanipulator.command == name:
\r
507 def _on_about(self, event):
\r
508 message = 'Hooke\n\n'+\
\r
509 'A free, open source data analysis platform\n\n'+\
\r
510 'Copyright 2006-2008 by Massimo Sandal\n'+\
\r
511 'Copyright 2010 by Dr. Rolf Schmidt\n\n'+\
\r
512 'Hooke is released under the GNU General Public License version 2.'
\r
513 dialog = wx.MessageDialog(self, message, 'About Hooke', wx.OK | wx.ICON_INFORMATION)
\r
517 def _on_close(self, event):
\r
519 self.gui.config['main height'] = str(self.GetSize().GetHeight())
\r
520 self.gui.config['main left'] = str(self.GetPosition()[0])
\r
521 self.gui.config['main top'] = str(self.GetPosition()[1])
\r
522 self.gui.config['main width'] = str(self.GetSize().GetWidth())
\r
523 # push changes back to Hooke.config?
\r
524 self._c['manager'].UnInit()
\r
525 del self._c['manager']
\r
528 def _update_perspectives(self):
\r
529 """Add perspectives to menubar and _perspectives.
\r
531 self._perspectives = {
\r
532 'Default': self._c['manager'].SavePerspective(),
\r
534 path = self.gui.config['perspective path']
\r
535 if os.path.isdir(path):
\r
536 files = sorted(os.listdir(path))
\r
537 for fname in files:
\r
538 name, extension = os.path.splitext(fname)
\r
539 if extension != '.txt':
\r
541 fpath = os.path.join(path, fpath)
\r
542 if not os.path.isfile(fpath):
\r
545 with open(fpath, 'rU') as f:
\r
546 perspective = f.readline()
\r
548 self._perspectives[name] = perspective
\r
550 selected_perspective = self.gui.config['active perspective']
\r
551 if not self._perspectives.has_key(selected_perspective):
\r
552 self.gui.config['active perspective'] = 'Default' # TODO: push to engine's Hooke
\r
554 self._update_perspective_menu()
\r
555 self._restore_perspective(selected_perspective)
\r
557 def _update_perspective_menu(self):
\r
558 self._c['menu bar']._c['perspective'].update(
\r
559 sorted(self._perspectives.keys()),
\r
560 self.gui.config['active perspective'],
\r
561 self._on_restore_perspective)
\r
563 def _on_restore_perspective(self, event):
\r
564 name = self.MenuBar.FindItemById(event.GetId()).GetLabel()
\r
565 self._restore_perspective(name)
\r
567 def _on_save_perspective(self, event):
\r
568 def nameExists(name):
\r
569 menu_position = self.MenuBar.FindMenu('Perspective')
\r
570 menu = self.MenuBar.GetMenu(menu_position)
\r
571 for item in menu.GetMenuItems():
\r
572 if item.GetText() == name:
\r
578 dialog = wx.TextEntryDialog(self, 'Enter a name for the new perspective:', 'Save perspective')
\r
579 dialog.SetValue('New perspective')
\r
580 if dialog.ShowModal() != wx.ID_OK:
\r
583 name = dialog.GetValue()
\r
585 if nameExists(name):
\r
586 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
587 if dialogConfirm.ShowModal() == wx.ID_YES:
\r
592 perspective = self._c['manager'].SavePerspective()
\r
593 self._SavePerspectiveToFile(name, perspective)
\r
594 self.gui.config['active perspectives'] = name
\r
595 self._update_perspective_menu()
\r
596 # if nameExists(name):
\r
597 # #check the corresponding menu item
\r
598 # menu_item = self.GetPerspectiveMenuItem(name)
\r
599 # #replace the perspectiveStr in _pespectives
\r
600 # self._perspectives[name] = perspective
\r
602 # #because we deal with radio items, we need to do some extra work
\r
603 # #delete all menu items from the perspectives menu
\r
604 # for item in self._perspectives_menu.GetMenuItems():
\r
605 # self._perspectives_menu.DeleteItem(item)
\r
606 # #recreate the perspectives menu
\r
607 # self._perspectives_menu.Append(ID_SavePerspective, 'Save Perspective')
\r
608 # self._perspectives_menu.Append(ID_DeletePerspective, 'Delete Perspective')
\r
609 # self._perspectives_menu.AppendSeparator()
\r
610 # #convert the perspectives dictionary into a list
\r
611 # # the list contains:
\r
612 # #[0]: name of the perspective
\r
613 # #[1]: perspective
\r
614 # perspectives_list = [key for key, value in self._perspectives.iteritems()]
\r
615 # perspectives_list.append(name)
\r
616 # perspectives_list.sort()
\r
617 # #add all previous perspectives
\r
618 # for index, item in enumerate(perspectives_list):
\r
619 # menu_item = self._perspectives_menu.AppendRadioItem(ID_FirstPerspective + index, item)
\r
621 # menu_item.Check()
\r
622 # #add the new perspective to _perspectives
\r
623 # self._perspectives[name] = perspective
\r
625 def _on_delete_perspective(self, event):
\r
626 dialog = panel.selection.Selection(
\r
627 options=sorted(os.listdir(self.gui.config['perspective path'])),
\r
628 message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
\r
629 button_id=wx.ID_DELETE,
\r
630 button_callback=self._on_delete_perspective,
\r
632 label='Delete perspective(s)',
\r
633 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
\r
634 dialog.CenterOnScreen()
\r
637 self._update_perspective_menu()
\r
638 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
\r
639 # http://trac.wxwidgets.org/ticket/3258
\r
640 # ) that makes the radio item indicator in the menu disappear.
\r
641 # The code should be fine once this issue is fixed.
\r
643 def _on_delete_perspective(self, event, items, selected_items):
\r
644 for item in selected_items:
\r
645 self._perspectives.remove(item)
\r
646 if item == self.gui.config['active perspective']:
\r
647 self.gui.config['active perspective'] = 'Default'
\r
648 path = os.path.join(self.gui.config['perspective path'],
\r
651 self._update_perspective_menu()
\r
653 def _on_dir_ctrl_left_double_click(self, event):
\r
654 file_path = self.panelFolders.GetPath()
\r
655 if os.path.isfile(file_path):
\r
656 if file_path.endswith('.hkp'):
\r
657 self.do_loadlist(file_path)
\r
660 def _on_erase_background(self, event):
\r
663 def OnExit(self, event):
\r
666 def _on_next(self, event):
\r
669 Go to the next curve in the playlist.
\r
670 If we are at the last curve, we come back to the first.
\r
674 selected_item = self._c['playlists']._c['tree'].GetSelection()
\r
675 if self._c['playlists']._c['tree'].ItemHasChildren(selected_item):
\r
676 #GetFirstChild returns a tuple
\r
677 #we only need the first element
\r
678 next_item = self._c['playlists']._c['tree'].GetFirstChild(selected_item)[0]
\r
680 next_item = self._c['playlists']._c['tree'].GetNextSibling(selected_item)
\r
681 if not next_item.IsOk():
\r
682 parent_item = self._c['playlists']._c['tree'].GetItemParent(selected_item)
\r
683 #GetFirstChild returns a tuple
\r
684 #we only need the first element
\r
685 next_item = self._c['playlists']._c['tree'].GetFirstChild(parent_item)[0]
\r
686 self._c['playlists']._c['tree'].SelectItem(next_item, True)
\r
687 if not self._c['playlists']._c['tree'].ItemHasChildren(selected_item):
\r
688 playlist = self.GetActivePlaylist()
\r
689 if playlist.count > 1:
\r
691 self.statusbar.SetStatusText(playlist.get_status_string(), 0)
\r
695 def _on_notebook_page_close(self, event):
\r
696 ctrl = event.GetEventObject()
\r
697 playlist_name = ctrl.GetPageText(ctrl._curpage)
\r
698 self.DeleteFromPlaylists(playlist_name)
\r
700 def OnPaneClose(self, event):
\r
703 def _on_previous(self, event):
\r
706 Go to the previous curve in the playlist.
\r
707 If we are at the first curve, we jump to the last.
\r
709 Syntax: previous, p
\r
711 #playlist = self.playlists[self.GetActivePlaylistName()][0]
\r
712 #select the previous curve and tell the user if we wrapped around
\r
713 #self.AppendToOutput(playlist.previous())
\r
714 selected_item = self._c['playlists']._c['tree'].GetSelection()
\r
715 if self._c['playlists']._c['tree'].ItemHasChildren(selected_item):
\r
716 previous_item = self._c['playlists']._c['tree'].GetLastChild(selected_item)
\r
718 previous_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item)
\r
719 if not previous_item.IsOk():
\r
720 parent_item = self._c['playlists']._c['tree'].GetItemParent(selected_item)
\r
721 previous_item = self._c['playlists']._c['tree'].GetLastChild(parent_item)
\r
722 self._c['playlists']._c['tree'].SelectItem(previous_item, True)
\r
723 playlist = self.GetActivePlaylist()
\r
724 if playlist.count > 1:
\r
725 playlist.previous()
\r
726 self.statusbar.SetStatusText(playlist.get_status_string(), 0)
\r
730 def OnPropGridChanged (self, event):
\r
731 prop = event.GetProperty()
\r
733 item_section = self.panelProperties.SelectedTreeItem
\r
734 item_plugin = self._c['commands']._c['tree'].GetItemParent(item_section)
\r
735 plugin = self._c['commands']._c['tree'].GetItemText(item_plugin)
\r
736 config = self.gui.config[plugin]
\r
737 property_section = self._c['commands']._c['tree'].GetItemText(item_section)
\r
738 property_key = prop.GetName()
\r
739 property_value = prop.GetDisplayedString()
\r
741 config[property_section][property_key]['value'] = property_value
\r
743 def OnResultsCheck(self, index, flag):
\r
744 results = self.GetActivePlot().results
\r
745 if results.has_key(self.results_str):
\r
746 results[self.results_str].results[index].visible = flag
\r
747 results[self.results_str].update()
\r
751 def _on_size(self, event):
\r
754 def OnUpdateNote(self, event):
\r
756 Saves the note to the active file.
\r
758 active_file = self.GetActiveFile()
\r
759 active_file.note = self.panelNote.Editor.GetValue()
\r
761 def _on_view(self, event):
\r
762 menu_id = event.GetId()
\r
763 menu_item = self.MenuBar.FindItemById(menu_id)
\r
764 menu_label = menu_item.GetLabel()
\r
766 pane = self._c['manager'].GetPane(menu_label)
\r
767 pane.Show(not pane.IsShown())
\r
768 #if we don't do the following, the Folders pane does not resize properly on hide/show
\r
769 if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
\r
770 #folders_size = pane.GetSize()
\r
771 self.panelFolders.Fit()
\r
772 self._c['manager'].Update()
\r
774 def _clickize(self, xvector, yvector, index):
\r
776 Returns a ClickedPoint() object from an index and vectors of x, y coordinates
\r
778 point = lib.clickedpoint.ClickedPoint()
\r
779 point.index = index
\r
780 point.absolute_coords = xvector[index], yvector[index]
\r
781 point.find_graph_coords(xvector, yvector)
\r
784 def _delta(self, message='Click 2 points', block=0):
\r
786 Calculates the difference between two clicked points
\r
788 clicked_points = self._measure_N_points(N=2, message=message, block=block)
\r
790 plot = self.GetDisplayedPlotCorrected()
\r
791 curve = plot.curves[block]
\r
793 delta = lib.delta.Delta()
\r
794 delta.point1.x = clicked_points[0].graph_coords[0]
\r
795 delta.point1.y = clicked_points[0].graph_coords[1]
\r
796 delta.point2.x = clicked_points[1].graph_coords[0]
\r
797 delta.point2.y = clicked_points[1].graph_coords[1]
\r
798 delta.units.x = curve.units.x
\r
799 delta.units.y = curve.units.y
\r
803 def _measure_N_points(self, N, message='', block=0):
\r
805 General helper function for N-points measurements
\r
806 By default, measurements are done on the retraction
\r
809 dialog = wx.MessageDialog(None, message, 'Info', wx.OK)
\r
812 figure = self.GetActiveFigure()
\r
814 xvector = self.displayed_plot.curves[block].x
\r
815 yvector = self.displayed_plot.curves[block].y
\r
817 clicked_points = figure.ginput(N, timeout=-1, show_clicks=True)
\r
820 for clicked_point in clicked_points:
\r
821 point = lib.clickedpoint.ClickedPoint()
\r
822 point.absolute_coords = clicked_point[0], clicked_point[1]
\r
824 #TODO: make this optional?
\r
825 #so far, the clicked point is taken, not the corresponding data point
\r
826 point.find_graph_coords(xvector, yvector)
\r
827 point.is_line_edge = True
\r
828 point.is_marker = True
\r
829 points.append(point)
\r
832 def do_copylog(self):
\r
834 Copies all files in the current playlist that have a note to the destination folder.
\r
835 destination: select folder where you want the files to be copied
\r
836 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
838 playlist = self.GetActivePlaylist()
\r
839 if playlist is not None:
\r
840 destination = self.GetStringFromConfig('core', 'copylog', 'destination')
\r
841 if not os.path.isdir(destination):
\r
842 os.makedirs(destination)
\r
843 for current_file in playlist.files:
\r
844 if current_file.note:
\r
845 shutil.copy(current_file.filename, destination)
\r
846 if current_file.driver.filetype == 'mfp1d':
\r
847 filename = current_file.filename.replace('deflection', 'LVDT', 1)
\r
848 path, name = os.path.split(filename)
\r
849 filename = os.path.join(path, 'lvdt', name)
\r
850 use_LVDT_folder = self.GetBoolFromConfig('core', 'copylog', 'use_LVDT_folder')
\r
851 if use_LVDT_folder:
\r
852 destination = os.path.join(destination, 'LVDT')
\r
853 shutil.copy(filename, destination)
\r
855 def do_plotmanipulators(self):
\r
857 Please select the plotmanipulators you would like to use
\r
858 and define the order in which they will be applied to the data.
\r
860 Click 'Execute' to apply your changes.
\r
864 def do_preferences(self):
\r
866 Please set general preferences for Hooke here.
\r
867 hide_curve_extension: hides the extension of the force curve files.
\r
868 not recommended for 'picoforce' files
\r
874 Use this command for testing purposes. You find do_test in hooke.py.
\r
878 def do_version(self):
\r
882 Prints the current version and codename, plus library version. Useful for debugging.
\r
884 self.AppendToOutput('Hooke ' + __version__ + ' (' + __codename__ + ')')
\r
885 self.AppendToOutput('Released on: ' + __releasedate__)
\r
886 self.AppendToOutput('---')
\r
887 self.AppendToOutput('Python version: ' + python_version)
\r
888 self.AppendToOutput('WxPython version: ' + wx_version)
\r
889 self.AppendToOutput('Matplotlib version: ' + mpl_version)
\r
890 self.AppendToOutput('SciPy version: ' + scipy_version)
\r
891 self.AppendToOutput('NumPy version: ' + numpy_version)
\r
892 self.AppendToOutput('ConfigObj version: ' + configobj_version)
\r
893 self.AppendToOutput('wxPropertyGrid version: ' + '.'.join([str(PROPGRID_MAJOR), str(PROPGRID_MINOR), str(PROPGRID_RELEASE)]))
\r
894 self.AppendToOutput('---')
\r
895 self.AppendToOutput('Platform: ' + str(platform.uname()))
\r
896 self.AppendToOutput('******************************')
\r
897 self.AppendToOutput('Loaded plugins')
\r
898 self.AppendToOutput('---')
\r
900 #sort the plugins into alphabetical order
\r
901 plugins_list = [key for key, value in self.plugins.iteritems()]
\r
902 plugins_list.sort()
\r
903 for plugin in plugins_list:
\r
904 self.AppendToOutput(plugin)
\r
906 def UpdateNote(self):
\r
907 #update the note for the active file
\r
908 active_file = self.GetActiveFile()
\r
909 if active_file is not None:
\r
910 self.panelNote.Editor.SetValue(active_file.note)
\r
912 def UpdatePlaylistsTreeSelection(self):
\r
913 playlist = self.GetActivePlaylist()
\r
914 if playlist is not None:
\r
915 if playlist.index >= 0:
\r
916 self.statusbar.SetStatusText(playlist.get_status_string(), 0)
\r
920 def UpdatePlot(self, plot=None):
\r
922 def add_to_plot(curve, set_scale=True):
\r
923 if curve.visible and curve.x and curve.y:
\r
924 #get the index of the subplot to use as destination
\r
925 destination = (curve.destination.column - 1) * number_of_rows + curve.destination.row - 1
\r
926 #set all parameters for the plot
\r
927 axes_list[destination].set_title(curve.title)
\r
929 axes_list[destination].set_xlabel(curve.prefix.x + curve.units.x)
\r
930 axes_list[destination].set_ylabel(curve.prefix.y + curve.units.y)
\r
931 #set the formatting details for the scale
\r
932 formatter_x = lib.curve.PrefixFormatter(curve.decimals.x, curve.prefix.x, use_zero)
\r
933 formatter_y = lib.curve.PrefixFormatter(curve.decimals.y, curve.prefix.y, use_zero)
\r
934 axes_list[destination].xaxis.set_major_formatter(formatter_x)
\r
935 axes_list[destination].yaxis.set_major_formatter(formatter_y)
\r
936 if curve.style == 'plot':
\r
937 axes_list[destination].plot(curve.x, curve.y, color=curve.color, label=curve.label, lw=curve.linewidth, zorder=1)
\r
938 if curve.style == 'scatter':
\r
939 axes_list[destination].scatter(curve.x, curve.y, color=curve.color, label=curve.label, s=curve.size, zorder=2)
\r
940 #add the legend if necessary
\r
942 axes_list[destination].legend()
\r
945 active_file = self.GetActiveFile()
\r
946 if not active_file.driver:
\r
947 #the first time we identify a file, the following need to be set
\r
948 active_file.identify(self.drivers)
\r
949 for curve in active_file.plot.curves:
\r
950 curve.decimals.x = self.GetIntFromConfig('core', 'preferences', 'x_decimals')
\r
951 curve.decimals.y = self.GetIntFromConfig('core', 'preferences', 'y_decimals')
\r
952 curve.legend = self.GetBoolFromConfig('core', 'preferences', 'legend')
\r
953 curve.prefix.x = self.GetStringFromConfig('core', 'preferences', 'x_prefix')
\r
954 curve.prefix.y = self.GetStringFromConfig('core', 'preferences', 'y_prefix')
\r
955 if active_file.driver is None:
\r
956 self.AppendToOutput('Invalid file: ' + active_file.filename)
\r
958 self.displayed_plot = copy.deepcopy(active_file.plot)
\r
959 #add raw curves to plot
\r
960 self.displayed_plot.raw_curves = copy.deepcopy(self.displayed_plot.curves)
\r
961 #apply all active plotmanipulators
\r
962 self.displayed_plot = self.ApplyPlotmanipulators(self.displayed_plot, active_file)
\r
963 #add corrected curves to plot
\r
964 self.displayed_plot.corrected_curves = copy.deepcopy(self.displayed_plot.curves)
\r
967 self.displayed_plot = copy.deepcopy(plot)
\r
969 figure = self.GetActiveFigure()
\r
972 #use '0' instead of e.g. '0.00' for scales
\r
973 use_zero = self.GetBoolFromConfig('core', 'preferences', 'use_zero')
\r
974 #optionally remove the extension from the title of the plot
\r
975 hide_curve_extension = self.GetBoolFromConfig('core', 'preferences', 'hide_curve_extension')
\r
976 if hide_curve_extension:
\r
977 title = lh.remove_extension(self.displayed_plot.title)
\r
979 title = self.displayed_plot.title
\r
980 figure.suptitle(title, fontsize=14)
\r
981 #create the list of all axes necessary (rows and columns)
\r
983 number_of_columns = max([curve.destination.column for curve in self.displayed_plot.curves])
\r
984 number_of_rows = max([curve.destination.row for curve in self.displayed_plot.curves])
\r
985 for index in range(number_of_rows * number_of_columns):
\r
986 axes_list.append(figure.add_subplot(number_of_rows, number_of_columns, index + 1))
\r
988 #add all curves to the corresponding plots
\r
989 for curve in self.displayed_plot.curves:
\r
992 #make sure the titles of 'subplots' do not overlap with the axis labels of the 'main plot'
\r
993 figure.subplots_adjust(hspace=0.3)
\r
996 self.panelResults.ClearResults()
\r
997 if self.displayed_plot.results.has_key(self.results_str):
\r
998 for curve in self.displayed_plot.results[self.results_str].results:
\r
999 add_to_plot(curve, set_scale=False)
\r
1000 self.panelResults.DisplayResults(self.displayed_plot.results[self.results_str])
\r
1002 self.panelResults.ClearResults()
\r
1004 figure.canvas.draw()
\r
1006 def _on_curve_select(self, playlist, curve):
\r
1007 #create the plot tab and add playlist to the dictionary
\r
1008 plotPanel = panel.plot.PlotPanel(self, ID_FirstPlot + len(self.playlists))
\r
1009 notebook_tab = self._c['notebook'].AddPage(plotPanel, playlist.name, True)
\r
1010 #tab_index = self._c['notebook'].GetSelection()
\r
1011 playlist.figure = plotPanel.get_figure()
\r
1012 self.playlists[playlist.name] = playlist
\r
1013 #self.playlists[playlist.name] = [playlist, figure]
\r
1014 self.statusbar.SetStatusText(playlist.get_status_string(), 0)
\r
1019 def _on_playlist_left_doubleclick(self):
\r
1020 index = self._c['notebook'].GetSelection()
\r
1021 current_playlist = self._c['notebook'].GetPageText(index)
\r
1022 if current_playlist != playlist_name:
\r
1023 index = self._GetPlaylistTab(playlist_name)
\r
1024 self._c['notebook'].SetSelection(index)
\r
1025 self.statusbar.SetStatusText(playlist.get_status_string(), 0)
\r
1029 def _on_playlist_delete(self, playlist):
\r
1030 notebook = self.Parent.plotNotebook
\r
1031 index = self.Parent._GetPlaylistTab(playlist.name)
\r
1032 notebook.SetSelection(index)
\r
1033 notebook.DeletePage(notebook.GetSelection())
\r
1034 self.Parent.DeleteFromPlaylists(playlist_name)
\r
1037 class HookeApp (wx.App):
\r
1038 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
\r
1040 self.commands = commands
\r
1041 self.inqueue = inqueue
\r
1042 self.outqueue = outqueue
\r
1043 super(HookeApp, self).__init__(*args, **kwargs)
\r
1046 self.SetAppName('Hooke')
\r
1047 self.SetVendorName('')
\r
1048 self._setup_splash_screen()
\r
1050 height = int(self.gui.config['main height']) # HACK: config should convert
\r
1051 width = int(self.gui.config['main width'])
\r
1052 top = int(self.gui.config['main top'])
\r
1053 left = int(self.gui.config['main left'])
\r
1055 # Sometimes, the ini file gets confused and sets 'left' and
\r
1056 # 'top' to large negative numbers. Here we catch and fix
\r
1057 # this. Keep small negative numbers, the user might want
\r
1065 'frame': HookeFrame(
\r
1066 self.gui, self.commands, parent=None, title='Hooke',
\r
1067 pos=(left, top), size=(width, height),
\r
1068 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
\r
1070 self._c['frame'].Show(True)
\r
1071 self.SetTopWindow(self._c['frame'])
\r
1074 def _setup_splash_screen(self):
\r
1075 if self.gui.config['show splash screen']:
\r
1076 path = self.gui.config['splash screen image']
\r
1077 if os.path.isfile(path):
\r
1078 duration = int(self.gui.config['splash screen duration']) # HACK: config should decode types
\r
1080 bitmap=wx.Image(path).ConvertToBitmap(),
\r
1081 splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
\r
1082 milliseconds=duration,
\r
1085 # For some reason splashDuration and sleep do not
\r
1086 # correspond to each other at least not on Windows.
\r
1087 # Maybe it's because duration is in milliseconds and
\r
1088 # sleep in seconds. Thus we need to increase the
\r
1089 # sleep time a bit. A factor of 1.2 seems to work.
\r
1091 time.sleep(sleepFactor * duration / 1000)
\r
1097 class GUI (UserInterface):
\r
1098 """wxWindows graphical user interface.
\r
1100 def __init__(self):
\r
1101 super(GUI, self).__init__(name='gui')
\r
1103 def default_settings(self):
\r
1104 """Return a list of :class:`hooke.config.Setting`\s for any
\r
1105 configurable UI settings.
\r
1107 The suggested section setting is::
\r
1109 Setting(section=self.setting_section, help=self.__doc__)
\r
1112 Setting(section=self.setting_section, help=self.__doc__),
\r
1113 Setting(section=self.setting_section, option='icon image',
\r
1114 value=os.path.join('doc', 'img', 'microscope.ico'),
\r
1115 help='Path to the hooke icon image.'),
\r
1116 Setting(section=self.setting_section, option='show splash screen',
\r
1118 help='Enable/disable the splash screen'),
\r
1119 Setting(section=self.setting_section, option='splash screen image',
\r
1120 value=os.path.join('doc', 'img', 'hooke.jpg'),
\r
1121 help='Path to the Hooke splash screen image.'),
\r
1122 Setting(section=self.setting_section, option='splash screen duration',
\r
1124 help='Duration of the splash screen in milliseconds.'),
\r
1125 Setting(section=self.setting_section, option='perspective path',
\r
1126 value=os.path.join('resources', 'gui', 'perspective'),
\r
1127 help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.
\r
1128 Setting(section=self.setting_section, option='hide extensions',
\r
1130 help='Hide file extensions when displaying names.'),
\r
1131 Setting(section=self.setting_section, option='folders-workdir',
\r
1133 help='This should probably go...'),
\r
1134 Setting(section=self.setting_section, option='folders-filters',
\r
1136 help='This should probably go...'),
\r
1137 Setting(section=self.setting_section, option='active perspective',
\r
1139 help='Name of active perspective file (or "Default").'),
\r
1140 Setting(section=self.setting_section, option='folders-filter-index',
\r
1142 help='This should probably go...'),
\r
1143 Setting(section=self.setting_section, option='main height',
\r
1145 help='Height of main window in pixels.'),
\r
1146 Setting(section=self.setting_section, option='main width',
\r
1148 help='Width of main window in pixels.'),
\r
1149 Setting(section=self.setting_section, option='main top',
\r
1151 help='Pixels from screen top to top of main window.'),
\r
1152 Setting(section=self.setting_section, option='main left',
\r
1154 help='Pixels from screen left to left of main window.'),
\r
1155 Setting(section=self.setting_section, option='selected command',
\r
1156 value='load playlist',
\r
1157 help='Name of the initially selected command.'),
\r
1160 def _app(self, commands, ui_to_command_queue, command_to_ui_queue):
\r
1164 app = HookeApp(gui=self,
\r
1165 commands=commands,
\r
1166 inqueue=ui_to_command_queue,
\r
1167 outqueue=command_to_ui_queue,
\r
1168 redirect=redirect)
\r
1171 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
\r
1172 app = self._app(commands, ui_to_command_queue, command_to_ui_queue)
\r