From: W. Trevor King Date: Tue, 27 Jul 2010 20:35:43 +0000 (-0400) Subject: Got 'Perspectives' menu working X-Git-Url: http://git.tremily.us/?a=commitdiff_plain;h=a61f53eeae3801beda2073ff395f52c79330b69a;p=hooke.git Got 'Perspectives' menu working --- diff --git a/hooke/ui/gui/__init__.py b/hooke/ui/gui/__init__.py index e9e45f5..1a6492e 100644 --- a/hooke/ui/gui/__init__.py +++ b/hooke/ui/gui/__init__.py @@ -9,6 +9,7 @@ import wxversion wxversion.select(WX_GOOD) import copy +import os import os.path import platform import shutil @@ -30,6 +31,8 @@ from ...command import CommandExit, Exit, Success, Failure, Command, Argument from ...config import Setting from ...interaction import Request, BooleanRequest, ReloadUserInterfaceConfig from ...ui import UserInterface, CommandMessage +from .dialog.selection import Selection as SelectionDialog +from .dialog.save_file import select_save_file from . import menu as menu from . import navbar as navbar from . import panel as panel @@ -82,6 +85,7 @@ class HookeFrame (wx.Frame): 'view_panel': self._on_panel_visibility, 'save_perspective': self._on_save_perspective, 'delete_perspective': self._on_delete_perspective, + 'select_perspective': self._on_select_perspective, }) self.SetMenuBar(self._c['menu bar']) @@ -90,7 +94,7 @@ class HookeFrame (wx.Frame): style=wx.ST_SIZEGRIP) self.SetStatusBar(self._c['status bar']) - self._update_perspectives() + self._setup_perspectives() self._bind_events() name = self.gui.config['active perspective'] @@ -303,22 +307,6 @@ class HookeFrame (wx.Frame): return index return -1 - def _restore_perspective(self, name): - # TODO: cleanup - self.gui.config['active perspective'] = name # TODO: push to engine's Hooke - self._c['manager'].LoadPerspective(self._perspectives[name]) - self._c['manager'].Update() - for pane in self._c['manager'].GetAllPanes(): - if pane.name in self._c['menu bar']._c['view']._c.keys(): - pane.Check(pane.window.IsShown()) - - def _SavePerspectiveToFile(self, name, perspective): - filename = ''.join([name, '.txt']) - filename = lh.get_file_path(filename, ['perspective']) - perspectivesFile = open(filename, 'w') - perspectivesFile.write(perspective) - perspectivesFile.close() - def select_plugin(self, _class=None, method=None, plugin=None): for option in config[section]: properties.append([option, config[section][option]]) @@ -457,7 +445,7 @@ class HookeFrame (wx.Frame): del self._c['manager'] self.Destroy() - def _update_perspectives(self): + def _setup_perspectives(self): """Add perspectives to menubar and _perspectives. """ self._perspectives = { @@ -468,9 +456,9 @@ class HookeFrame (wx.Frame): files = sorted(os.listdir(path)) for fname in files: name, extension = os.path.splitext(fname) - if extension != '.txt': + if extension != self.gui.config['perspective extension']: continue - fpath = os.path.join(path, fpath) + fpath = os.path.join(path, fname) if not os.path.isfile(fpath): continue perspective = None @@ -483,105 +471,94 @@ class HookeFrame (wx.Frame): if not self._perspectives.has_key(selected_perspective): self.gui.config['active perspective'] = 'Default' # TODO: push to engine's Hooke - self._update_perspective_menu() self._restore_perspective(selected_perspective) + self._update_perspective_menu() def _update_perspective_menu(self): self._c['menu bar']._c['perspective'].update( sorted(self._perspectives.keys()), - self.gui.config['active perspective'], - self._on_restore_perspective) - - def _on_restore_perspective(self, event): - name = self._c['menu bar'].FindItemById(event.GetId()).GetLabel() + self.gui.config['active perspective']) + + def _save_perspective(self, perspective, perspective_dir, name, + extension=None): + path = os.path.join(perspective_dir, name) + if extension != None: + path += extension + if not os.path.isdir(perspective_dir): + os.makedirs(perspective_dir) + with open(path, 'w') as f: + f.write(perspective) + self._perspectives[name] = perspective self._restore_perspective(name) + self._update_perspective_menu() - def _on_save_perspective(self, event): - def nameExists(name): - menu_position = self._c['menu bar'].FindMenu('Perspective') - menu = self._c['menu bar'].GetMenu(menu_position) - for item in menu.GetMenuItems(): - if item.GetText() == name: - return True - return False - - done = False - while not done: - dialog = wx.TextEntryDialog(self, 'Enter a name for the new perspective:', 'Save perspective') - dialog.SetValue('New perspective') - if dialog.ShowModal() != wx.ID_OK: - return - else: - name = dialog.GetValue() - - if nameExists(name): - 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) - if dialogConfirm.ShowModal() == wx.ID_YES: - done = True - else: - done = True + def _delete_perspectives(self, perspective_dir, names, + extension=None): + print 'pop', names + for name in names: + path = os.path.join(perspective_dir, name) + if extension != None: + path += extension + os.remove(path) + del(self._perspectives[name]) + self._update_perspective_menu() + if self.gui.config['active perspective'] in names: + self._restore_perspective('Default') + # TODO: does this bug still apply? + # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258 + # http://trac.wxwidgets.org/ticket/3258 + # ) that makes the radio item indicator in the menu disappear. + # The code should be fine once this issue is fixed. + def _restore_perspective(self, name): + if name != self.gui.config['active perspective']: + print 'restoring perspective:', name + self.gui.config['active perspective'] = name # TODO: push to engine's Hooke + self._c['manager'].LoadPerspective(self._perspectives[name]) + self._c['manager'].Update() + for pane in self._c['manager'].GetAllPanes(): + if pane.name in self._c['menu bar']._c['view']._c.keys(): + pane.Check(pane.window.IsShown()) + + def _on_save_perspective(self, *args): perspective = self._c['manager'].SavePerspective() - self._SavePerspectiveToFile(name, perspective) - self.gui.config['active perspectives'] = name - self._update_perspective_menu() -# if nameExists(name): -# #check the corresponding menu item -# menu_item = self.GetPerspectiveMenuItem(name) -# #replace the perspectiveStr in _pespectives -# self._perspectives[name] = perspective -# else: -# #because we deal with radio items, we need to do some extra work -# #delete all menu items from the perspectives menu -# for item in self._perspectives_menu.GetMenuItems(): -# self._perspectives_menu.DeleteItem(item) -# #recreate the perspectives menu -# self._perspectives_menu.Append(ID_SavePerspective, 'Save Perspective') -# self._perspectives_menu.Append(ID_DeletePerspective, 'Delete Perspective') -# self._perspectives_menu.AppendSeparator() -# #convert the perspectives dictionary into a list -# # the list contains: -# #[0]: name of the perspective -# #[1]: perspective -# perspectives_list = [key for key, value in self._perspectives.iteritems()] -# perspectives_list.append(name) -# perspectives_list.sort() -# #add all previous perspectives -# for index, item in enumerate(perspectives_list): -# menu_item = self._perspectives_menu.AppendRadioItem(ID_FirstPerspective + index, item) -# if item == name: -# menu_item.Check() -# #add the new perspective to _perspectives -# self._perspectives[name] = perspective - - def _on_delete_perspective(self, event): - dialog = panel.selection.Selection( - options=sorted(os.listdir(self.gui.config['perspective path'])), + name = self.gui.config['active perspective'] + if name == 'Default': + name = 'New perspective' + name = select_save_file( + directory=self.gui.config['perspective path'], + name=name, + extension=self.gui.config['perspective extension'], + parent=self, + message='Enter a name for the new perspective:', + caption='Save perspective') + if name == None: + return + self._save_perspective( + perspective, self.gui.config['perspective path'], name=name, + extension=self.gui.config['perspective extension']) + + def _on_delete_perspective(self, *args, **kwargs): + options = sorted([p for p in self._perspectives.keys() + if p != 'Default']) + dialog = SelectionDialog( + options=options, message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n", button_id=wx.ID_DELETE, - callbacks={'button': self._on_delete_perspective}, selection_style='multiple', parent=self, - label='Delete perspective(s)', + title='Delete perspective(s)', style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER) dialog.CenterOnScreen() dialog.ShowModal() + names = [options[i] for i in dialog.selected] dialog.Destroy() - self._update_perspective_menu() - # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258 - # http://trac.wxwidgets.org/ticket/3258 - # ) that makes the radio item indicator in the menu disappear. - # The code should be fine once this issue is fixed. + self._delete_perspectives( + self.gui.config['perspective path'], names=names, + extension=self.gui.config['perspective extension']) - def _on_delete_perspective(self, _class, method, options, selected): - for p in selected: - self._perspectives.remove(p) - if p == self.gui.config['active perspective']: - self.gui.config['active perspective'] = 'Default' - path = os.path.join(self.gui.config['perspective path'], - p+'.txt') - remove(path) - self._update_perspective_menu() + def _on_select_perspective(self, _class, method, name): + self._restore_perspective(name) def _on_dir_ctrl_left_double_click(self, event): file_path = self.panelFolders.GetPath() @@ -951,7 +928,7 @@ class HookeFrame (wx.Frame): notebook.SetSelection(index) notebook.DeletePage(notebook.GetSelection()) self.Parent.DeleteFromPlaylists(playlist_name) - + class HookeApp (wx.App): """A :class:`wx.App` wrapper around :class:`HookeFrame`. @@ -997,7 +974,8 @@ class HookeApp (wx.App): return True def _setup_splash_screen(self): - if self.gui.config['show splash screen']: + if self.gui.config['show splash screen'] == 'True': # HACK: config should decode + print 'splash', self.gui.config['show splash screen'] path = self.gui.config['splash screen image'] if os.path.isfile(path): duration = int(self.gui.config['splash screen duration']) # HACK: config should decode types @@ -1047,6 +1025,9 @@ class GUI (UserInterface): Setting(section=self.setting_section, option='perspective path', value=os.path.join('resources', 'gui', 'perspective'), help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH. + Setting(section=self.setting_section, option='perspective extension', + value='.txt', + help='Extension for perspective files.'), Setting(section=self.setting_section, option='hide extensions', value=False, help='Hide file extensions when displaying names.'), diff --git a/hooke/ui/gui/dialog/__init__.py b/hooke/ui/gui/dialog/__init__.py index 0d03794..0cb446e 100644 --- a/hooke/ui/gui/dialog/__init__.py +++ b/hooke/ui/gui/dialog/__init__.py @@ -1,39 +1,4 @@ # Copyright -from ....util.pluggable import IsSubclass, construct_graph - - -HANDLER_MODULES = [ - 'boolean', - 'float', -# 'int' -# 'point', - 'selection', - 'string' - ] -"""List of handler modules. TODO: autodiscovery -""" - -class Handler (object): - """Base class for :class:`~hooke.interaction.Request` handlers. - - :attr:`name` identifies the request type and should match the - module name. - """ - def __init__(self, name): - self.name = name - - def run(self, hooke_frame, msg): - raise NotImplemented - - def _cancel(self, *args, **kwargs): - # TODO: somehow abort the running command - - -HANDLERS = construct_odict( - this_modname=__name__, - submodnames=USER_INTERFACE_MODULES, - class_selector=IsSubclass(UserInterface, blacklist=[UserInterface])) -""":class:`hooke.compat.odict.odict` of :class:`Handler` -instances keyed by `.name`. +"""A collection of useful popup dialogs for user interaction. """ diff --git a/hooke/ui/gui/dialog/save_file.py b/hooke/ui/gui/dialog/save_file.py new file mode 100644 index 0000000..ade99b2 --- /dev/null +++ b/hooke/ui/gui/dialog/save_file.py @@ -0,0 +1,46 @@ +# Copyright + +"""Define :func:`select_save_file` +""" + +import os.path + +import wx + + +def select_save_file(directory, name, extension=None, *args, **kwargs): + """Get a filename from the user for saving data. + + 1) Prompt the user for a name using `name` as the default. + + * If the user cancels, return `None` + * If the selected name does not exist, return it. + + 2) If the selected name already exists, ask for clobber + confirmation. + + * If clobbering is ok, return the selected name. + * Otherwise, return to (1). + """ + def path(name): + return os.path.join(directory, name+extension) + def name_exists(name): + os.path.exists(path(name)) + + while True: + dialog = wx.TextEntryDialog(*args, **kwargs) + dialog.SetValue(name) + if dialog.ShowModal() != wx.ID_OK: + return # abort + name = dialog.GetValue() + if not name_exists(name): + return name + dialogConfirm = wx.MessageDialog( + parent=self, + message='\n\n'.join( + ['A file with this name already exists.', + 'Do you want to replace it?']), + caption='Confirm', + style=wx.YES_NO|wx.ICON_QUESTION|wx.CENTER) + if dialogConfirm.ShowModal() == wx.ID_YES: + return name diff --git a/hooke/ui/gui/dialog/selection.py b/hooke/ui/gui/dialog/selection.py index 15baec2..bc5f185 100644 --- a/hooke/ui/gui/dialog/selection.py +++ b/hooke/ui/gui/dialog/selection.py @@ -4,6 +4,7 @@ """ from os import remove +import types import wx @@ -22,11 +23,13 @@ class Selection (wx.Dialog): .. _standard wx IDs: http://docs.wxwidgets.org/stable/wx_stdevtid.html#stdevtid """ - def __init__(self, options, message, button_id, callbacks, + def __init__(self, options, message, button_id, callbacks=None, default=None, selection_style='single', *args, **kwargs): super(Selection, self).__init__(*args, **kwargs) self._options = options + if callbacks == None: + callbacks = {} self._callbacks = callbacks self._selection_style = selection_style @@ -39,46 +42,40 @@ class Selection (wx.Dialog): size = wx.Size(175, 200) if selection_style == 'single': self._c['listbox'] = wx.ListBox( - parent=self, size=size, list=options) + parent=self, size=size, choices=options) if default != None: self._c['listbox'].SetSelection(default) else: assert selection_style == 'multiple', selection_style self._c['listbox'] = wx.CheckListBox( - parent=self, size=size, list=options) + parent=self, size=size, choices=options) if default != None: self._c['listbox'].Check(default) self.Bind(wx.EVT_BUTTON, self.button, self._c['button']) self.Bind(wx.EVT_BUTTON, self.cancel, self._c['cancel']) - border_width = 5 - b = wx.BoxSizer(wx.HORIZONTAL) - b.Add(window=self._c['button'], - flag=wx.GROW|wx.ALIGN_CENTER_VERTICAL|wx.ALL, - border=border_width) - b.Add(window=self._c['cancel'], - flag=wx.GROW|wx.ALIGN_CENTER_VERTICAL|wx.ALL, - border=border_width) - + self._add(b, 'button') + self._add(b, 'cancel') v = wx.BoxSizer(wx.VERTICAL) - v.Add(window=self._c['text'], - flag=wx.GROW|wx.ALIGN_CENTER_VERTICAL|wx.ALL, - border=border_width) - v.Add(window=self._c['listbox'], - proportion=1, - flag=wx.GROW|wx.ALIGN_CENTER_VERTICAL|wx.ALL, - border=border_width) - v.Add(window=wx.StaticLine( + self._add(v, 'text') + self._add(v, 'listbox') + self._add(v, wx.StaticLine( parent=self, size=(20,-1), style=wx.LI_HORIZONTAL), - flag=wx.GROW, - border=border_width) - v.Add(window=b, - flag=wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_CENTER_HORIZONTAL|wx.ALL, - border=border_width) + flag=wx.GROW) + self._add(v, b) self.SetSizer(v) v.Fit(self) + def _add(self, sizer, item, + flag=wx.GROW|wx.ALIGN_CENTER_VERTICAL|wx.ALL, + border=5): + kwargs = {'flag':flag, 'border':border} + if isinstance(item, types.StringTypes): + item = self._c[item] + kwargs['item'] = item # window + sizer.Add(**kwargs) + @callback def cancel(self, event): """Close the dialog. @@ -92,6 +89,7 @@ class Selection (wx.Dialog): selected = self._c['listbox'].GetSelection() else: assert self._selection_style == 'multiple', self._selection_style - selected = self._c['listbox'].GetChecked()) + selected = self._c['listbox'].GetChecked() + self.selected = selected in_callback(self, options=self._options, selected=selected) self.EndModal(wx.ID_CLOSE) diff --git a/hooke/ui/gui/menu.py b/hooke/ui/gui/menu.py index 320bc43..c422f9f 100644 --- a/hooke/ui/gui/menu.py +++ b/hooke/ui/gui/menu.py @@ -21,6 +21,7 @@ class Menu (wx.Menu): """ def __init__(self, parent=None, **kwargs): self._parent = parent + self._bindings = [] super(Menu, self).__init__(**kwargs) def Bind(self, **kwargs): @@ -29,6 +30,24 @@ class Menu (wx.Menu): while not isinstance(obj, wx.Frame): obj = obj._parent obj.Bind(**kwargs) + self._bindings.append(kwargs) + + def Unbind(self, **kwargs): + assert 'id' in kwargs, kwargs + try: + self._bindings.remove(kwargs) + except ValueError: + pass + kwargs.pop('handler', None) + obj = self + while not isinstance(obj, wx.Frame): + obj = obj._parent + obj.Unbind(**kwargs) + + def _unbind_all_items(self): + for kwargs in self._bindings: + self.Unbind(**kwargs) + self._bindings = [] class MenuBar (wx.MenuBar): @@ -89,24 +108,44 @@ class PerspectiveMenu (Menu): self._callbacks = callbacks self._c = {} - def update(self, perspectives, selected, callback): + def update(self, perspectives, selected): """Rebuild the perspectives menu. """ + self._unbind_all_items() for item in self.GetMenuItems(): - self.UnBind(item.GetId) self.DeleteItem(item) self._c = { 'save': self.Append(id=wx.ID_ANY, text='Save Perspective'), 'delete': self.Append(id=wx.ID_ANY, text='Delete Perspective'), } + self.Bind(event=wx.EVT_MENU, handler=self.save_perspective, + id=self._c['save'].GetId()) + self.Bind(event=wx.EVT_MENU, handler=self.delete_perspective, + id=self._c['delete'].GetId()) self.AppendSeparator() for label in perspectives: self._c[label] = self.AppendRadioItem(id=wx.ID_ANY, text=label) - self.Bind(event=wx.EVT_MENU, handler=callback, + self.Bind(event=wx.EVT_MENU, handler=self.select_perspective, id=self._c[label].GetId()) if label == selected: self._c[label].Check(True) + @callback + def save_perspective(self, event): + pass + + @callback + def delete_perspective(self, event): + pass + + def select_perspective(self, event): + _id = event.GetId() + item = self.FindItemById(_id) + label = item.GetLabel() + selected = item.IsChecked() + assert selected == True, label + in_callback(self, name=label) + class HelpMenu (Menu): def __init__(self, callbacks=None, **kwargs):