Major rework of gui.panel.playlist.
[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                     parent=self,\r
247                     style=wx.WANTS_CHARS|wx.NO_BORDER,\r
248                     # WANTS_CHARS so the panel doesn't eat the Return key.\r
249                     size=(160, 200)), 'right'),\r
250             #('properties', panel.propertyeditor.PropertyEditor(self),'right'),\r
251             ('assistant', wx.TextCtrl(\r
252                     parent=self,\r
253                     pos=wx.Point(0, 0),\r
254                     size=wx.Size(150, 90),\r
255                     style=wx.NO_BORDER|wx.TE_MULTILINE), 'right'),\r
256             ('output', wx.TextCtrl(\r
257                     parent=self,\r
258                     pos=wx.Point(0, 0),\r
259                     size=wx.Size(150, 90),\r
260                     style=wx.NO_BORDER|wx.TE_MULTILINE), 'bottom'),\r
261             ('results', panel.results.Results(self), 'bottom'),\r
262             ]:\r
263             self._add_panel(label, p, style)\r
264         self._c['assistant'].SetEditable(False)\r
265 \r
266     def _add_panel(self, label, panel, style):\r
267         self._c[label] = panel\r
268         cap_label = label.capitalize()\r
269         info = aui.AuiPaneInfo().Name(cap_label).Caption(cap_label)\r
270         if style == 'left':\r
271             info.Left().CloseButton(True).MaximizeButton(False)\r
272         elif style == 'center':\r
273             info.CenterPane().PaneBorder(False)\r
274         elif style == 'right':\r
275             info.Right().CloseButton(True).MaximizeButton(False)\r
276         else:\r
277             assert style == 'bottom', style\r
278             info.Bottom().CloseButton(True).MaximizeButton(False)\r
279         self._c['manager'].AddPane(panel, info)\r
280 \r
281     def _setup_toolbars(self):\r
282         self._c['navbar'] = NavBar(self, style=wx.TB_FLAT | wx.TB_NODIVIDER)\r
283 \r
284         self._c['manager'].AddPane(\r
285             self._c['navbar'],\r
286             aui.AuiPaneInfo().Name('Navigation').Caption('Navigation'\r
287                 ).ToolbarPane().Top().Layer(1).Row(1).LeftDockable(False\r
288                 ).RightDockable(False))\r
289 \r
290     def _bind_events(self):\r
291         # TODO: figure out if we can use the eventManager for menu\r
292         # ranges and events of 'self' without raising an assertion\r
293         # fail error.\r
294         self.Bind(wx.EVT_ERASE_BACKGROUND, self._on_erase_background)\r
295         self.Bind(wx.EVT_SIZE, self._on_size)\r
296         self.Bind(wx.EVT_CLOSE, self._on_close)\r
297         self.Bind(wx.EVT_MENU, self._on_close, id=wx.ID_EXIT)\r
298         self.Bind(wx.EVT_MENU, self._on_about, id=wx.ID_ABOUT)\r
299         self.Bind(aui.EVT_AUI_PANE_CLOSE, self.OnPaneClose)\r
300         self.Bind(aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self._on_notebook_page_close)\r
301 \r
302         for value in self._c['menu bar']._c['view']._c.values():\r
303             self.Bind(wx.EVT_MENU_RANGE, self._on_view, value)\r
304 \r
305         self.Bind(wx.EVT_MENU, self._on_save_perspective,\r
306                   self._c['menu bar']._c['perspective']._c['save'])\r
307         self.Bind(wx.EVT_MENU, self._on_delete_perspective,\r
308                   self._c['menu bar']._c['perspective']._c['delete'])\r
309 \r
310         self.Bind(wx.EVT_TOOL, self._on_next, self._c['navbar']._c['next'])\r
311         self.Bind(wx.EVT_TOOL, self._on_previous,self._c['navbar']._c['previous'])\r
312 \r
313         treeCtrl = self._c['folders'].GetTreeCtrl()\r
314         treeCtrl.Bind(wx.EVT_LEFT_DCLICK, self._on_dir_ctrl_left_double_click)\r
315         \r
316         # TODO: playlist callbacks\r
317         return # TODO: cleanup\r
318         #commands tree\r
319         evtmgr.eventManager.Register(self.OnExecute, wx.EVT_BUTTON, self._c['commands'].ExecuteButton)\r
320         evtmgr.eventManager.Register(self.OnTreeCtrlCommandsSelectionChanged, wx.EVT_TREE_SEL_CHANGED, self._c['commands']._c['tree'])\r
321         evtmgr.eventManager.Register(self.OnTreeCtrlItemActivated, wx.EVT_TREE_ITEM_ACTIVATED, self._c['commands']._c['tree'])\r
322         evtmgr.eventManager.Register(self.OnUpdateNote, wx.EVT_BUTTON, self.panelNote.UpdateButton)\r
323         #property editor\r
324         self.panelProperties.pg.Bind(wxpg.EVT_PG_CHANGED, self.OnPropGridChanged)\r
325         #results panel\r
326         self.panelResults.results_list.OnCheckItem = self.OnResultsCheck\r
327 \r
328     def _GetActiveFileIndex(self):\r
329         lib.playlist.Playlist = self.GetActivePlaylist()\r
330         #get the selected item from the tree\r
331         selected_item = self._c['playlists']._c['tree'].GetSelection()\r
332         #test if a playlist or a curve was double-clicked\r
333         if self._c['playlists']._c['tree'].ItemHasChildren(selected_item):\r
334             return -1\r
335         else:\r
336             count = 0\r
337             selected_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item)\r
338             while selected_item.IsOk():\r
339                 count += 1\r
340                 selected_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item)\r
341             return count\r
342 \r
343     def _GetPlaylistTab(self, name):\r
344         for index, page in enumerate(self._c['notebook']._tabs._pages):\r
345             if page.caption == name:\r
346                 return index\r
347         return -1\r
348 \r
349     def _restore_perspective(self, name):\r
350         # TODO: cleanup\r
351         self.gui.config['active perspective'] = name  # TODO: push to engine's Hooke\r
352         self._c['manager'].LoadPerspective(self._perspectives[name])\r
353         self._c['manager'].Update()\r
354         for pane in self._c['manager'].GetAllPanes():\r
355             if pane.name in self._c['menu bar']._c['view']._c.keys():\r
356                 pane.Check(pane.window.IsShown())\r
357 \r
358     def _SavePerspectiveToFile(self, name, perspective):\r
359         filename = ''.join([name, '.txt'])\r
360         filename = lh.get_file_path(filename, ['perspective'])\r
361         perspectivesFile = open(filename, 'w')\r
362         perspectivesFile.write(perspective)\r
363         perspectivesFile.close()\r
364 \r
365     def AddPlaylistFromFiles(self, files=[], name='Untitled'):\r
366         if files:\r
367             playlist = lib.playlist.Playlist(self, self.drivers)\r
368             for item in files:\r
369                 playlist.add_curve(item)\r
370         if playlist.count > 0:\r
371             playlist.name = self._GetUniquePlaylistName(name)\r
372             playlist.reset()\r
373             self.AddTayliss(playlist)\r
374 \r
375     def AppendToOutput(self, text):\r
376         self.panelOutput.AppendText(''.join([text, '\n']))\r
377 \r
378     def AppliesPlotmanipulator(self, name):\r
379         '''\r
380         Returns True if the plotmanipulator 'name' is applied, False otherwise\r
381         name does not contain 'plotmanip_', just the name of the plotmanipulator (e.g. 'flatten')\r
382         '''\r
383         return self.GetBoolFromConfig('core', 'plotmanipulators', name)\r
384 \r
385     def ApplyPlotmanipulators(self, plot, plot_file):\r
386         '''\r
387         Apply all active plotmanipulators.\r
388         '''\r
389         if plot is not None and plot_file is not None:\r
390             manipulated_plot = copy.deepcopy(plot)\r
391             for plotmanipulator in self.plotmanipulators:\r
392                 if self.GetBoolFromConfig('core', 'plotmanipulators', plotmanipulator.name):\r
393                     manipulated_plot = plotmanipulator.method(manipulated_plot, plot_file)\r
394             return manipulated_plot\r
395 \r
396     def GetActiveFigure(self):\r
397         playlist_name = self.GetActivePlaylistName()\r
398         figure = self.playlists[playlist_name].figure\r
399         if figure is not None:\r
400             return figure\r
401         return None\r
402 \r
403     def GetActiveFile(self):\r
404         playlist = self.GetActivePlaylist()\r
405         if playlist is not None:\r
406             return playlist.get_active_file()\r
407         return None\r
408 \r
409     def GetActivePlot(self):\r
410         playlist = self.GetActivePlaylist()\r
411         if playlist is not None:\r
412             return playlist.get_active_file().plot\r
413         return None\r
414 \r
415     def GetDisplayedPlot(self):\r
416         plot = copy.deepcopy(self.displayed_plot)\r
417         #plot.curves = []\r
418         #plot.curves = copy.deepcopy(plot.curves)\r
419         return plot\r
420 \r
421     def GetDisplayedPlotCorrected(self):\r
422         plot = copy.deepcopy(self.displayed_plot)\r
423         plot.curves = []\r
424         plot.curves = copy.deepcopy(plot.corrected_curves)\r
425         return plot\r
426 \r
427     def GetDisplayedPlotRaw(self):\r
428         plot = copy.deepcopy(self.displayed_plot)\r
429         plot.curves = []\r
430         plot.curves = copy.deepcopy(plot.raw_curves)\r
431         return plot\r
432 \r
433     def GetDockArt(self):\r
434         return self._c['manager'].GetArtProvider()\r
435 \r
436     def GetPlotmanipulator(self, name):\r
437         '''\r
438         Returns a plot manipulator function from its name\r
439         '''\r
440         for plotmanipulator in self.plotmanipulators:\r
441             if plotmanipulator.name == name:\r
442                 return plotmanipulator\r
443         return None\r
444 \r
445     def GetPerspectiveMenuItem(self, name):\r
446         if self._perspectives.has_key(name):\r
447             perspectives_list = [key for key, value in self._perspectives.iteritems()]\r
448             perspectives_list.sort()\r
449             index = perspectives_list.index(name)\r
450             perspective_Id = ID_FirstPerspective + index\r
451             menu_item = self.MenuBar.FindItemById(perspective_Id)\r
452             return menu_item\r
453         else:\r
454             return None\r
455 \r
456     def HasPlotmanipulator(self, name):\r
457         '''\r
458         returns True if the plotmanipulator 'name' is loaded, False otherwise\r
459         '''\r
460         for plotmanipulator in self.plotmanipulators:\r
461             if plotmanipulator.command == name:\r
462                 return True\r
463         return False\r
464 \r
465     def _on_about(self, event):\r
466         message = 'Hooke\n\n'+\\r
467             'A free, open source data analysis platform\n\n'+\\r
468             'Copyright 2006-2008 by Massimo Sandal\n'+\\r
469             'Copyright 2010 by Dr. Rolf Schmidt\n\n'+\\r
470             'Hooke is released under the GNU General Public License version 2.'\r
471         dialog = wx.MessageDialog(self, message, 'About Hooke', wx.OK | wx.ICON_INFORMATION)\r
472         dialog.ShowModal()\r
473         dialog.Destroy()\r
474 \r
475     def _on_close(self, event):\r
476         # apply changes\r
477         self.gui.config['main height'] = str(self.GetSize().GetHeight())\r
478         self.gui.config['main left'] = str(self.GetPosition()[0])\r
479         self.gui.config['main top'] = str(self.GetPosition()[1])\r
480         self.gui.config['main width'] = str(self.GetSize().GetWidth())\r
481         # push changes back to Hooke.config?\r
482         self._c['manager'].UnInit()\r
483         del self._c['manager']\r
484         self.Destroy()\r
485 \r
486     def _update_perspectives(self):\r
487         """Add perspectives to menubar and _perspectives.\r
488         """\r
489         self._perspectives = {\r
490             'Default': self._c['manager'].SavePerspective(),\r
491             }\r
492         path = self.gui.config['perspective path']\r
493         if os.path.isdir(path):\r
494             files = sorted(os.listdir(path))\r
495             for fname in files:\r
496                 name, extension = os.path.splitext(fname)\r
497                 if extension != '.txt':\r
498                     continue\r
499                 fpath = os.path.join(path, fpath)\r
500                 if not os.path.isfile(fpath):\r
501                     continue\r
502                 perspective = None\r
503                 with open(fpath, 'rU') as f:\r
504                     perspective = f.readline()\r
505                 if perspective:\r
506                     self._perspectives[name] = perspective\r
507 \r
508         selected_perspective = self.gui.config['active perspective']\r
509         if not self._perspectives.has_key(selected_perspective):\r
510             self.gui.config['active perspective'] = 'Default'  # TODO: push to engine's Hooke\r
511 \r
512         self._update_perspective_menu()\r
513         self._restore_perspective(selected_perspective)\r
514 \r
515     def _update_perspective_menu(self):\r
516         self._c['menu bar']._c['perspective'].update(\r
517             sorted(self._perspectives.keys()),\r
518             self.gui.config['active perspective'],\r
519             self._on_restore_perspective)\r
520 \r
521     def _on_restore_perspective(self, event):\r
522         name = self.MenuBar.FindItemById(event.GetId()).GetLabel()\r
523         self._restore_perspective(name)\r
524 \r
525     def _on_save_perspective(self, event):\r
526         def nameExists(name):\r
527             menu_position = self.MenuBar.FindMenu('Perspective')\r
528             menu = self.MenuBar.GetMenu(menu_position)\r
529             for item in menu.GetMenuItems():\r
530                 if item.GetText() == name:\r
531                     return True\r
532             return False\r
533 \r
534         done = False\r
535         while not done:\r
536             dialog = wx.TextEntryDialog(self, 'Enter a name for the new perspective:', 'Save perspective')\r
537             dialog.SetValue('New perspective')\r
538             if dialog.ShowModal() != wx.ID_OK:\r
539                 return\r
540             else:\r
541                 name = dialog.GetValue()\r
542 \r
543             if nameExists(name):\r
544                 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
545                 if dialogConfirm.ShowModal() == wx.ID_YES:\r
546                     done = True\r
547             else:\r
548                 done = True\r
549 \r
550         perspective = self._c['manager'].SavePerspective()\r
551         self._SavePerspectiveToFile(name, perspective)\r
552         self.gui.config['active perspectives'] = name\r
553         self._update_perspective_menu()\r
554 #        if nameExists(name):\r
555 #            #check the corresponding menu item\r
556 #            menu_item = self.GetPerspectiveMenuItem(name)\r
557 #            #replace the perspectiveStr in _pespectives\r
558 #            self._perspectives[name] = perspective\r
559 #        else:\r
560 #            #because we deal with radio items, we need to do some extra work\r
561 #            #delete all menu items from the perspectives menu\r
562 #            for item in self._perspectives_menu.GetMenuItems():\r
563 #                self._perspectives_menu.DeleteItem(item)\r
564 #            #recreate the perspectives menu\r
565 #            self._perspectives_menu.Append(ID_SavePerspective, 'Save Perspective')\r
566 #            self._perspectives_menu.Append(ID_DeletePerspective, 'Delete Perspective')\r
567 #            self._perspectives_menu.AppendSeparator()\r
568 #            #convert the perspectives dictionary into a list\r
569 #            # the list contains:\r
570 #            #[0]: name of the perspective\r
571 #            #[1]: perspective\r
572 #            perspectives_list = [key for key, value in self._perspectives.iteritems()]\r
573 #            perspectives_list.append(name)\r
574 #            perspectives_list.sort()\r
575 #            #add all previous perspectives\r
576 #            for index, item in enumerate(perspectives_list):\r
577 #                menu_item = self._perspectives_menu.AppendRadioItem(ID_FirstPerspective + index, item)\r
578 #                if item == name:\r
579 #                    menu_item.Check()\r
580 #            #add the new perspective to _perspectives\r
581 #            self._perspectives[name] = perspective\r
582 \r
583     def _on_delete_perspective(self, event):\r
584         dialog = panel.selection.Selection(\r
585             options=sorted(os.listdir(self.gui.config['perspective path'])),\r
586             message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",\r
587             button_id=wx.ID_DELETE,\r
588             button_callback=self._on_delete_perspective,\r
589             parent=self,\r
590             label='Delete perspective(s)',\r
591             style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)\r
592         dialog.CenterOnScreen()\r
593         dialog.ShowModal()\r
594         dialog.Destroy()\r
595         self._update_perspective_menu()\r
596         # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258\r
597         #   http://trac.wxwidgets.org/ticket/3258 \r
598         # ) that makes the radio item indicator in the menu disappear.\r
599         # The code should be fine once this issue is fixed.\r
600 \r
601     def _on_delete_perspective(self, event, items, selected_items):\r
602         for item in selected_items:\r
603             self._perspectives.remove(item)\r
604             if item == self.gui.config['active perspective']:\r
605                 self.gui.config['active perspective'] = 'Default'\r
606             path = os.path.join(self.gui.config['perspective path'],\r
607                                 item+'.txt')\r
608             remove(path)\r
609         self._update_perspective_menu()\r
610 \r
611     def _on_dir_ctrl_left_double_click(self, event):\r
612         file_path = self.panelFolders.GetPath()\r
613         if os.path.isfile(file_path):\r
614             if file_path.endswith('.hkp'):\r
615                 self.do_loadlist(file_path)\r
616         event.Skip()\r
617 \r
618     def _on_erase_background(self, event):\r
619         event.Skip()\r
620 \r
621     def OnExecute(self, event):\r
622         item = self._c['commands']._c['tree'].GetSelection()\r
623         if item.IsOk():\r
624             if not self._c['commands']._c['tree'].ItemHasChildren(item):\r
625                 item_text = self._c['commands']._c['tree'].GetItemText(item)\r
626                 command = ''.join(['self.do_', item_text, '()'])\r
627                 #self.AppendToOutput(command + '\n')\r
628                 exec(command)\r
629 \r
630     def OnExit(self, event):\r
631         self.Close()\r
632 \r
633     def _on_next(self, event):\r
634         '''\r
635         NEXT\r
636         Go to the next curve in the playlist.\r
637         If we are at the last curve, we come back to the first.\r
638         -----\r
639         Syntax: next, n\r
640         '''\r
641         selected_item = self._c['playlists']._c['tree'].GetSelection()\r
642         if self._c['playlists']._c['tree'].ItemHasChildren(selected_item):\r
643             #GetFirstChild returns a tuple\r
644             #we only need the first element\r
645             next_item = self._c['playlists']._c['tree'].GetFirstChild(selected_item)[0]\r
646         else:\r
647             next_item = self._c['playlists']._c['tree'].GetNextSibling(selected_item)\r
648             if not next_item.IsOk():\r
649                 parent_item = self._c['playlists']._c['tree'].GetItemParent(selected_item)\r
650                 #GetFirstChild returns a tuple\r
651                 #we only need the first element\r
652                 next_item = self._c['playlists']._c['tree'].GetFirstChild(parent_item)[0]\r
653         self._c['playlists']._c['tree'].SelectItem(next_item, True)\r
654         if not self._c['playlists']._c['tree'].ItemHasChildren(selected_item):\r
655             playlist = self.GetActivePlaylist()\r
656             if playlist.count > 1:\r
657                 playlist.next()\r
658                 self.statusbar.SetStatusText(playlist.get_status_string(), 0)\r
659                 self.UpdateNote()\r
660                 self.UpdatePlot()\r
661 \r
662     def _on_notebook_page_close(self, event):\r
663         ctrl = event.GetEventObject()\r
664         playlist_name = ctrl.GetPageText(ctrl._curpage)\r
665         self.DeleteFromPlaylists(playlist_name)\r
666 \r
667     def OnPaneClose(self, event):\r
668         event.Skip()\r
669 \r
670     def _on_previous(self, event):\r
671         '''\r
672         PREVIOUS\r
673         Go to the previous curve in the playlist.\r
674         If we are at the first curve, we jump to the last.\r
675         -------\r
676         Syntax: previous, p\r
677         '''\r
678         #playlist = self.playlists[self.GetActivePlaylistName()][0]\r
679         #select the previous curve and tell the user if we wrapped around\r
680         #self.AppendToOutput(playlist.previous())\r
681         selected_item = self._c['playlists']._c['tree'].GetSelection()\r
682         if self._c['playlists']._c['tree'].ItemHasChildren(selected_item):\r
683             previous_item = self._c['playlists']._c['tree'].GetLastChild(selected_item)\r
684         else:\r
685             previous_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item)\r
686             if not previous_item.IsOk():\r
687                 parent_item = self._c['playlists']._c['tree'].GetItemParent(selected_item)\r
688                 previous_item = self._c['playlists']._c['tree'].GetLastChild(parent_item)\r
689         self._c['playlists']._c['tree'].SelectItem(previous_item, True)\r
690         playlist = self.GetActivePlaylist()\r
691         if playlist.count > 1:\r
692             playlist.previous()\r
693             self.statusbar.SetStatusText(playlist.get_status_string(), 0)\r
694             self.UpdateNote()\r
695             self.UpdatePlot()\r
696 \r
697     def OnPropGridChanged (self, event):\r
698         prop = event.GetProperty()\r
699         if prop:\r
700             item_section = self.panelProperties.SelectedTreeItem\r
701             item_plugin = self._c['commands']._c['tree'].GetItemParent(item_section)\r
702             plugin = self._c['commands']._c['tree'].GetItemText(item_plugin)\r
703             config = self.gui.config[plugin]\r
704             property_section = self._c['commands']._c['tree'].GetItemText(item_section)\r
705             property_key = prop.GetName()\r
706             property_value = prop.GetDisplayedString()\r
707 \r
708             config[property_section][property_key]['value'] = property_value\r
709 \r
710     def OnResultsCheck(self, index, flag):\r
711         results = self.GetActivePlot().results\r
712         if results.has_key(self.results_str):\r
713             results[self.results_str].results[index].visible = flag\r
714             results[self.results_str].update()\r
715             self.UpdatePlot()\r
716 \r
717 \r
718     def _on_size(self, event):\r
719         event.Skip()\r
720 \r
721     def OnTreeCtrlCommandsSelectionChanged(self, event):\r
722         selected_item = event.GetItem()\r
723         if selected_item is not None:\r
724             plugin = ''\r
725             section = ''\r
726             #deregister/register the listener to avoid infinite loop\r
727             evtmgr.eventManager.DeregisterListener(self.OnTreeCtrlCommandsSelectionChanged)\r
728             self._c['commands']._c['tree'].SelectItem(selected_item)\r
729             evtmgr.eventManager.Register(self.OnTreeCtrlCommandsSelectionChanged, wx.EVT_TREE_SEL_CHANGED, self._c['commands']._c['tree'])\r
730             self.panelProperties.SelectedTreeItem = selected_item\r
731             #if a command was clicked\r
732             properties = []\r
733             if not self._c['commands']._c['tree'].ItemHasChildren(selected_item):\r
734                 item_plugin = self._c['commands']._c['tree'].GetItemParent(selected_item)\r
735                 plugin = self._c['commands']._c['tree'].GetItemText(item_plugin)\r
736                 if self.gui.config.has_key(plugin):\r
737                     #config = self._c['commands']._c['tree'].GetPyData(item_plugin)\r
738                     config = self.gui.config[plugin]\r
739                     section = self._c['commands']._c['tree'].GetItemText(selected_item)\r
740                     #display docstring in help window\r
741                     doc_string = eval('self.do_' + section + '.__doc__')\r
742                     if section in config:\r
743                         for option in config[section]:\r
744                             properties.append([option, config[section][option]])\r
745             else:\r
746                 plugin = self._c['commands']._c['tree'].GetItemText(selected_item)\r
747                 if plugin != 'core':\r
748                     doc_string = eval('plugins.' + plugin + '.' + plugin + 'Commands.__doc__')\r
749                 else:\r
750                     doc_string = 'The module "core" contains Hooke core functionality'\r
751             if doc_string is not None:\r
752                 self.panelAssistant.ChangeValue(doc_string)\r
753             else:\r
754                 self.panelAssistant.ChangeValue('')\r
755             panel.propertyeditor.PropertyEditor.Initialize(self.panelProperties, properties)\r
756             #save the currently selected command/plugin to the config file\r
757             self.gui.config['command']['command'] = section\r
758             self.gui.config['command']['plugin'] = plugin\r
759 \r
760     def OnTreeCtrlItemActivated(self, event):\r
761         self.OnExecute(event)\r
762 \r
763     def OnUpdateNote(self, event):\r
764         '''\r
765         Saves the note to the active file.\r
766         '''\r
767         active_file = self.GetActiveFile()\r
768         active_file.note = self.panelNote.Editor.GetValue()\r
769 \r
770     def _on_view(self, event):\r
771         menu_id = event.GetId()\r
772         menu_item = self.MenuBar.FindItemById(menu_id)\r
773         menu_label = menu_item.GetLabel()\r
774 \r
775         pane = self._c['manager'].GetPane(menu_label)\r
776         pane.Show(not pane.IsShown())\r
777         #if we don't do the following, the Folders pane does not resize properly on hide/show\r
778         if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():\r
779             #folders_size = pane.GetSize()\r
780             self.panelFolders.Fit()\r
781         self._c['manager'].Update()\r
782 \r
783     def _clickize(self, xvector, yvector, index):\r
784         '''\r
785         Returns a ClickedPoint() object from an index and vectors of x, y coordinates\r
786         '''\r
787         point = lib.clickedpoint.ClickedPoint()\r
788         point.index = index\r
789         point.absolute_coords = xvector[index], yvector[index]\r
790         point.find_graph_coords(xvector, yvector)\r
791         return point\r
792 \r
793     def _delta(self, message='Click 2 points', block=0):\r
794         '''\r
795         Calculates the difference between two clicked points\r
796         '''\r
797         clicked_points = self._measure_N_points(N=2, message=message, block=block)\r
798 \r
799         plot = self.GetDisplayedPlotCorrected()\r
800         curve = plot.curves[block]\r
801 \r
802         delta = lib.delta.Delta()\r
803         delta.point1.x = clicked_points[0].graph_coords[0]\r
804         delta.point1.y = clicked_points[0].graph_coords[1]\r
805         delta.point2.x = clicked_points[1].graph_coords[0]\r
806         delta.point2.y = clicked_points[1].graph_coords[1]\r
807         delta.units.x = curve.units.x\r
808         delta.units.y = curve.units.y\r
809 \r
810         return delta\r
811 \r
812     def _measure_N_points(self, N, message='', block=0):\r
813         '''\r
814         General helper function for N-points measurements\r
815         By default, measurements are done on the retraction\r
816         '''\r
817         if message:\r
818             dialog = wx.MessageDialog(None, message, 'Info', wx.OK)\r
819             dialog.ShowModal()\r
820 \r
821         figure = self.GetActiveFigure()\r
822 \r
823         xvector = self.displayed_plot.curves[block].x\r
824         yvector = self.displayed_plot.curves[block].y\r
825 \r
826         clicked_points = figure.ginput(N, timeout=-1, show_clicks=True)\r
827 \r
828         points = []\r
829         for clicked_point in clicked_points:\r
830             point = lib.clickedpoint.ClickedPoint()\r
831             point.absolute_coords = clicked_point[0], clicked_point[1]\r
832             point.dest = 0\r
833             #TODO: make this optional?\r
834             #so far, the clicked point is taken, not the corresponding data point\r
835             point.find_graph_coords(xvector, yvector)\r
836             point.is_line_edge = True\r
837             point.is_marker = True\r
838             points.append(point)\r
839         return points\r
840 \r
841     def do_copylog(self):\r
842         '''\r
843         Copies all files in the current playlist that have a note to the destination folder.\r
844         destination: select folder where you want the files to be copied\r
845         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
846         '''\r
847         playlist = self.GetActivePlaylist()\r
848         if playlist is not None:\r
849             destination = self.GetStringFromConfig('core', 'copylog', 'destination')\r
850             if not os.path.isdir(destination):\r
851                 os.makedirs(destination)\r
852             for current_file in playlist.files:\r
853                 if current_file.note:\r
854                     shutil.copy(current_file.filename, destination)\r
855                     if current_file.driver.filetype == 'mfp1d':\r
856                         filename = current_file.filename.replace('deflection', 'LVDT', 1)\r
857                         path, name = os.path.split(filename)\r
858                         filename = os.path.join(path, 'lvdt', name)\r
859                         use_LVDT_folder = self.GetBoolFromConfig('core', 'copylog', 'use_LVDT_folder')\r
860                         if use_LVDT_folder:\r
861                             destination = os.path.join(destination, 'LVDT')\r
862                         shutil.copy(filename, destination)\r
863 \r
864     def do_plotmanipulators(self):\r
865         '''\r
866         Please select the plotmanipulators you would like to use\r
867         and define the order in which they will be applied to the data.\r
868 \r
869         Click 'Execute' to apply your changes.\r
870         '''\r
871         self.UpdatePlot()\r
872 \r
873     def do_preferences(self):\r
874         '''\r
875         Please set general preferences for Hooke here.\r
876         hide_curve_extension: hides the extension of the force curve files.\r
877                               not recommended for 'picoforce' files\r
878         '''\r
879         pass\r
880 \r
881     def do_test(self):\r
882         '''\r
883         Use this command for testing purposes. You find do_test in hooke.py.\r
884         '''\r
885         pass\r
886 \r
887     def do_version(self):\r
888         '''\r
889         VERSION\r
890         ------\r
891         Prints the current version and codename, plus library version. Useful for debugging.\r
892         '''\r
893         self.AppendToOutput('Hooke ' + __version__ + ' (' + __codename__ + ')')\r
894         self.AppendToOutput('Released on: ' + __releasedate__)\r
895         self.AppendToOutput('---')\r
896         self.AppendToOutput('Python version: ' + python_version)\r
897         self.AppendToOutput('WxPython version: ' + wx_version)\r
898         self.AppendToOutput('Matplotlib version: ' + mpl_version)\r
899         self.AppendToOutput('SciPy version: ' + scipy_version)\r
900         self.AppendToOutput('NumPy version: ' + numpy_version)\r
901         self.AppendToOutput('ConfigObj version: ' + configobj_version)\r
902         self.AppendToOutput('wxPropertyGrid version: ' + '.'.join([str(PROPGRID_MAJOR), str(PROPGRID_MINOR), str(PROPGRID_RELEASE)]))\r
903         self.AppendToOutput('---')\r
904         self.AppendToOutput('Platform: ' + str(platform.uname()))\r
905         self.AppendToOutput('******************************')\r
906         self.AppendToOutput('Loaded plugins')\r
907         self.AppendToOutput('---')\r
908 \r
909         #sort the plugins into alphabetical order\r
910         plugins_list = [key for key, value in self.plugins.iteritems()]\r
911         plugins_list.sort()\r
912         for plugin in plugins_list:\r
913             self.AppendToOutput(plugin)\r
914 \r
915     def UpdateNote(self):\r
916         #update the note for the active file\r
917         active_file = self.GetActiveFile()\r
918         if active_file is not None:\r
919             self.panelNote.Editor.SetValue(active_file.note)\r
920 \r
921     def UpdatePlaylistsTreeSelection(self):\r
922         playlist = self.GetActivePlaylist()\r
923         if playlist is not None:\r
924             if playlist.index >= 0:\r
925                 self.statusbar.SetStatusText(playlist.get_status_string(), 0)\r
926                 self.UpdateNote()\r
927                 self.UpdatePlot()\r
928 \r
929     def UpdatePlot(self, plot=None):\r
930 \r
931         def add_to_plot(curve, set_scale=True):\r
932             if curve.visible and curve.x and curve.y:\r
933                 #get the index of the subplot to use as destination\r
934                 destination = (curve.destination.column - 1) * number_of_rows + curve.destination.row - 1\r
935                 #set all parameters for the plot\r
936                 axes_list[destination].set_title(curve.title)\r
937                 if set_scale:\r
938                     axes_list[destination].set_xlabel(curve.prefix.x + curve.units.x)\r
939                     axes_list[destination].set_ylabel(curve.prefix.y + curve.units.y)\r
940                     #set the formatting details for the scale\r
941                     formatter_x = lib.curve.PrefixFormatter(curve.decimals.x, curve.prefix.x, use_zero)\r
942                     formatter_y = lib.curve.PrefixFormatter(curve.decimals.y, curve.prefix.y, use_zero)\r
943                     axes_list[destination].xaxis.set_major_formatter(formatter_x)\r
944                     axes_list[destination].yaxis.set_major_formatter(formatter_y)\r
945                 if curve.style == 'plot':\r
946                     axes_list[destination].plot(curve.x, curve.y, color=curve.color, label=curve.label, lw=curve.linewidth, zorder=1)\r
947                 if curve.style == 'scatter':\r
948                     axes_list[destination].scatter(curve.x, curve.y, color=curve.color, label=curve.label, s=curve.size, zorder=2)\r
949                 #add the legend if necessary\r
950                 if curve.legend:\r
951                     axes_list[destination].legend()\r
952 \r
953         if plot is None:\r
954             active_file = self.GetActiveFile()\r
955             if not active_file.driver:\r
956                 #the first time we identify a file, the following need to be set\r
957                 active_file.identify(self.drivers)\r
958                 for curve in active_file.plot.curves:\r
959                     curve.decimals.x = self.GetIntFromConfig('core', 'preferences', 'x_decimals')\r
960                     curve.decimals.y = self.GetIntFromConfig('core', 'preferences', 'y_decimals')\r
961                     curve.legend = self.GetBoolFromConfig('core', 'preferences', 'legend')\r
962                     curve.prefix.x = self.GetStringFromConfig('core', 'preferences', 'x_prefix')\r
963                     curve.prefix.y = self.GetStringFromConfig('core', 'preferences', 'y_prefix')\r
964             if active_file.driver is None:\r
965                 self.AppendToOutput('Invalid file: ' + active_file.filename)\r
966                 return\r
967             self.displayed_plot = copy.deepcopy(active_file.plot)\r
968             #add raw curves to plot\r
969             self.displayed_plot.raw_curves = copy.deepcopy(self.displayed_plot.curves)\r
970             #apply all active plotmanipulators\r
971             self.displayed_plot = self.ApplyPlotmanipulators(self.displayed_plot, active_file)\r
972             #add corrected curves to plot\r
973             self.displayed_plot.corrected_curves = copy.deepcopy(self.displayed_plot.curves)\r
974         else:\r
975             active_file = None\r
976             self.displayed_plot = copy.deepcopy(plot)\r
977 \r
978         figure = self.GetActiveFigure()\r
979         figure.clear()\r
980 \r
981         #use '0' instead of e.g. '0.00' for scales\r
982         use_zero = self.GetBoolFromConfig('core', 'preferences', 'use_zero')\r
983         #optionally remove the extension from the title of the plot\r
984         hide_curve_extension = self.GetBoolFromConfig('core', 'preferences', 'hide_curve_extension')\r
985         if hide_curve_extension:\r
986             title = lh.remove_extension(self.displayed_plot.title)\r
987         else:\r
988             title = self.displayed_plot.title\r
989         figure.suptitle(title, fontsize=14)\r
990         #create the list of all axes necessary (rows and columns)\r
991         axes_list =[]\r
992         number_of_columns = max([curve.destination.column for curve in self.displayed_plot.curves])\r
993         number_of_rows = max([curve.destination.row for curve in self.displayed_plot.curves])\r
994         for index in range(number_of_rows * number_of_columns):\r
995             axes_list.append(figure.add_subplot(number_of_rows, number_of_columns, index + 1))\r
996 \r
997         #add all curves to the corresponding plots\r
998         for curve in self.displayed_plot.curves:\r
999             add_to_plot(curve)\r
1000 \r
1001         #make sure the titles of 'subplots' do not overlap with the axis labels of the 'main plot'\r
1002         figure.subplots_adjust(hspace=0.3)\r
1003 \r
1004         #display results\r
1005         self.panelResults.ClearResults()\r
1006         if self.displayed_plot.results.has_key(self.results_str):\r
1007             for curve in self.displayed_plot.results[self.results_str].results:\r
1008                 add_to_plot(curve, set_scale=False)\r
1009             self.panelResults.DisplayResults(self.displayed_plot.results[self.results_str])\r
1010         else:\r
1011             self.panelResults.ClearResults()\r
1012         #refresh the plot\r
1013         figure.canvas.draw()\r
1014 \r
1015     def _on_curve_select(self, playlist, curve):\r
1016         #create the plot tab and add playlist to the dictionary\r
1017         plotPanel = panel.plot.PlotPanel(self, ID_FirstPlot + len(self.playlists))\r
1018         notebook_tab = self._c['notebook'].AddPage(plotPanel, playlist.name, True)\r
1019         #tab_index = self._c['notebook'].GetSelection()\r
1020         playlist.figure = plotPanel.get_figure()\r
1021         self.playlists[playlist.name] = playlist\r
1022         #self.playlists[playlist.name] = [playlist, figure]\r
1023         self.statusbar.SetStatusText(playlist.get_status_string(), 0)\r
1024         self.UpdateNote()\r
1025         self.UpdatePlot()\r
1026 \r
1027 \r
1028     def _on_playlist_left_doubleclick(self):\r
1029         index = self._c['notebook'].GetSelection()\r
1030         current_playlist = self._c['notebook'].GetPageText(index)\r
1031         if current_playlist != playlist_name:\r
1032             index = self._GetPlaylistTab(playlist_name)\r
1033             self._c['notebook'].SetSelection(index)\r
1034         self.statusbar.SetStatusText(playlist.get_status_string(), 0)\r
1035         self.UpdateNote()\r
1036         self.UpdatePlot()\r
1037 \r
1038     def _on_playlist_delete(self, playlist):\r
1039         notebook = self.Parent.plotNotebook\r
1040         index = self.Parent._GetPlaylistTab(playlist.name)\r
1041         notebook.SetSelection(index)\r
1042         notebook.DeletePage(notebook.GetSelection())\r
1043         self.Parent.DeleteFromPlaylists(playlist_name)\r
1044         \r
1045 \r
1046 class HookeApp (wx.App):\r
1047     def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):\r
1048         self.gui = gui\r
1049         self.commands = commands\r
1050         self.inqueue = inqueue\r
1051         self.outqueue = outqueue\r
1052         super(HookeApp, self).__init__(*args, **kwargs)\r
1053 \r
1054     def OnInit(self):\r
1055         self.SetAppName('Hooke')\r
1056         self.SetVendorName('')\r
1057         self._setup_splash_screen()\r
1058 \r
1059         height = int(self.gui.config['main height']) # HACK: config should convert\r
1060         width = int(self.gui.config['main width'])\r
1061         top = int(self.gui.config['main top'])\r
1062         left = int(self.gui.config['main left'])\r
1063 \r
1064         # Sometimes, the ini file gets confused and sets 'left' and\r
1065         # 'top' to large negative numbers.  Here we catch and fix\r
1066         # this.  Keep small negative numbers, the user might want\r
1067         # those.\r
1068         if left < -width:\r
1069             left = 0\r
1070         if top < -height:\r
1071             top = 0\r
1072 \r
1073         self._c = {\r
1074             'frame': HookeFrame(\r
1075                 self.gui, self.commands, parent=None, title='Hooke',\r
1076                 pos=(left, top), size=(width, height),\r
1077                 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),\r
1078             }\r
1079         self._c['frame'].Show(True)\r
1080         self.SetTopWindow(self._c['frame'])\r
1081         return True\r
1082 \r
1083     def _setup_splash_screen(self):\r
1084         if self.gui.config['show splash screen']:\r
1085             path = self.gui.config['splash screen image']\r
1086             if os.path.isfile(path):\r
1087                 duration = int(self.gui.config['splash screen duration'])  # HACK: config should decode types\r
1088                 wx.SplashScreen(\r
1089                     bitmap=wx.Image(path).ConvertToBitmap(),\r
1090                     splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,\r
1091                     milliseconds=duration,\r
1092                     parent=None)\r
1093                 wx.Yield()\r
1094                 # For some reason splashDuration and sleep do not\r
1095                 # correspond to each other at least not on Windows.\r
1096                 # Maybe it's because duration is in milliseconds and\r
1097                 # sleep in seconds.  Thus we need to increase the\r
1098                 # sleep time a bit. A factor of 1.2 seems to work.\r
1099                 sleepFactor = 1.2\r
1100                 time.sleep(sleepFactor * duration / 1000)\r
1101 \r
1102     def OnExit(self):\r
1103         return True\r
1104 \r
1105 \r
1106 class GUI (UserInterface):\r
1107     """wxWindows graphical user interface.\r
1108     """\r
1109     def __init__(self):\r
1110         super(GUI, self).__init__(name='gui')\r
1111 \r
1112     def default_settings(self):\r
1113         """Return a list of :class:`hooke.config.Setting`\s for any\r
1114         configurable UI settings.\r
1115 \r
1116         The suggested section setting is::\r
1117 \r
1118             Setting(section=self.setting_section, help=self.__doc__)\r
1119         """\r
1120         return [\r
1121             Setting(section=self.setting_section, help=self.__doc__),\r
1122             Setting(section=self.setting_section, option='icon image',\r
1123                     value=os.path.join('doc', 'img', 'microscope.ico'),\r
1124                     help='Path to the hooke icon image.'),\r
1125             Setting(section=self.setting_section, option='show splash screen',\r
1126                     value=True,\r
1127                     help='Enable/disable the splash screen'),\r
1128             Setting(section=self.setting_section, option='splash screen image',\r
1129                     value=os.path.join('doc', 'img', 'hooke.jpg'),\r
1130                     help='Path to the Hooke splash screen image.'),\r
1131             Setting(section=self.setting_section, option='splash screen duration',\r
1132                     value=1000,\r
1133                     help='Duration of the splash screen in milliseconds.'),\r
1134             Setting(section=self.setting_section, option='perspective path',\r
1135                     value=os.path.join('resources', 'gui', 'perspective'),\r
1136                     help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.\r
1137             Setting(section=self.setting_section, option='hide extensions',\r
1138                     value=False,\r
1139                     help='Hide file extensions when displaying names.'),\r
1140             Setting(section=self.setting_section, option='folders-workdir',\r
1141                     value='.',\r
1142                     help='This should probably go...'),\r
1143             Setting(section=self.setting_section, option='folders-filters',\r
1144                     value='.',\r
1145                     help='This should probably go...'),\r
1146             Setting(section=self.setting_section, option='active perspective',\r
1147                     value='Default',\r
1148                     help='Name of active perspective file (or "Default").'),\r
1149             Setting(section=self.setting_section, option='folders-filter-index',\r
1150                     value='0',\r
1151                     help='This should probably go...'),\r
1152             Setting(section=self.setting_section, option='main height',\r
1153                     value=500,\r
1154                     help='Height of main window in pixels.'),\r
1155             Setting(section=self.setting_section, option='main width',\r
1156                     value=500,\r
1157                     help='Width of main window in pixels.'),\r
1158             Setting(section=self.setting_section, option='main top',\r
1159                     value=0,\r
1160                     help='Pixels from screen top to top of main window.'),\r
1161             Setting(section=self.setting_section, option='main left',\r
1162                     value=0,\r
1163                     help='Pixels from screen left to left of main window.'),            \r
1164             Setting(section=self.setting_section, option='selected command',\r
1165                     value='load playlist',\r
1166                     help='Name of the initially selected command.'),\r
1167             ]\r
1168 \r
1169     def _app(self, commands, ui_to_command_queue, command_to_ui_queue):\r
1170         redirect = True\r
1171         if __debug__:\r
1172             redirect=False\r
1173         app = HookeApp(gui=self,\r
1174                        commands=commands,\r
1175                        inqueue=ui_to_command_queue,\r
1176                        outqueue=command_to_ui_queue,\r
1177                        redirect=redirect)\r
1178         return app\r
1179 \r
1180     def run(self, commands, ui_to_command_queue, command_to_ui_queue):\r
1181         app = self._app(commands, ui_to_command_queue, command_to_ui_queue)\r
1182         app.MainLoop()\r