720c364befaee1ef2c5d464cebe3b77f4ef138af
[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 panel as panel\r
35 from . import prettyformat as prettyformat\r
36 \r
37 \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
45 \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
49         lines = [\r
50             '<h1>Welcome to Hooke</h1>',\r
51             '<h3>Features</h3>',\r
52             '<ul>',\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
59             '</ul>',\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
63             ]\r
64         ctrl.SetPage('\n'.join(lines))\r
65         return ctrl\r
66 \r
67 \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
72         self._c = {\r
73             'previous': self.AddLabelTool(\r
74                 id=wx.ID_PREVIEW_PREVIOUS,\r
75                 label='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
81                 label='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
85             }\r
86         self.Realize()\r
87 \r
88 \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
93 \r
94 \r
95 class ViewMenu (wx.Menu):\r
96     def __init__(self, *args, **kwargs):\r
97         super(ViewMenu, self).__init__(*args, **kwargs)\r
98         self._c = {\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
111             }\r
112         for item in self._c.values():\r
113             item.Check()\r
114 \r
115 \r
116 class PerspectiveMenu (wx.Menu):\r
117     def __init__(self, *args, **kwargs):\r
118         super(PerspectiveMenu, self).__init__(*args, **kwargs)\r
119         self._c = {}\r
120 \r
121     def update(self, perspectives, selected, callback):\r
122         """Rebuild the perspectives menu.\r
123         """\r
124         for item in self.GetMenuItems():\r
125             self.UnBind(item)\r
126             self.DeleteItem(item)\r
127         self._c = {\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
130             }\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
137             \r
138 \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
143 \r
144 \r
145 class MenuBar (wx.MenuBar):\r
146     def __init__(self, *args, **kwargs):\r
147         super(MenuBar, self).__init__(*args, **kwargs)\r
148         self._c = {\r
149             'file': FileMenu(),\r
150             'view': ViewMenu(),\r
151             'perspective': PerspectiveMenu(),\r
152             'help': HelpMenu(),\r
153             }\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
158 \r
159 \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
166 \r
167 \r
168 class HookeFrame (wx.Frame):\r
169     def __init__(self, gui, commands, *args, **kwargs):\r
170         super(HookeFrame, self).__init__(*args, **kwargs)\r
171         self.gui = gui\r
172         self.commands = commands\r
173         self._perspectives = {}  # {name: perspective_str}\r
174         self._c = {}\r
175 \r
176         self.SetIcon(wx.Icon(self.gui.config['icon image'], wx.BITMAP_TYPE_ICO))\r
177 \r
178         # setup frame manager\r
179         self._c['manager'] = aui.AuiManager()\r
180         self._c['manager'].SetManagedWindow(self)\r
181 \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
187 \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
192 \r
193         self._setup_panels()\r
194         self._setup_toolbars()\r
195         self._c['manager'].Update()  # commit pending changes\r
196 \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
200             )\r
201         self.SetMenuBar(self._c['menu bar'])\r
202 \r
203         self._c['status bar'] = StatusBar(self, style=wx.ST_SIZEGRIP)\r
204 \r
205         self._update_perspectives()\r
206         self._bind_events()\r
207 \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
218 \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
223                     parent=self,\r
224                     dir=self.gui.config['folders-workdir'],\r
225                     size=(200, 250),\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
231                     callbacks={},\r
232                     parent=self,\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
238                     parent=self,\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
246                     callbacks={\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
251                         },\r
252                     parent=self,\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
258                     parent=self,\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
263                     parent=self,\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
268             ]:\r
269             self._add_panel(label, p, style)\r
270         self._c['assistant'].SetEditable(False)\r
271 \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
282         else:\r
283             assert style == 'bottom', style\r
284             info.Bottom().CloseButton(True).MaximizeButton(False)\r
285         self._c['manager'].AddPane(panel, info)\r
286 \r
287     def _setup_toolbars(self):\r
288         self._c['navbar'] = NavBar(self, style=wx.TB_FLAT | wx.TB_NODIVIDER)\r
289 \r
290         self._c['manager'].AddPane(\r
291             self._c['navbar'],\r
292             aui.AuiPaneInfo().Name('Navigation').Caption('Navigation'\r
293                 ).ToolbarPane().Top().Layer(1).Row(1).LeftDockable(False\r
294                 ).RightDockable(False))\r
295 \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
299         # fail error.\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
307 \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
310 \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
315 \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
318 \r
319         treeCtrl = self._c['folders'].GetTreeCtrl()\r
320         treeCtrl.Bind(wx.EVT_LEFT_DCLICK, self._on_dir_ctrl_left_double_click)\r
321         \r
322         # TODO: playlist callbacks\r
323         return # TODO: cleanup\r
324         evtmgr.eventManager.Register(self.OnUpdateNote, wx.EVT_BUTTON, self.panelNote.UpdateButton)\r
325         #property editor\r
326         self.panelProperties.pg.Bind(wxpg.EVT_PG_CHANGED, self.OnPropGridChanged)\r
327         #results panel\r
328         self.panelResults.results_list.OnCheckItem = self.OnResultsCheck\r
329 \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
336             return -1\r
337         else:\r
338             count = 0\r
339             selected_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item)\r
340             while selected_item.IsOk():\r
341                 count += 1\r
342                 selected_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item)\r
343             return count\r
344 \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
348                 return index\r
349         return -1\r
350 \r
351     def _restore_perspective(self, name):\r
352         # TODO: cleanup\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
359 \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
366 \r
367     def execute_command(self, _class, method, command, args):\r
368         self.cmd.inqueue.put(CommandMessage(command, args))\r
369         while True:\r
370             msg = self.cmd.outqueue.get()\r
371             if isinstance(msg, Exit):\r
372                 return True\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
376                 break\r
377             elif isinstance(msg, ReloadUserInterfaceConfig):\r
378                 self.cmd.ui.reload_config(msg.config)\r
379                 continue\r
380             elif isinstance(msg, Request):\r
381                 self._handle_request(msg)\r
382                 continue\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
387                 #exec(command)\r
388 \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
392 \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
398         else:\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
402         else:\r
403             self.panelAssistant.ChangeValue('')\r
404         panel.propertyeditor.PropertyEditor.Initialize(self.panelProperties, properties)\r
405         self.gui.config['selected command'] = command\r
406 \r
407     def AddPlaylistFromFiles(self, files=[], name='Untitled'):\r
408         if files:\r
409             playlist = lib.playlist.Playlist(self, self.drivers)\r
410             for item in files:\r
411                 playlist.add_curve(item)\r
412         if playlist.count > 0:\r
413             playlist.name = self._GetUniquePlaylistName(name)\r
414             playlist.reset()\r
415             self.AddTayliss(playlist)\r
416 \r
417     def AppendToOutput(self, text):\r
418         self.panelOutput.AppendText(''.join([text, '\n']))\r
419 \r
420     def AppliesPlotmanipulator(self, name):\r
421         '''\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
424         '''\r
425         return self.GetBoolFromConfig('core', 'plotmanipulators', name)\r
426 \r
427     def ApplyPlotmanipulators(self, plot, plot_file):\r
428         '''\r
429         Apply all active plotmanipulators.\r
430         '''\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
437 \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
442             return figure\r
443         return None\r
444 \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
449         return None\r
450 \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
455         return None\r
456 \r
457     def GetDisplayedPlot(self):\r
458         plot = copy.deepcopy(self.displayed_plot)\r
459         #plot.curves = []\r
460         #plot.curves = copy.deepcopy(plot.curves)\r
461         return plot\r
462 \r
463     def GetDisplayedPlotCorrected(self):\r
464         plot = copy.deepcopy(self.displayed_plot)\r
465         plot.curves = []\r
466         plot.curves = copy.deepcopy(plot.corrected_curves)\r
467         return plot\r
468 \r
469     def GetDisplayedPlotRaw(self):\r
470         plot = copy.deepcopy(self.displayed_plot)\r
471         plot.curves = []\r
472         plot.curves = copy.deepcopy(plot.raw_curves)\r
473         return plot\r
474 \r
475     def GetDockArt(self):\r
476         return self._c['manager'].GetArtProvider()\r
477 \r
478     def GetPlotmanipulator(self, name):\r
479         '''\r
480         Returns a plot manipulator function from its name\r
481         '''\r
482         for plotmanipulator in self.plotmanipulators:\r
483             if plotmanipulator.name == name:\r
484                 return plotmanipulator\r
485         return None\r
486 \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
494             return menu_item\r
495         else:\r
496             return None\r
497 \r
498     def HasPlotmanipulator(self, name):\r
499         '''\r
500         returns True if the plotmanipulator 'name' is loaded, False otherwise\r
501         '''\r
502         for plotmanipulator in self.plotmanipulators:\r
503             if plotmanipulator.command == name:\r
504                 return True\r
505         return False\r
506 \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
514         dialog.ShowModal()\r
515         dialog.Destroy()\r
516 \r
517     def _on_close(self, event):\r
518         # apply changes\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
526         self.Destroy()\r
527 \r
528     def _update_perspectives(self):\r
529         """Add perspectives to menubar and _perspectives.\r
530         """\r
531         self._perspectives = {\r
532             'Default': self._c['manager'].SavePerspective(),\r
533             }\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
540                     continue\r
541                 fpath = os.path.join(path, fpath)\r
542                 if not os.path.isfile(fpath):\r
543                     continue\r
544                 perspective = None\r
545                 with open(fpath, 'rU') as f:\r
546                     perspective = f.readline()\r
547                 if perspective:\r
548                     self._perspectives[name] = perspective\r
549 \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
553 \r
554         self._update_perspective_menu()\r
555         self._restore_perspective(selected_perspective)\r
556 \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
562 \r
563     def _on_restore_perspective(self, event):\r
564         name = self.MenuBar.FindItemById(event.GetId()).GetLabel()\r
565         self._restore_perspective(name)\r
566 \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
573                     return True\r
574             return False\r
575 \r
576         done = False\r
577         while not done:\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
581                 return\r
582             else:\r
583                 name = dialog.GetValue()\r
584 \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
588                     done = True\r
589             else:\r
590                 done = True\r
591 \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
601 #        else:\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
620 #                if item == name:\r
621 #                    menu_item.Check()\r
622 #            #add the new perspective to _perspectives\r
623 #            self._perspectives[name] = perspective\r
624 \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
631             parent=self,\r
632             label='Delete perspective(s)',\r
633             style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)\r
634         dialog.CenterOnScreen()\r
635         dialog.ShowModal()\r
636         dialog.Destroy()\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
642 \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
649                                 item+'.txt')\r
650             remove(path)\r
651         self._update_perspective_menu()\r
652 \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
658         event.Skip()\r
659 \r
660     def _on_erase_background(self, event):\r
661         event.Skip()\r
662 \r
663     def OnExit(self, event):\r
664         self.Close()\r
665 \r
666     def _on_next(self, event):\r
667         '''\r
668         NEXT\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
671         -----\r
672         Syntax: next, n\r
673         '''\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
679         else:\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
690                 playlist.next()\r
691                 self.statusbar.SetStatusText(playlist.get_status_string(), 0)\r
692                 self.UpdateNote()\r
693                 self.UpdatePlot()\r
694 \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
699 \r
700     def OnPaneClose(self, event):\r
701         event.Skip()\r
702 \r
703     def _on_previous(self, event):\r
704         '''\r
705         PREVIOUS\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
708         -------\r
709         Syntax: previous, p\r
710         '''\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
717         else:\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
727             self.UpdateNote()\r
728             self.UpdatePlot()\r
729 \r
730     def OnPropGridChanged (self, event):\r
731         prop = event.GetProperty()\r
732         if prop:\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
740 \r
741             config[property_section][property_key]['value'] = property_value\r
742 \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
748             self.UpdatePlot()\r
749 \r
750 \r
751     def _on_size(self, event):\r
752         event.Skip()\r
753 \r
754     def OnUpdateNote(self, event):\r
755         '''\r
756         Saves the note to the active file.\r
757         '''\r
758         active_file = self.GetActiveFile()\r
759         active_file.note = self.panelNote.Editor.GetValue()\r
760 \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
765 \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
773 \r
774     def _clickize(self, xvector, yvector, index):\r
775         '''\r
776         Returns a ClickedPoint() object from an index and vectors of x, y coordinates\r
777         '''\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
782         return point\r
783 \r
784     def _delta(self, message='Click 2 points', block=0):\r
785         '''\r
786         Calculates the difference between two clicked points\r
787         '''\r
788         clicked_points = self._measure_N_points(N=2, message=message, block=block)\r
789 \r
790         plot = self.GetDisplayedPlotCorrected()\r
791         curve = plot.curves[block]\r
792 \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
800 \r
801         return delta\r
802 \r
803     def _measure_N_points(self, N, message='', block=0):\r
804         '''\r
805         General helper function for N-points measurements\r
806         By default, measurements are done on the retraction\r
807         '''\r
808         if message:\r
809             dialog = wx.MessageDialog(None, message, 'Info', wx.OK)\r
810             dialog.ShowModal()\r
811 \r
812         figure = self.GetActiveFigure()\r
813 \r
814         xvector = self.displayed_plot.curves[block].x\r
815         yvector = self.displayed_plot.curves[block].y\r
816 \r
817         clicked_points = figure.ginput(N, timeout=-1, show_clicks=True)\r
818 \r
819         points = []\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
823             point.dest = 0\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
830         return points\r
831 \r
832     def do_copylog(self):\r
833         '''\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
837         '''\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
854 \r
855     def do_plotmanipulators(self):\r
856         '''\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
859 \r
860         Click 'Execute' to apply your changes.\r
861         '''\r
862         self.UpdatePlot()\r
863 \r
864     def do_preferences(self):\r
865         '''\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
869         '''\r
870         pass\r
871 \r
872     def do_test(self):\r
873         '''\r
874         Use this command for testing purposes. You find do_test in hooke.py.\r
875         '''\r
876         pass\r
877 \r
878     def do_version(self):\r
879         '''\r
880         VERSION\r
881         ------\r
882         Prints the current version and codename, plus library version. Useful for debugging.\r
883         '''\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
899 \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
905 \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
911 \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
917                 self.UpdateNote()\r
918                 self.UpdatePlot()\r
919 \r
920     def UpdatePlot(self, plot=None):\r
921 \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
928                 if set_scale:\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
941                 if curve.legend:\r
942                     axes_list[destination].legend()\r
943 \r
944         if plot is None:\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
957                 return\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
965         else:\r
966             active_file = None\r
967             self.displayed_plot = copy.deepcopy(plot)\r
968 \r
969         figure = self.GetActiveFigure()\r
970         figure.clear()\r
971 \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
978         else:\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
982         axes_list =[]\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
987 \r
988         #add all curves to the corresponding plots\r
989         for curve in self.displayed_plot.curves:\r
990             add_to_plot(curve)\r
991 \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
994 \r
995         #display results\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
1001         else:\r
1002             self.panelResults.ClearResults()\r
1003         #refresh the plot\r
1004         figure.canvas.draw()\r
1005 \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
1015         self.UpdateNote()\r
1016         self.UpdatePlot()\r
1017 \r
1018 \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
1026         self.UpdateNote()\r
1027         self.UpdatePlot()\r
1028 \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
1035         \r
1036 \r
1037 class HookeApp (wx.App):\r
1038     def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):\r
1039         self.gui = gui\r
1040         self.commands = commands\r
1041         self.inqueue = inqueue\r
1042         self.outqueue = outqueue\r
1043         super(HookeApp, self).__init__(*args, **kwargs)\r
1044 \r
1045     def OnInit(self):\r
1046         self.SetAppName('Hooke')\r
1047         self.SetVendorName('')\r
1048         self._setup_splash_screen()\r
1049 \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
1054 \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
1058         # those.\r
1059         if left < -width:\r
1060             left = 0\r
1061         if top < -height:\r
1062             top = 0\r
1063 \r
1064         self._c = {\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
1069             }\r
1070         self._c['frame'].Show(True)\r
1071         self.SetTopWindow(self._c['frame'])\r
1072         return True\r
1073 \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
1079                 wx.SplashScreen(\r
1080                     bitmap=wx.Image(path).ConvertToBitmap(),\r
1081                     splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,\r
1082                     milliseconds=duration,\r
1083                     parent=None)\r
1084                 wx.Yield()\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
1090                 sleepFactor = 1.2\r
1091                 time.sleep(sleepFactor * duration / 1000)\r
1092 \r
1093     def OnExit(self):\r
1094         return True\r
1095 \r
1096 \r
1097 class GUI (UserInterface):\r
1098     """wxWindows graphical user interface.\r
1099     """\r
1100     def __init__(self):\r
1101         super(GUI, self).__init__(name='gui')\r
1102 \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
1106 \r
1107         The suggested section setting is::\r
1108 \r
1109             Setting(section=self.setting_section, help=self.__doc__)\r
1110         """\r
1111         return [\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
1117                     value=True,\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
1123                     value=1000,\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
1129                     value=False,\r
1130                     help='Hide file extensions when displaying names.'),\r
1131             Setting(section=self.setting_section, option='folders-workdir',\r
1132                     value='.',\r
1133                     help='This should probably go...'),\r
1134             Setting(section=self.setting_section, option='folders-filters',\r
1135                     value='.',\r
1136                     help='This should probably go...'),\r
1137             Setting(section=self.setting_section, option='active perspective',\r
1138                     value='Default',\r
1139                     help='Name of active perspective file (or "Default").'),\r
1140             Setting(section=self.setting_section, option='folders-filter-index',\r
1141                     value='0',\r
1142                     help='This should probably go...'),\r
1143             Setting(section=self.setting_section, option='main height',\r
1144                     value=500,\r
1145                     help='Height of main window in pixels.'),\r
1146             Setting(section=self.setting_section, option='main width',\r
1147                     value=500,\r
1148                     help='Width of main window in pixels.'),\r
1149             Setting(section=self.setting_section, option='main top',\r
1150                     value=0,\r
1151                     help='Pixels from screen top to top of main window.'),\r
1152             Setting(section=self.setting_section, option='main left',\r
1153                     value=0,\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
1158             ]\r
1159 \r
1160     def _app(self, commands, ui_to_command_queue, command_to_ui_queue):\r
1161         redirect = True\r
1162         if __debug__:\r
1163             redirect=False\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
1169         return app\r
1170 \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
1173         app.MainLoop()\r