Broke menu, navbar, statusbar, panel.notebook, and panel.welcome out of gui
[hooke.git] / hooke / ui / gui / __init__.py
1 # Copyright\r
2 \r
3 """Defines :class:`GUI` providing a wxWindows interface to Hooke.\r
4 """\r
5 \r
6 WX_GOOD=['2.8']\r
7 \r
8 import wxversion\r
9 wxversion.select(WX_GOOD)\r
10 \r
11 import copy\r
12 import os.path\r
13 import platform\r
14 import shutil\r
15 import time\r
16 \r
17 import wx.html\r
18 import wx.aui as aui\r
19 import wx.lib.evtmgr as evtmgr\r
20 \r
21 \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
26 \r
27 from matplotlib.ticker import FuncFormatter\r
28 \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
39 \r
40 \r
41 class HookeFrame (wx.Frame):\r
42     """The main Hooke-interface window.\r
43 \r
44     \r
45     """\r
46     def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):\r
47         super(HookeFrame, self).__init__(*args, **kwargs)\r
48         self.gui = gui\r
49         self.commands = commands\r
50         self.inqueue = inqueue\r
51         self.outqueue = outqueue\r
52         self._perspectives = {}  # {name: perspective_str}\r
53         self._c = {}\r
54 \r
55         self.SetIcon(wx.Icon(self.gui.config['icon image'], wx.BITMAP_TYPE_ICO))\r
56 \r
57         # setup frame manager\r
58         self._c['manager'] = aui.AuiManager()\r
59         self._c['manager'].SetManagedWindow(self)\r
60 \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
66 \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
71 \r
72         self._setup_panels()\r
73         self._setup_toolbars()\r
74         self._c['manager'].Update()  # commit pending changes\r
75 \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
79             callbacks={},\r
80             )\r
81         self.SetMenuBar(self._c['menu bar'])\r
82 \r
83         self._c['status bar'] = statubar.StatusBar(\r
84             parent=self,\r
85             style=wx.ST_SIZEGRIP)\r
86 \r
87         self._update_perspectives()\r
88         self._bind_events()\r
89 \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
100 \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
105                     parent=self,\r
106                     dir=self.gui.config['folders-workdir'],\r
107                     size=(200, 250),\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
113                     callbacks={},\r
114                     parent=self,\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
120                     parent=self,\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
128                     callbacks={\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
133                         },\r
134                     parent=self,\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
140                     parent=self,\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
145                     parent=self,\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
150             ]:\r
151             self._add_panel(label, p, style)\r
152         self._c['assistant'].SetEditable(False)\r
153 \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
164         else:\r
165             assert style == 'bottom', style\r
166             info.Bottom().CloseButton(True).MaximizeButton(False)\r
167         self._c['manager'].AddPane(panel, info)\r
168 \r
169     def _setup_toolbars(self):\r
170         self._c['navbar'] = navbar.NavBar(\r
171             callbacks={\r
172                 'next': self._next_curve,\r
173                 'previous': self._previous_curve,\r
174                 },\r
175             parent=self,\r
176             style=wx.TB_FLAT | wx.TB_NODIVIDER)\r
177 \r
178         self._c['manager'].AddPane(\r
179             self._c['navbar'],\r
180             aui.AuiPaneInfo().Name('Navigation').Caption('Navigation'\r
181                 ).ToolbarPane().Top().Layer(1).Row(1).LeftDockable(False\r
182                 ).RightDockable(False))\r
183 \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
187         # fail error.\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
195 \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
198 \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
203 \r
204         treeCtrl = self._c['folders'].GetTreeCtrl()\r
205         treeCtrl.Bind(wx.EVT_LEFT_DCLICK, self._on_dir_ctrl_left_double_click)\r
206         \r
207         # TODO: playlist callbacks\r
208         return # TODO: cleanup\r
209         evtmgr.eventManager.Register(self.OnUpdateNote, wx.EVT_BUTTON, self.panelNote.UpdateButton)\r
210         #property editor\r
211         self.panelProperties.pg.Bind(wxpg.EVT_PG_CHANGED, self.OnPropGridChanged)\r
212         #results panel\r
213         self.panelResults.results_list.OnCheckItem = self.OnResultsCheck\r
214 \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
221             return -1\r
222         else:\r
223             count = 0\r
224             selected_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item)\r
225             while selected_item.IsOk():\r
226                 count += 1\r
227                 selected_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item)\r
228             return count\r
229 \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
233                 return index\r
234         return -1\r
235 \r
236     def _restore_perspective(self, name):\r
237         # TODO: cleanup\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
244 \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
251 \r
252     def execute_command(self, _class, method, command, args):\r
253         self.cmd.inqueue.put(CommandMessage(command, args))\r
254         while True:\r
255             msg = self.cmd.outqueue.get()\r
256             if isinstance(msg, Exit):\r
257                 return True\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
261                 break\r
262             elif isinstance(msg, ReloadUserInterfaceConfig):\r
263                 self.cmd.ui.reload_config(msg.config)\r
264                 continue\r
265             elif isinstance(msg, Request):\r
266                 self._handle_request(msg)\r
267                 continue\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
272                 #exec(command)\r
273 \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
277 \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
283         else:\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
287         else:\r
288             self.panelAssistant.ChangeValue('')\r
289         panel.propertyeditor.PropertyEditor.Initialize(self.panelProperties, properties)\r
290         self.gui.config['selected command'] = command\r
291 \r
292     def AddPlaylistFromFiles(self, files=[], name='Untitled'):\r
293         if files:\r
294             playlist = lib.playlist.Playlist(self, self.drivers)\r
295             for item in files:\r
296                 playlist.add_curve(item)\r
297         if playlist.count > 0:\r
298             playlist.name = self._GetUniquePlaylistName(name)\r
299             playlist.reset()\r
300             self.AddTayliss(playlist)\r
301 \r
302     def AppendToOutput(self, text):\r
303         self.panelOutput.AppendText(''.join([text, '\n']))\r
304 \r
305     def AppliesPlotmanipulator(self, name):\r
306         '''\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
309         '''\r
310         return self.GetBoolFromConfig('core', 'plotmanipulators', name)\r
311 \r
312     def ApplyPlotmanipulators(self, plot, plot_file):\r
313         '''\r
314         Apply all active plotmanipulators.\r
315         '''\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
322 \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
327             return figure\r
328         return None\r
329 \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
334         return None\r
335 \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
340         return None\r
341 \r
342     def GetDisplayedPlot(self):\r
343         plot = copy.deepcopy(self.displayed_plot)\r
344         #plot.curves = []\r
345         #plot.curves = copy.deepcopy(plot.curves)\r
346         return plot\r
347 \r
348     def GetDisplayedPlotCorrected(self):\r
349         plot = copy.deepcopy(self.displayed_plot)\r
350         plot.curves = []\r
351         plot.curves = copy.deepcopy(plot.corrected_curves)\r
352         return plot\r
353 \r
354     def GetDisplayedPlotRaw(self):\r
355         plot = copy.deepcopy(self.displayed_plot)\r
356         plot.curves = []\r
357         plot.curves = copy.deepcopy(plot.raw_curves)\r
358         return plot\r
359 \r
360     def GetDockArt(self):\r
361         return self._c['manager'].GetArtProvider()\r
362 \r
363     def GetPlotmanipulator(self, name):\r
364         '''\r
365         Returns a plot manipulator function from its name\r
366         '''\r
367         for plotmanipulator in self.plotmanipulators:\r
368             if plotmanipulator.name == name:\r
369                 return plotmanipulator\r
370         return None\r
371 \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
379             return menu_item\r
380         else:\r
381             return None\r
382 \r
383     def HasPlotmanipulator(self, name):\r
384         '''\r
385         returns True if the plotmanipulator 'name' is loaded, False otherwise\r
386         '''\r
387         for plotmanipulator in self.plotmanipulators:\r
388             if plotmanipulator.command == name:\r
389                 return True\r
390         return False\r
391 \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
399         dialog.ShowModal()\r
400         dialog.Destroy()\r
401 \r
402     def _on_close(self, event):\r
403         # apply changes\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
411         self.Destroy()\r
412 \r
413     def _update_perspectives(self):\r
414         """Add perspectives to menubar and _perspectives.\r
415         """\r
416         self._perspectives = {\r
417             'Default': self._c['manager'].SavePerspective(),\r
418             }\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
425                     continue\r
426                 fpath = os.path.join(path, fpath)\r
427                 if not os.path.isfile(fpath):\r
428                     continue\r
429                 perspective = None\r
430                 with open(fpath, 'rU') as f:\r
431                     perspective = f.readline()\r
432                 if perspective:\r
433                     self._perspectives[name] = perspective\r
434 \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
438 \r
439         self._update_perspective_menu()\r
440         self._restore_perspective(selected_perspective)\r
441 \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
447 \r
448     def _on_restore_perspective(self, event):\r
449         name = self.MenuBar.FindItemById(event.GetId()).GetLabel()\r
450         self._restore_perspective(name)\r
451 \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
458                     return True\r
459             return False\r
460 \r
461         done = False\r
462         while not done:\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
466                 return\r
467             else:\r
468                 name = dialog.GetValue()\r
469 \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
473                     done = True\r
474             else:\r
475                 done = True\r
476 \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
486 #        else:\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
505 #                if item == name:\r
506 #                    menu_item.Check()\r
507 #            #add the new perspective to _perspectives\r
508 #            self._perspectives[name] = perspective\r
509 \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
516             parent=self,\r
517             label='Delete perspective(s)',\r
518             style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)\r
519         dialog.CenterOnScreen()\r
520         dialog.ShowModal()\r
521         dialog.Destroy()\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
527 \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
534                                 item+'.txt')\r
535             remove(path)\r
536         self._update_perspective_menu()\r
537 \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
543         event.Skip()\r
544 \r
545     def _on_erase_background(self, event):\r
546         event.Skip()\r
547 \r
548     def _next_curve(self, *args):\r
549         '''\r
550         NEXT\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
553         -----\r
554         Syntax: next, n\r
555         '''\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
561         else:\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
572                 playlist.next()\r
573                 self._c['status bar'].set_playlist(playlist)\r
574                 self.UpdateNote()\r
575                 self.UpdatePlot()\r
576 \r
577     def _previous_curve(self, *args):\r
578         '''\r
579         PREVIOUS\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
582         -------\r
583         Syntax: previous, p\r
584         '''\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
591         else:\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
601             self.UpdateNote()\r
602             self.UpdatePlot()\r
603 \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
608 \r
609     def OnPaneClose(self, event):\r
610         event.Skip()\r
611 \r
612     def OnPropGridChanged (self, event):\r
613         prop = event.GetProperty()\r
614         if prop:\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
622 \r
623             config[property_section][property_key]['value'] = property_value\r
624 \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
630             self.UpdatePlot()\r
631 \r
632 \r
633     def _on_size(self, event):\r
634         event.Skip()\r
635 \r
636     def OnUpdateNote(self, event):\r
637         '''\r
638         Saves the note to the active file.\r
639         '''\r
640         active_file = self.GetActiveFile()\r
641         active_file.note = self.panelNote.Editor.GetValue()\r
642 \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
647 \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
655 \r
656     def _clickize(self, xvector, yvector, index):\r
657         '''\r
658         Returns a ClickedPoint() object from an index and vectors of x, y coordinates\r
659         '''\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
664         return point\r
665 \r
666     def _delta(self, message='Click 2 points', block=0):\r
667         '''\r
668         Calculates the difference between two clicked points\r
669         '''\r
670         clicked_points = self._measure_N_points(N=2, message=message, block=block)\r
671 \r
672         plot = self.GetDisplayedPlotCorrected()\r
673         curve = plot.curves[block]\r
674 \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
682 \r
683         return delta\r
684 \r
685     def _measure_N_points(self, N, message='', block=0):\r
686         '''\r
687         General helper function for N-points measurements\r
688         By default, measurements are done on the retraction\r
689         '''\r
690         if message:\r
691             dialog = wx.MessageDialog(None, message, 'Info', wx.OK)\r
692             dialog.ShowModal()\r
693 \r
694         figure = self.GetActiveFigure()\r
695 \r
696         xvector = self.displayed_plot.curves[block].x\r
697         yvector = self.displayed_plot.curves[block].y\r
698 \r
699         clicked_points = figure.ginput(N, timeout=-1, show_clicks=True)\r
700 \r
701         points = []\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
705             point.dest = 0\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
712         return points\r
713 \r
714     def do_copylog(self):\r
715         '''\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
719         '''\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
736 \r
737     def do_plotmanipulators(self):\r
738         '''\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
741 \r
742         Click 'Execute' to apply your changes.\r
743         '''\r
744         self.UpdatePlot()\r
745 \r
746     def do_preferences(self):\r
747         '''\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
751         '''\r
752         pass\r
753 \r
754     def do_test(self):\r
755         '''\r
756         Use this command for testing purposes. You find do_test in hooke.py.\r
757         '''\r
758         pass\r
759 \r
760     def do_version(self):\r
761         '''\r
762         VERSION\r
763         ------\r
764         Prints the current version and codename, plus library version. Useful for debugging.\r
765         '''\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
781 \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
787 \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
793 \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
799                 self.UpdateNote()\r
800                 self.UpdatePlot()\r
801 \r
802     def UpdatePlot(self, plot=None):\r
803 \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
810                 if set_scale:\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
823                 if curve.legend:\r
824                     axes_list[destination].legend()\r
825 \r
826         if plot is None:\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
839                 return\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
847         else:\r
848             active_file = None\r
849             self.displayed_plot = copy.deepcopy(plot)\r
850 \r
851         figure = self.GetActiveFigure()\r
852         figure.clear()\r
853 \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
860         else:\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
864         axes_list =[]\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
869 \r
870         #add all curves to the corresponding plots\r
871         for curve in self.displayed_plot.curves:\r
872             add_to_plot(curve)\r
873 \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
876 \r
877         #display results\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
883         else:\r
884             self.panelResults.ClearResults()\r
885         #refresh the plot\r
886         figure.canvas.draw()\r
887 \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
897         self.UpdateNote()\r
898         self.UpdatePlot()\r
899 \r
900 \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
908         self.UpdateNote()\r
909         self.UpdatePlot()\r
910 \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
917         \r
918 \r
919 class HookeApp (wx.App):\r
920     """A :class:`wx.App` wrapper around :class:`HookeFrame`.\r
921 \r
922     Tosses up a splash screen and then loads :class:`HookeFrame` in\r
923     its own window.\r
924     """\r
925     def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):\r
926         self.gui = gui\r
927         self.commands = commands\r
928         self.inqueue = inqueue\r
929         self.outqueue = outqueue\r
930         super(HookeApp, self).__init__(*args, **kwargs)\r
931 \r
932     def OnInit(self):\r
933         self.SetAppName('Hooke')\r
934         self.SetVendorName('')\r
935         self._setup_splash_screen()\r
936 \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
941 \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
945         # those.\r
946         if left < -width:\r
947             left = 0\r
948         if top < -height:\r
949             top = 0\r
950 \r
951         self._c = {\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
957             }\r
958         self._c['frame'].Show(True)\r
959         self.SetTopWindow(self._c['frame'])\r
960         return True\r
961 \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
967                 wx.SplashScreen(\r
968                     bitmap=wx.Image(path).ConvertToBitmap(),\r
969                     splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,\r
970                     milliseconds=duration,\r
971                     parent=None)\r
972                 wx.Yield()\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
978                 sleepFactor = 1.2\r
979                 time.sleep(sleepFactor * duration / 1000)\r
980 \r
981 \r
982 class GUI (UserInterface):\r
983     """wxWindows graphical user interface.\r
984     """\r
985     def __init__(self):\r
986         super(GUI, self).__init__(name='gui')\r
987 \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
991 \r
992         The suggested section setting is::\r
993 \r
994             Setting(section=self.setting_section, help=self.__doc__)\r
995         """\r
996         return [\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
1002                     value=True,\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
1008                     value=1000,\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
1014                     value=False,\r
1015                     help='Hide file extensions when displaying names.'),\r
1016             Setting(section=self.setting_section, option='folders-workdir',\r
1017                     value='.',\r
1018                     help='This should probably go...'),\r
1019             Setting(section=self.setting_section, option='folders-filters',\r
1020                     value='.',\r
1021                     help='This should probably go...'),\r
1022             Setting(section=self.setting_section, option='active perspective',\r
1023                     value='Default',\r
1024                     help='Name of active perspective file (or "Default").'),\r
1025             Setting(section=self.setting_section, option='folders-filter-index',\r
1026                     value='0',\r
1027                     help='This should probably go...'),\r
1028             Setting(section=self.setting_section, option='main height',\r
1029                     value=500,\r
1030                     help='Height of main window in pixels.'),\r
1031             Setting(section=self.setting_section, option='main width',\r
1032                     value=500,\r
1033                     help='Width of main window in pixels.'),\r
1034             Setting(section=self.setting_section, option='main top',\r
1035                     value=0,\r
1036                     help='Pixels from screen top to top of main window.'),\r
1037             Setting(section=self.setting_section, option='main left',\r
1038                     value=0,\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
1043             ]\r
1044 \r
1045     def _app(self, commands, ui_to_command_queue, command_to_ui_queue):\r
1046         redirect = True\r
1047         if __debug__:\r
1048             redirect=False\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
1054         return app\r
1055 \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
1058         app.MainLoop()\r