From: W. Trevor King Date: Sat, 31 Jul 2010 17:27:54 +0000 (-0400) Subject: Several changes while getting 'plot' panel working X-Git-Url: http://git.tremily.us/?a=commitdiff_plain;h=6f0c7079be6c90618df05a4dc6f1adc187b2f0ba;p=hooke.git Several changes while getting 'plot' panel working --- diff --git a/hooke/ui/gui/__init__.py b/hooke/ui/gui/__init__.py index b1e854d..5dac90f 100644 --- a/hooke/ui/gui/__init__.py +++ b/hooke/ui/gui/__init__.py @@ -1,6 +1,7 @@ # Copyright """Defines :class:`GUI` providing a wxWidgets interface to Hooke. + """ WX_GOOD=['2.8'] @@ -18,15 +19,11 @@ import time import wx.html import wx.aui as aui import wx.lib.evtmgr as evtmgr - - # wxPropertyGrid is included in wxPython >= 2.9.1, see # http://wxpropgrid.sourceforge.net/cgi-bin/index?page=download # until then, we'll avoid it because of the *nix build problems. #import wx.propgrid as wxpg -from matplotlib.ticker import FuncFormatter - from ...command import CommandExit, Exit, Success, Failure, Command, Argument from ...config import Setting from ...interaction import Request, BooleanRequest, ReloadUserInterfaceConfig @@ -36,7 +33,7 @@ from .dialog.save_file import select_save_file from . import menu as menu from . import navbar as navbar from . import panel as panel -from .panel.propertyeditor2 import prop_from_argument, prop_from_setting +from .panel.propertyeditor import prop_from_argument, prop_from_setting from . import prettyformat as prettyformat from . import statusbar as statusbar @@ -68,7 +65,7 @@ class HookeFrame (wx.Frame): # Min size for the frame itself isn't completely done. See # the end of FrameManager::Update() for the test code. For # now, just hard code a frame minimum size. - self.SetMinSize(wx.Size(500, 500)) + #self.SetMinSize(wx.Size(500, 500)) self._setup_panels() self._setup_toolbars() @@ -76,8 +73,10 @@ class HookeFrame (wx.Frame): # Create the menubar after the panes so that the default # perspective is created with all panes open + panels = [p for p in self._c.values() if isinstance(p, panel.Panel)] self._c['menu bar'] = menu.HookeMenuBar( parent=self, + panels=panels, callbacks={ 'close': self._on_close, 'about': self._on_about, @@ -96,19 +95,22 @@ class HookeFrame (wx.Frame): self._setup_perspectives() self._bind_events() - name = self.gui.config['active perspective'] + self.execute_command( + command=self._command_by_name('load playlist'), + args={'input':'test/data/test'}, + ) return # TODO: cleanup - self.playlists = self._c['playlists'].Playlists + self.playlists = self._c['playlist'].Playlists self._displayed_plot = None #load default list, if possible - self.do_loadlist(self.GetStringFromConfig('core', 'preferences', 'playlist')) + self.do_loadlist(self.GetStringFromConfig('core', 'preferences', 'playlists')) # GUI maintenance def _setup_panels(self): client_size = self.GetClientSize() - for label,p,style in [ + for p,style in [ # ('folders', wx.GenericDirCtrl( # parent=self, # dir=self.gui.config['folders-workdir'], @@ -116,7 +118,7 @@ class HookeFrame (wx.Frame): # style=wx.DIRCTRL_SHOW_FILTERS, # filter=self.gui.config['folders-filters'], # defaultFilter=int(self.gui.config['folders-filter-index'])), 'left'), #HACK: config should convert - ('playlists', panel.PANELS['playlist']( + (panel.PANELS['playlist']( callbacks={ 'delete_playlist':self._on_user_delete_playlist, '_delete_playlist':self._on_delete_playlist, @@ -125,7 +127,6 @@ class HookeFrame (wx.Frame): '_on_set_selected_playlist':self._on_set_selected_playlist, '_on_set_selected_curve':self._on_set_selected_curve, }, - config=self.gui.config, parent=self, style=wx.WANTS_CHARS|wx.NO_BORDER, # WANTS_CHARS so the panel doesn't eat the Return key. @@ -141,7 +142,7 @@ class HookeFrame (wx.Frame): # size=wx.Size(430, 200), # style=aui.AUI_NB_DEFAULT_STYLE # | aui.AUI_NB_TAB_EXTERNAL_MOVE | wx.NO_BORDER), 'center'), - ('commands', panel.PANELS['commands']( + (panel.PANELS['commands']( commands=self.commands, selected=self.gui.config['selected command'], callbacks={ @@ -155,7 +156,7 @@ class HookeFrame (wx.Frame): # WANTS_CHARS so the panel doesn't eat the Return key. # size=(160, 200), ), 'right'), - ('property', panel.PANELS['propertyeditor2']( + (panel.PANELS['propertyeditor']( callbacks={}, parent=self, style=wx.WANTS_CHARS, @@ -166,7 +167,15 @@ class HookeFrame (wx.Frame): # pos=wx.Point(0, 0), # size=wx.Size(150, 90), # style=wx.NO_BORDER|wx.TE_MULTILINE), 'right'), - ('output', panel.PANELS['output']( + (panel.PANELS['plot']( + callbacks={ + }, + parent=self, + style=wx.WANTS_CHARS|wx.NO_BORDER, + # WANTS_CHARS so the panel doesn't eat the Return key. +# size=(160, 200), + ), 'center'), + (panel.PANELS['output']( parent=self, pos=wx.Point(0, 0), size=wx.Size(150, 90), @@ -174,13 +183,13 @@ class HookeFrame (wx.Frame): 'bottom'), # ('results', panel.results.Results(self), 'bottom'), ]: - self._add_panel(label, p, style) + self._add_panel(p, style) #self._c['assistant'].SetEditable(False) - def _add_panel(self, label, panel, style): - self._c[label] = panel - cap_label = label.capitalize() - info = aui.AuiPaneInfo().Name(cap_label).Caption(cap_label) + def _add_panel(self, panel, style): + self._c[panel.name] = panel + m_name = panel.managed_name + info = aui.AuiPaneInfo().Name(m_name).Caption(m_name) info.PaneBorder(False).CloseButton(True).MaximizeButton(False) if style == 'top': info.Top() @@ -263,6 +272,17 @@ class HookeFrame (wx.Frame): + # Panel utility functions + + def _file_name(self, name): + """Cleanup names according to configured preferences. + """ + if self.gui.config['hide extensions'] == 'True': # HACK: config should decode + name,ext = os.path.splitext(name) + return name + + + # Command handling def _command_by_name(self, name): @@ -277,10 +297,10 @@ class HookeFrame (wx.Frame): command=None, args=None): if args == None: args = {} - if ('property' in self._c + if ('property editor' in self._c and self.gui.config['selected command'] == command): arg_names = [arg.name for arg in command.arguments] - for name,value in self._c['property'].get_values().items(): + for name,value in self._c['property editor'].get_values().items(): if name in arg_names: args[name] = value print 'executing', command.name, args @@ -353,22 +373,25 @@ class HookeFrame (wx.Frame): """ if not isinstance(results[-1], Success): self._postprocess_text(command, results=results) + return assert len(results) == 2, results playlist = results[0] - self._c['playlists']._c['tree'].add_playlist(playlist) + self._c['playlist']._c['tree'].add_playlist(playlist) def _postprocess_get_playlist(self, command, args={}, results=[]): if not isinstance(results[-1], Success): self._postprocess_text(command, results=results) + return assert len(results) == 2, results playlist = results[0] - self._c['playlists']._c['tree'].update_playlist(playlist) + self._c['playlist']._c['tree'].update_playlist(playlist) def _postprocess_get_curve(self, command, args={}, results=[]): """Update `self` to show the curve. """ if not isinstance(results[-1], Success): self._postprocess_text(command, results=results) + return assert len(results) == 2, results curve = results[0] if args.get('curve', None) == None: @@ -378,8 +401,11 @@ class HookeFrame (wx.Frame): playlist = results[0] else: raise NotImplementedError() - self._c['playlists']._c['tree'].set_selected_curve( - playlist, curve) + if 'playlist' in self._c: + self._c['playlist']._c['tree'].set_selected_curve( + playlist, curve) + if 'plot' in self._c: + self._c['plot'].set_curve(curve, config=self.gui.config) def _postprocess_next_curve(self, command, args={}, results=[]): """No-op. Only call 'next curve' via `self._next_curve()`. @@ -397,16 +423,16 @@ class HookeFrame (wx.Frame): def _GetActiveFileIndex(self): lib.playlist.Playlist = self.GetActivePlaylist() #get the selected item from the tree - selected_item = self._c['playlists']._c['tree'].GetSelection() + selected_item = self._c['playlist']._c['tree'].GetSelection() #test if a playlist or a curve was double-clicked - if self._c['playlists']._c['tree'].ItemHasChildren(selected_item): + if self._c['playlist']._c['tree'].ItemHasChildren(selected_item): return -1 else: count = 0 - selected_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item) + selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item) while selected_item.IsOk(): count += 1 - selected_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item) + selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item) return count def _GetPlaylistTab(self, name): @@ -568,92 +594,6 @@ class HookeFrame (wx.Frame): self.UpdateNote() self.UpdatePlot() - def UpdatePlot(self, plot=None): - - def add_to_plot(curve, set_scale=True): - if curve.visible and curve.x and curve.y: - #get the index of the subplot to use as destination - destination = (curve.destination.column - 1) * number_of_rows + curve.destination.row - 1 - #set all parameters for the plot - axes_list[destination].set_title(curve.title) - if set_scale: - axes_list[destination].set_xlabel(curve.prefix.x + curve.units.x) - axes_list[destination].set_ylabel(curve.prefix.y + curve.units.y) - #set the formatting details for the scale - formatter_x = lib.curve.PrefixFormatter(curve.decimals.x, curve.prefix.x, use_zero) - formatter_y = lib.curve.PrefixFormatter(curve.decimals.y, curve.prefix.y, use_zero) - axes_list[destination].xaxis.set_major_formatter(formatter_x) - axes_list[destination].yaxis.set_major_formatter(formatter_y) - if curve.style == 'plot': - axes_list[destination].plot(curve.x, curve.y, color=curve.color, label=curve.label, lw=curve.linewidth, zorder=1) - if curve.style == 'scatter': - axes_list[destination].scatter(curve.x, curve.y, color=curve.color, label=curve.label, s=curve.size, zorder=2) - #add the legend if necessary - if curve.legend: - axes_list[destination].legend() - - if plot is None: - active_file = self.GetActiveFile() - if not active_file.driver: - #the first time we identify a file, the following need to be set - active_file.identify(self.drivers) - for curve in active_file.plot.curves: - curve.decimals.x = self.GetIntFromConfig('core', 'preferences', 'x_decimals') - curve.decimals.y = self.GetIntFromConfig('core', 'preferences', 'y_decimals') - curve.legend = self.GetBoolFromConfig('core', 'preferences', 'legend') - curve.prefix.x = self.GetStringFromConfig('core', 'preferences', 'x_prefix') - curve.prefix.y = self.GetStringFromConfig('core', 'preferences', 'y_prefix') - if active_file.driver is None: - self.AppendToOutput('Invalid file: ' + active_file.filename) - return - self.displayed_plot = copy.deepcopy(active_file.plot) - #add raw curves to plot - self.displayed_plot.raw_curves = copy.deepcopy(self.displayed_plot.curves) - #apply all active plotmanipulators - self.displayed_plot = self.ApplyPlotmanipulators(self.displayed_plot, active_file) - #add corrected curves to plot - self.displayed_plot.corrected_curves = copy.deepcopy(self.displayed_plot.curves) - else: - active_file = None - self.displayed_plot = copy.deepcopy(plot) - - figure = self.GetActiveFigure() - figure.clear() - - #use '0' instead of e.g. '0.00' for scales - use_zero = self.GetBoolFromConfig('core', 'preferences', 'use_zero') - #optionally remove the extension from the title of the plot - hide_curve_extension = self.GetBoolFromConfig('core', 'preferences', 'hide_curve_extension') - if hide_curve_extension: - title = lh.remove_extension(self.displayed_plot.title) - else: - title = self.displayed_plot.title - figure.suptitle(title, fontsize=14) - #create the list of all axes necessary (rows and columns) - axes_list =[] - number_of_columns = max([curve.destination.column for curve in self.displayed_plot.curves]) - number_of_rows = max([curve.destination.row for curve in self.displayed_plot.curves]) - for index in range(number_of_rows * number_of_columns): - axes_list.append(figure.add_subplot(number_of_rows, number_of_columns, index + 1)) - - #add all curves to the corresponding plots - for curve in self.displayed_plot.curves: - add_to_plot(curve) - - #make sure the titles of 'subplots' do not overlap with the axis labels of the 'main plot' - figure.subplots_adjust(hspace=0.3) - - #display results - self.panelResults.ClearResults() - if self.displayed_plot.results.has_key(self.results_str): - for curve in self.displayed_plot.results[self.results_str].results: - add_to_plot(curve, set_scale=False) - self.panelResults.DisplayResults(self.displayed_plot.results[self.results_str]) - else: - self.panelResults.ClearResults() - #refresh the plot - figure.canvas.draw() - def _on_curve_select(self, playlist, curve): #create the plot tab and add playlist to the dictionary plotPanel = panel.plot.PlotPanel(self, ID_FirstPlot + len(self.playlists)) @@ -692,7 +632,7 @@ class HookeFrame (wx.Frame): #self.select_plugin(plugin=command.plugin) if 'assistant' in self._c: self._c['assitant'].ChangeValue(command.help) - self._c['property'].clear() + self._c['property editor'].clear() for argument in command.arguments: if argument.name == 'help': continue @@ -717,7 +657,7 @@ class HookeFrame (wx.Frame): argument, curves=curves, playlists=playlists) if p == None: continue # property intentionally not handled (yet) - self._c['property'].append_property(p) + self._c['property editor'].append_property(p) self.gui.config['selected command'] = command # TODO: push to engine @@ -824,7 +764,7 @@ 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._restore_perspective(selected_perspective) + self._restore_perspective(selected_perspective, force=True) self._update_perspective_menu() def _update_perspective_menu(self): @@ -863,15 +803,16 @@ class HookeFrame (wx.Frame): # ) 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']: + def _restore_perspective(self, name, force=False): + if name != self.gui.config['active perspective'] or force == True: 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()) + view = self._c['menu bar']._c['view'] + if pane.name in view._c.keys(): + view._c[pane.name].Check(pane.window.IsShown()) def _on_save_perspective(self, *args): perspective = self._c['manager'].SavePerspective() @@ -1016,6 +957,18 @@ class GUI (UserInterface): Setting(section=self.setting_section, option='hide extensions', value=False, help='Hide file extensions when displaying names.'), + Setting(section=self.setting_section, option='plot legend', + value=True, + help='Enable/disable the plot legend.'), + Setting(section=self.setting_section, option='plot x format', + value='None', + help='Display format for plot x values.'), + Setting(section=self.setting_section, option='plot y format', + value='None', + help='Display format for plot y values.'), + Setting(section=self.setting_section, option='plot zero', + value=0, + help='Select "0" vs. e.g. "0.00" for plot axes?'), Setting(section=self.setting_section, option='folders-workdir', value='.', help='This should probably go...'), diff --git a/hooke/ui/gui/menu.py b/hooke/ui/gui/menu.py index c422f9f..068e32e 100644 --- a/hooke/ui/gui/menu.py +++ b/hooke/ui/gui/menu.py @@ -6,7 +6,6 @@ import wx from ...util.callback import callback, in_callback -from . import panel as panel class Menu (wx.Menu): @@ -79,14 +78,14 @@ class FileMenu (Menu): class ViewMenu (Menu): - def __init__(self, callbacks=None, **kwargs): + def __init__(self, panels, callbacks=None, **kwargs): super(ViewMenu, self).__init__(**kwargs) if callbacks == None: callbacks = {} self._callbacks = callbacks self._c = {} - for i,panelname in enumerate(sorted(panel.PANELS.keys())): - text = '%s\tF%d' % (panelname.capitalize(), i+5) + for i,panelname in enumerate(sorted([p.managed_name for p in panels])): + text = '%s\tF%d' % (panelname, i+5) self._c[panelname] = self.AppendCheckItem(id=wx.ID_ANY, text=text) for item in self._c.values(): item.Check() @@ -162,7 +161,7 @@ class HelpMenu (Menu): class HookeMenuBar (MenuBar): - def __init__(self, callbacks=None, **kwargs): + def __init__(self, panels, callbacks=None, **kwargs): super(HookeMenuBar, self).__init__(**kwargs) if callbacks == None: callbacks = {} @@ -173,5 +172,8 @@ class HookeMenuBar (MenuBar): for key in ['file', 'view', 'perspective', 'help']: cap_key = key.capitalize() _class = globals()['%sMenu' % cap_key] - self._c[key] = _class(parent=self, callbacks=callbacks) + kwargs = {} + if key == 'view': + kwargs['panels'] = panels + self._c[key] = _class(parent=self, callbacks=callbacks, **kwargs) self.Append(self._c[key], cap_key) diff --git a/hooke/ui/gui/panel/__init__.py b/hooke/ui/gui/panel/__init__.py index 4a2a8da..11e5187 100644 --- a/hooke/ui/gui/panel/__init__.py +++ b/hooke/ui/gui/panel/__init__.py @@ -1,5 +1,8 @@ # Copyright +"""The `panel` module provides optional submodules that add GUI panels. +""" + from ....util.pluggable import IsSubclass, construct_odict @@ -9,9 +12,8 @@ PANEL_MODULES = [ # 'notebook', 'output', 'playlist', -# 'plot', -# 'propertyeditor', - 'propertyeditor2', + 'plot', + 'propertyeditor', # 'results', # 'selection', # 'welcome', @@ -28,6 +30,8 @@ class Panel (object): def __init__(self, name=None, callbacks=None, **kwargs): super(Panel, self).__init__(**kwargs) self.name = name + self.managed_name = name.capitalize() + self._hooke_frame = kwargs.get('parent', None) if callbacks == None: callbacks = {} self._callbacks = callbacks diff --git a/hooke/ui/gui/panel/playlist.py b/hooke/ui/gui/panel/playlist.py index 5cbeda1..d9c996c 100644 --- a/hooke/ui/gui/panel/playlist.py +++ b/hooke/ui/gui/panel/playlist.py @@ -28,7 +28,9 @@ class Menu (wx.Menu): class Tree (wx.TreeCtrl): """:class:`wx.TreeCtrl` subclass handling playlist and curve selection. """ - def __init__(self, config, callbacks, *args, **kwargs): + def __init__(self, *args, **kwargs): + self._panel = kwargs['parent'] + self._callbacks = self._panel._callbacks # TODO: CallbackClass.set_callback{,s}() super(Tree, self).__init__(*args, **kwargs) imglist = wx.ImageList(width=16, height=16, mask=True, initialCount=2) imglist.Add(wx.ArtProvider.GetBitmap( @@ -48,8 +50,6 @@ class Tree (wx.TreeCtrl): self.Bind(wx.EVT_RIGHT_DOWN, self._on_context_menu) self.Bind(wx.EVT_TREE_SEL_CHANGED, self._on_select) - self.config = config - self._callbacks = callbacks self._setup_playlists() def _setup_playlists(self): @@ -61,13 +61,6 @@ class Tree (wx.TreeCtrl): self._id_for_name = {} # {name: id} self._name_for_id = {} # {id: name} - def _name(self, name): - """Cleanup names according to configured preferences. - """ - if self.config['hide extensions'] == 'True': # HACK: config should decode - name,ext = os.path.splitext(name) - return name - def _is_curve(self, name): # name from ._id_for_name / ._name_for_id """Return `True` if `name` corresponds to a :class:`hooke.curve.Curve`. """ @@ -128,7 +121,7 @@ class Tree (wx.TreeCtrl): self._playlists[playlist.name] = playlist p_id = self.AppendItem( parent=self._c['root'], - text=self._name(playlist.name), + text=self._panel._hooke_frame._file_name(playlist.name), image=self.image['playlist']) self._id_for_name[playlist.name] = p_id self._name_for_id[p_id] = playlist.name @@ -154,7 +147,7 @@ class Tree (wx.TreeCtrl): p.append(curve) c_id = self.AppendItem( parent=self._id_for_name[playlist_name], - text=self._name(curve.name), + text=self._panel._hooke_frame._file_name(curve.name), image=self.image['curve']) self._id_for_name[(p.name, curve.name)] = c_id self._name_for_id[c_id] = (p.name, curve.name) @@ -324,15 +317,12 @@ class Tree (wx.TreeCtrl): class Playlist (Panel, wx.Panel): """:class:`wx.Panel` subclass wrapper for :class:`Tree`. """ - def __init__(self, config, callbacks, *args, **kwargs): + def __init__(self, callbacks=None, **kwargs): # Use the WANTS_CHARS style so the panel doesn't eat the Return key. - super(Playlist, self).__init__(*args, **kwargs) - self.name = 'playlist panel' - + super(Playlist, self).__init__( + name='playlist', callbacks=callbacks, **kwargs) self._c = { 'tree': Tree( - config=config, - callbacks=callbacks, parent=self, size=wx.Size(160, 250), style=wx.TR_DEFAULT_STYLE | wx.NO_BORDER | wx.TR_HIDE_ROOT), diff --git a/hooke/ui/gui/panel/plot.py b/hooke/ui/gui/panel/plot.py index b1c7f78..2281812 100644 --- a/hooke/ui/gui/panel/plot.py +++ b/hooke/ui/gui/panel/plot.py @@ -1,144 +1,168 @@ # Copyright """Plot panel for Hooke. + +Notes +----- +Originally based on `this example`_. + +.. _this example: + http://matplotlib.sourceforge.net/examples/user_interfaces/embedding_in_wx2.html """ +import matplotlib +matplotlib.use('WXAgg') # use wxpython with antigrain (agg) rendering from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas -from matplotlib.backends.backend_wx import NavigationToolbar2Wx +from matplotlib.backends.backend_wx import NavigationToolbar2Wx as NavToolbar from matplotlib.figure import Figure - import wx -# There are many comments in here from the demo app. -# They should come in handy to expand the functionality in the future. - -class HookeCustomToolbar(NavigationToolbar2Wx): - - def __init__(self, plotCanvas): - NavigationToolbar2Wx.__init__(self, plotCanvas) - # add new toolbar buttons - #glyph_file = 'resources' + os.sep + 'pipette.png' - #glyph = wx.Image(glyph_file, wx.BITMAP_TYPE_ANY).ConvertToBitmap() - - #self.AddCheckTool(ON_CUSTOM_PICK, glyph, shortHelp='Select a data point', longHelp='Select a data point') - #wx.EVT_TOOL(self, ON_CUSTOM_PICK, self.OnSelectPoint) - - # remove the unwanted button -# POSITION_OF_CONFIGURE_SUBPLOTS_BTN = 6 -# self.DeleteToolByPos(POSITION_OF_CONFIGURE_SUBPLOTS_BTN) - - #def OnSelectPoint(self, event): - #self.Parent.Parent.Parent.pick_active = True - - -#class LineBuilder: - #def __init__(self, line): - #self.line = line - #self.xs = list(line.get_xdata()) - #self.ys = list(line.get_ydata()) - #self.cid = line.figure.canvas.mpl_connect('button_press_event', self) - - #def __call__(self, event): - #print 'click', event - #if event.inaxes != self.line.axes: - #return - #self.xs.append(event.xdata) - #self.ys.append(event.ydata) - #self.line.set_data(self.xs, self.ys) - #self.line.figure.canvas.draw() - +from ....util.callback import callback, in_callback +from . import Panel -class PlotPanel(wx.Panel): - - def __init__(self, parent, ID): - wx.Panel.__init__(self, parent, ID, style=wx.WANTS_CHARS|wx.NO_BORDER, size=(160, 200)) - - self.figure = Figure() - self.canvas = FigureCanvas(self, -1, self.figure) - self.SetColor(wx.NamedColor('WHITE')) - - self.sizer = wx.BoxSizer(wx.VERTICAL) - self.sizer.Add(self.canvas, 1, wx.LEFT | wx.TOP | wx.GROW) - self.SetSizer(self.sizer) - self.Fit() +class PlotPanel (Panel, wx.Panel): + """UI for graphical curve display. + """ + def __init__(self, callbacks=None, **kwargs): self.display_coordinates = False + self.style = 'line' + self._curve = None + super(PlotPanel, self).__init__( + name='plot', callbacks=callbacks, **kwargs) + self._c = {} + self._c['figure'] = Figure() + self._c['canvas'] = FigureCanvas( + parent=self, id=wx.ID_ANY, figure=self._c['figure']) + self._c['toolbar'] = NavToolbar(self._c['canvas']) + + self._set_color(wx.NamedColor('WHITE')) + sizer = wx.BoxSizer(wx.VERTICAL) + sizer.Add(self._c['canvas'], 1, wx.LEFT | wx.TOP | wx.GROW) + self._setup_toolbar(toolbar=self._c['toolbar'], sizer=sizer) + self.SetSizer(sizer) + self.Fit() - self.figure.canvas.mpl_connect('button_press_event', self.OnClick) - self.figure.canvas.mpl_connect('axes_enter_event', self.OnEnterAxes) - self.figure.canvas.mpl_connect('axes_leave_event', self.OnLeaveAxes) - self.figure.canvas.mpl_connect('motion_notify_event', self.OnMouseMove) - self.add_toolbar() # comment this out for no toolbar - - def add_toolbar(self): - self.toolbar = HookeCustomToolbar(self.canvas) - self.toolbar.Realize() + self.Bind(wx.EVT_SIZE, self._on_size) + self._c['figure'].canvas.mpl_connect( + 'button_press_event', self._on_click) + self._c['figure'].canvas.mpl_connect( + 'axes_enter_event', self._on_enter_axes) + self._c['figure'].canvas.mpl_connect( + 'axes_leave_event', self._on_leave_axes) + self._c['figure'].canvas.mpl_connect( + 'motion_notify_event', self._on_mouse_move) + + def _setup_toolbar(self, toolbar, sizer): + self._c['toolbar'].Realize() # call after putting items in the toolbar if wx.Platform == '__WXMAC__': # Mac platform (OSX 10.3, MacPython) does not seem to cope with # having a toolbar in a sizer. This work-around gets the buttons # back, but at the expense of having the toolbar at the top - self.SetToolBar(self.toolbar) - else: + self.SetToolBar(toolbar) + elif wx.Platform == '__WXMSW__': # On Windows platform, default window size is incorrect, so set # toolbar width to figure width. - tw, th = self.toolbar.GetSizeTuple() - fw, fh = self.canvas.GetSizeTuple() + tw, th = toolbar.GetSizeTuple() + fw, fh = self._c['canvas'].GetSizeTuple() # By adding toolbar in sizer, we are able to put it at the bottom # of the frame - so appearance is closer to GTK version. # As noted above, doesn't work for Mac. - self.toolbar.SetSize(wx.Size(fw, th)) - self.sizer.Add(self.toolbar, 0, wx.LEFT | wx.EXPAND) - # update the axes menu on the toolbar - self.toolbar.update() - - def get_figure(self): - return self.figure - - def SetColor(self, rgbtuple): - ''' - Set figure and canvas colours to be the same - ''' - if not rgbtuple: + toolbar.SetSize(wx.Size(fw, th)) + sizer.Add(toolbar, 0 , wx.LEFT | wx.EXPAND) + else: + sizer.Add(toolbar, 0 , wx.LEFT | wx.EXPAND) + self._c['toolbar'].update() # update the axes menu on the toolbar + + def _set_color(self, rgbtuple=None): + """Set both figure and canvas colors to `rgbtuple`. + """ + if rgbtuple == None: rgbtuple = wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE).Get() - col = [c / 255.0 for c in rgbtuple] - self.figure.set_facecolor(col) - self.figure.set_edgecolor(col) - self.canvas.SetBackgroundColour(wx.Colour(*rgbtuple)) + col = [c/255.0 for c in rgbtuple] + self._c['figure'].set_facecolor(col) + self._c['figure'].set_edgecolor(col) + self._c['canvas'].SetBackgroundColour(wx.Colour(*rgbtuple)) - def SetStatusText(self, text, field=1): - self.Parent.Parent.statusbar.SetStatusText(text, field) + #def SetStatusText(self, text, field=1): + # self.Parent.Parent.statusbar.SetStatusText(text, field) - def OnClick(self, event): + def _on_size(self, event): + event.Skip() + wx.CallAfter(self._resize_canvas) + + def _on_click(self, event): #self.SetStatusText(str(event.xdata)) #print 'button=%d, x=%d, y=%d, xdata=%f, ydata=%f'%(event.button, event.x, event.y, event.xdata, event.ydata) pass - def OnEnterAxes(self, event): + def _on_enter_axes(self, event): self.display_coordinates = True - def OnLeaveAxes(self, event): + def _on_leave_axes(self, event): self.display_coordinates = False - self.SetStatusText('') + #self.SetStatusText('') - def OnMouseMove(self, event): + def _on_mouse_move(self, event): if event.guiEvent.m_shiftDown: - self.toolbar.set_cursor(2) - #print 'hand: ' + str(wx.CURSOR_HAND) - #print 'cross: ' + str(wx.CURSOR_CROSS) - #print 'ibeam: ' + str(wx.CURSOR_IBEAM) - #print 'wait: ' + str(wx.CURSOR_WAIT) - #print 'hourglass: ' + str(wx.HOURGLASS_CURSOR) + self._c['toolbar'].set_cursor(wx.CURSOR_RIGHT_ARROW) else: - self.toolbar.set_cursor(1) - - #axes = self.figure.axes[0] - #line, = axes.plot([event.x - 20 , event.x + 20], [event.y - 20, event.y + 20]) - - #line.figure.canvas.draw() + self._c['toolbar'].set_cursor(wx.CURSOR_ARROW) if self.display_coordinates: - coordinateString = ''.join(['x: ', str(event.xdata), ' y: ', str(event.ydata)]) + coordinateString = ''.join( + ['x: ', str(event.xdata), ' y: ', str(event.ydata)]) #TODO: pretty format - self.SetStatusText(coordinateString) + #self.SetStatusText(coordinateString) + + def _resize_canvas(self): + print 'resizing' + w,h = self.GetClientSize() + tw,th = self._c['toolbar'].GetSizeTuple() + dpi = float(self._c['figure'].get_dpi()) + self._c['figure'].set_figwidth(w/dpi) + self._c['figure'].set_figheight((h-th)/dpi) + self._c['canvas'].draw() + self.Refresh() def OnPaint(self, event): - self.canvas.draw() + print 'painting' + super(PlotPanel, self).OnPaint(event) + self._c['canvas'].draw() + + def set_curve(self, curve, config={}): + self._curve = curve + self.update(config=config) + + def update(self, config={}): + print 'updating' + x_format = config['plot x format'] + y_format = config['plot y format'] + zero = config['plot zero'] + + self._c['figure'].clear() + self._c['figure'].suptitle( + self._hooke_frame._file_name(self._curve.name), + fontsize=12) + axes = self._c['figure'].add_subplot(1, 1, 1) + +# if x_format != 'None': +# f = lib.curve.PrefixFormatter(curve.decimals.x, curve.prefix.x, use_zero) +# axes.xaxis.set_major_formatter(f) +# if y_format != 'None': +# f = lib.curve.PrefixFormatter(curve.decimals.y, curve.prefix.y, use_zero) +# axes.yaxis.set_major_formatter(f) + + x_name = 'z piezo (m)' + y_name = 'deflection (m)' + #axes.set_xlabel(x_name) + #axes.set_ylabel(y_name) + + self._c['figure'].hold(True) + for i,data in enumerate(self._curve.data): + axes.plot(data[:,data.info['columns'].index(x_name)], + data[:,data.info['columns'].index(y_name)], + '.', + label=data.info['name']) + if config['plot legend'] == 'True': # HACK: config should convert + axes.legend(loc='best') + self._c['canvas'].draw() diff --git a/hooke/ui/gui/panel/propertyeditor-propgrid.py b/hooke/ui/gui/panel/propertyeditor-propgrid.py new file mode 100644 index 0000000..edf0968 --- /dev/null +++ b/hooke/ui/gui/panel/propertyeditor-propgrid.py @@ -0,0 +1,500 @@ +# Copyright + +"""Property editor panel for Hooke. +""" + +import sys +import os.path + +import wx +import wx.propgrid as wxpg + +# There are many comments and code fragments in here from the demo app. +# They should come in handy to expand the functionality in the future. + +class Display (object): + property_descriptor = [] + def __init__(self): + pass + +class ValueObject (object): + def __init__(self): + pass + + +class IntProperty2 (wxpg.PyProperty): + """This is a simple re-implementation of wxIntProperty. + """ + def __init__(self, label, name = wxpg.LABEL_AS_NAME, value=0): + wxpg.PyProperty.__init__(self, label, name) + self.SetValue(value) + + def GetClassName(self): + return "IntProperty2" + + def GetEditor(self): + return "TextCtrl" + + def GetValueAsString(self, flags): + return str(self.GetValue()) + + def PyStringToValue(self, s, flags): + try: + v = int(s) + if self.GetValue() != v: + return v + except TypeError: + if flags & wxpg.PG_REPORT_ERROR: + wx.MessageBox("Cannot convert '%s' into a number."%s, "Error") + return False + + def PyIntToValue(self, v, flags): + if (self.GetValue() != v): + return v + + +class PyFilesProperty(wxpg.PyArrayStringProperty): + def __init__(self, label, name = wxpg.LABEL_AS_NAME, value=[]): + wxpg.PyArrayStringProperty.__init__(self, label, name, value) + self.SetValue(value) + + def OnSetValue(self, v): + self.value = v + self.display = ', '.join(self.value) + + def GetValueAsString(self, argFlags): + return self.display + + def PyStringToValue(self, s, flags): + return [a.strip() for a in s.split(',')] + + def OnEvent(self, propgrid, ctrl, event): + if event.GetEventType() == wx.wxEVT_COMMAND_BUTTON_CLICKED: + # Show dialog to select a string, call DoSetValue and + # return True, if value changed. + return True + + return False + + +class PyObjectPropertyValue: + """\ + Value type of our sample PyObjectProperty. We keep a simple dash-delimited + list of string given as argument to constructor. + """ + def __init__(self, s=None): + try: + self.ls = [a.strip() for a in s.split('-')] + except: + self.ls = [] + + def __repr__(self): + return ' - '.join(self.ls) + + +class PyObjectProperty(wxpg.PyProperty): + """\ + Another simple example. This time our value is a PyObject (NOTE: we can't + return an arbitrary python object in DoGetValue. It cannot be a simple + type such as int, bool, double, or string, nor an array or wxObject based. + Dictionary, None, or any user-specified Python object is allowed). + """ + def __init__(self, label, name = wxpg.LABEL_AS_NAME, value=None): + wxpg.PyProperty.__init__(self, label, name) + self.SetValue(value) + + def GetClassName(self): + return self.__class__.__name__ + + def GetEditor(self): + return "TextCtrl" + + def GetValueAsString(self, flags): + return repr(self.GetValue()) + + def PyStringToValue(self, s, flags): + return PyObjectPropertyValue(s) + + +class ShapeProperty(wxpg.PyEnumProperty): + """\ + Demonstrates use of OnCustomPaint method. + """ + def __init__(self, label, name = wxpg.LABEL_AS_NAME, value=-1): + wxpg.PyEnumProperty.__init__(self, label, name, ['Line','Circle','Rectangle'], [0,1,2], value) + + def OnMeasureImage(self, index): + return wxpg.DEFAULT_IMAGE_SIZE + + def OnCustomPaint(self, dc, rect, paint_data): + """\ + paint_data.m_choiceItem is -1 if we are painting the control, + in which case we need to get the drawn item using DoGetValue. + """ + item = paint_data.m_choiceItem + if item == -1: + item = self.DoGetValue() + + dc.SetPen(wx.Pen(wx.BLACK)) + dc.SetBrush(wx.Brush(wx.BLACK)) + + if item == 0: + dc.DrawLine(rect.x,rect.y,rect.x+rect.width,rect.y+rect.height) + elif item == 1: + half_width = rect.width / 2 + dc.DrawCircle(rect.x+half_width,rect.y+half_width,half_width-3) + elif item == 2: + dc.DrawRectangle(rect.x, rect.y, rect.width, rect.height) + + +class LargeImagePickerCtrl(wx.Window): + """\ + Control created and used by LargeImageEditor. + """ + def __init__(self): + pre = wx.PreWindow() + self.PostCreate(pre) + + def Create(self, parent, id_, pos, size, style = 0): + wx.Window.Create(self, parent, id_, pos, size, style | wx.BORDER_SIMPLE) + img_spc = size[1] + self.tc = wx.TextCtrl(self, -1, "", (img_spc,0), (2048,size[1]), wx.BORDER_NONE) + self.SetBackgroundColour(wx.WHITE) + self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM) + self.property = None + self.bmp = None + self.Bind(wx.EVT_PAINT, self.OnPaint) + + def OnPaint(self, event): + dc = wx.BufferedPaintDC(self) + + whiteBrush = wx.Brush(wx.WHITE) + dc.SetBackground(whiteBrush) + dc.Clear() + + bmp = self.bmp + if bmp: + dc.DrawBitmap(bmp, 2, 2) + else: + dc.SetPen(wx.Pen(wx.BLACK)) + dc.SetBrush(whiteBrush) + dc.DrawRectangle(2, 2, 64, 64) + + def RefreshThumbnail(self): + """\ + We use here very simple image scaling code. + """ + if not self.property: + self.bmp = None + return + + path = self.property.DoGetValue() + + if not os.path.isfile(path): + self.bmp = None + return + + image = wx.Image(path) + image.Rescale(64, 64) + self.bmp = wx.BitmapFromImage(image) + + def SetProperty(self, property): + self.property = property + self.tc.SetValue(property.GetDisplayedString()) + self.RefreshThumbnail() + + def SetValue(self, s): + self.RefreshThumbnail() + self.tc.SetValue(s) + + def GetLastPosition(self): + return self.tc.GetLastPosition() + + +class LargeImageEditor(wxpg.PyEditor): + """\ + Double-height text-editor with image in front. + """ + def __init__(self): + wxpg.PyEditor.__init__(self) + + def CreateControls(self, propgrid, property, pos, sz): + try: + h = 64 + 6 + x = propgrid.GetSplitterPosition() + x2 = propgrid.GetClientSize().x + bw = propgrid.GetRowHeight() + lipc = LargeImagePickerCtrl() + if sys.platform == 'win32': + lipc.Hide() + lipc.Create(propgrid, wxpg.PG_SUBID1, (x,pos[1]), (x2-x-bw,h)) + lipc.SetProperty(property) + # Hmmm.. how to have two-stage creation without subclassing? + #btn = wx.PreButton() + #pre = wx.PreWindow() + #self.PostCreate(pre) + #if sys.platform == 'win32': + # btn.Hide() + #btn.Create(propgrid, wxpg.PG_SUBID2, '...', (x2-bw,pos[1]), (bw,h), wx.WANTS_CHARS) + btn = wx.Button(propgrid, wxpg.PG_SUBID2, '...', (x2-bw,pos[1]), (bw,h), wx.WANTS_CHARS) + return (lipc, btn) + except: + import traceback + print traceback.print_exc() + + def UpdateControl(self, property, ctrl): + ctrl.SetValue(property.GetDisplayedString()) + + def DrawValue(self, dc, property, rect): + if not (property.GetFlags() & wxpg.PG_PROP_AUTO_UNSPECIFIED): + dc.DrawText( property.GetDisplayedString(), rect.x+5, rect.y ); + + def OnEvent(self, propgrid, ctrl, event): + if not ctrl: + return False + + evtType = event.GetEventType() + + if evtType == wx.wxEVT_COMMAND_TEXT_ENTER: + if propgrid.IsEditorsValueModified(): + return True + + elif evtType == wx.wxEVT_COMMAND_TEXT_UPDATED: + if not property.HasFlag(wxpg.PG_PROP_AUTO_UNSPECIFIED) or not ctrl or \ + ctrl.GetLastPosition() > 0: + + # We must check this since an 'empty' text event + # may be triggered when creating the property. + PG_FL_IN_SELECT_PROPERTY = 0x00100000 + if not (propgrid.GetInternalFlags() & PG_FL_IN_SELECT_PROPERTY): + event.Skip(); + event.SetId(propgrid.GetId()); + + propgrid.EditorsValueWasModified(); + + return False + + + def CopyValueFromControl(self, property, ctrl): + tc = ctrl.tc + res = property.SetValueFromString(tc.GetValue(),0) + # Changing unspecified always causes event (returning + # true here should be enough to trigger it). + if not res and property.IsFlagSet(wxpg.PG_PROP_AUTO_UNSPECIFIED): + res = True + + return res + + def SetValueToUnspecified(self, ctrl): + ctrl.tc.Remove(0,len(ctrl.tc.GetValue())); + + def SetControlStringValue(self, ctrl, txt): + ctrl.SetValue(txt) + + def OnFocus(self, property, ctrl): + ctrl.tc.SetSelection(-1,-1) + ctrl.tc.SetFocus() + + +class PropertyEditor(wx.Panel): + + def __init__(self, parent): + # Use the WANTS_CHARS style so the panel doesn't eat the Return key. + wx.Panel.__init__(self, parent, -1, style=wx.WANTS_CHARS, size=(160, 200)) + + sizer = wx.BoxSizer(wx.VERTICAL) + + self.pg = wxpg.PropertyGrid(self, style=wxpg.PG_SPLITTER_AUTO_CENTER|wxpg.PG_AUTO_SORT) + + # Show help as tooltips + self.pg.SetExtraStyle(wxpg.PG_EX_HELP_AS_TOOLTIPS) + + #pg.Bind(wxpg.EVT_PG_CHANGED, self.OnPropGridChange) + #pg.Bind(wxpg.EVT_PG_SELECTED, self.OnPropGridSelect) + #self.pg.Bind(wxpg.EVT_PG_RIGHT_CLICK, self.OnPropGridRightClick) + + # Needed by custom image editor + wx.InitAllImageHandlers() + + # + # Let's create a simple custom editor + # + # NOTE: Editor must be registered *before* adding a property that uses it. + self.pg.RegisterEditor(LargeImageEditor) + + ''' + # + # Add properties + # + + pg.Append( wxpg.PropertyCategory("1 - Basic Properties") ) + pg.Append( wxpg.StringProperty("String",value="Some Text") ) + pg.Append( wxpg.IntProperty("Int",value=100) ) + pg.Append( wxpg.FloatProperty("Float",value=100.0) ) + pg.Append( wxpg.BoolProperty("Bool",value=True) ) + pg.Append( wxpg.BoolProperty("Bool_with_Checkbox",value=True) ) + pg.SetPropertyAttribute("Bool_with_Checkbox", "UseCheckbox", True) + + pg.Append( wxpg.PropertyCategory("2 - More Properties") ) + pg.Append( wxpg.LongStringProperty("LongString",value="This is a\\nmulti-line string\\nwith\\ttabs\\nmixed\\tin.") ) + pg.Append( wxpg.DirProperty("Dir",value="C:\\Windows") ) + pg.Append( wxpg.FileProperty("File",value="C:\\Windows\\system.ini") ) + pg.Append( wxpg.ArrayStringProperty("ArrayString",value=['A','B','C']) ) + + pg.Append( wxpg.EnumProperty("Enum","Enum", + ['wxPython Rules','wxPython Rocks','wxPython Is The Best'], + [10,11,12],0) ) + pg.Append( wxpg.EditEnumProperty("EditEnum","EditEnumProperty",['A','B','C'],[0,1,2],"Text Not in List") ) + + pg.Append( wxpg.PropertyCategory("3 - Advanced Properties") ) + pg.Append( wxpg.DateProperty("Date",value=wx.DateTime_Now()) ) + pg.Append( wxpg.FontProperty("Font",value=self.GetFont()) ) + pg.Append( wxpg.ColourProperty("Colour",value=self.GetBackgroundColour()) ) + pg.Append( wxpg.SystemColourProperty("SystemColour") ) + pg.Append( wxpg.ImageFileProperty("ImageFile") ) + pg.Append( wxpg.MultiChoiceProperty("MultiChoice",choices=['wxWidgets','QT','GTK+']) ) + + pg.Append( wxpg.PropertyCategory("4 - Additional Properties") ) + pg.Append( wxpg.PointProperty("Point",value=self.GetPosition()) ) + pg.Append( wxpg.SizeProperty("Size",value=self.GetSize()) ) + pg.Append( wxpg.FontDataProperty("FontData") ) + pg.Append( wxpg.IntProperty("IntWithSpin",value=256) ) + pg.SetPropertyEditor("IntWithSpin","SpinCtrl") + pg.Append( wxpg.DirsProperty("Dirs",value=['C:/Lib','C:/Bin']) ) + pg.SetPropertyHelpString( "String", "String Property help string!" ) + pg.SetPropertyHelpString( "Dirs", "Dirs Property help string!" ) + + pg.SetPropertyAttribute( "File", wxpg.PG_FILE_SHOW_FULL_PATH, 0 ) + pg.SetPropertyAttribute( "File", wxpg.PG_FILE_INITIAL_PATH, "C:\\Program Files\\Internet Explorer" ) + pg.SetPropertyAttribute( "Date", wxpg.PG_DATE_PICKER_STYLE, wx.DP_DROPDOWN|wx.DP_SHOWCENTURY ) + + pg.Append( wxpg.PropertyCategory("5 - Custom Properties") ) + pg.Append( IntProperty2("IntProperty2", value=1024) ) + + pg.Append( ShapeProperty("ShapeProperty", value=0) ) + pg.Append( PyObjectProperty("PyObjectProperty") ) + + pg.Append( wxpg.ImageFileProperty("ImageFileWithLargeEditor") ) + pg.SetPropertyEditor("ImageFileWithLargeEditor", "LargeImageEditor") + + + pg.SetPropertyClientData( "Point", 1234 ) + if pg.GetPropertyClientData( "Point" ) != 1234: + raise ValueError("Set/GetPropertyClientData() failed") + + # Test setting unicode string + pg.GetPropertyByName("String").SetValue(u"Some Unicode Text") + + # + # Test some code that *should* fail (but not crash) + #try: + #a_ = pg.GetPropertyValue( "NotARealProperty" ) + #pg.EnableProperty( "NotAtAllRealProperty", False ) + #pg.SetPropertyHelpString( "AgaintNotARealProperty", "Dummy Help String" ) + #except: + #pass + #raise + + ''' + sizer.Add(self.pg, 1, wx.EXPAND) + self.SetSizer(sizer) + sizer.SetSizeHints(self) + + self.SelectedTreeItem = None + + def GetPropertyValues(self): + return self.pg.GetPropertyValues() + + def Initialize(self, properties): + pg = self.pg + pg.Clear() + + if properties: + for element in properties: + if element[1]['type'] == 'arraystring': + elements = element[1]['elements'] + if 'value' in element[1]: + property_value = element[1]['value'] + else: + property_value = element[1]['default'] + #retrieve individual strings + property_value = split(property_value, ' ') + #remove " delimiters + values = [value.strip('"') for value in property_value] + pg.Append(wxpg.ArrayStringProperty(element[0], value=values)) + + if element[1]['type'] == 'boolean': + if 'value' in element[1]: + property_value = element[1].as_bool('value') + else: + property_value = element[1].as_bool('default') + property_control = wxpg.BoolProperty(element[0], value=property_value) + pg.Append(property_control) + pg.SetPropertyAttribute(element[0], 'UseCheckbox', True) + + #if element[0] == 'category': + #pg.Append(wxpg.PropertyCategory(element[1])) + + if element[1]['type'] == 'color': + if 'value' in element[1]: + property_value = element[1]['value'] + else: + property_value = element[1]['default'] + property_value = eval(property_value) + pg.Append(wxpg.ColourProperty(element[0], value=property_value)) + + if element[1]['type'] == 'enum': + elements = element[1]['elements'] + if 'value' in element[1]: + property_value = element[1]['value'] + else: + property_value = element[1]['default'] + pg.Append(wxpg.EnumProperty(element[0], element[0], elements, [], elements.index(property_value))) + + if element[1]['type'] == 'filename': + if 'value' in element[1]: + property_value = element[1]['value'] + else: + property_value = element[1]['default'] + pg.Append(wxpg.FileProperty(element[0], value=property_value)) + + if element[1]['type'] == 'float': + if 'value' in element[1]: + property_value = element[1].as_float('value') + else: + property_value = element[1].as_float('default') + property_control = wxpg.FloatProperty(element[0], value=property_value) + pg.Append(property_control) + + if element[1]['type'] == 'folder': + if 'value' in element[1]: + property_value = element[1]['value'] + else: + property_value = element[1]['default'] + pg.Append(wxpg.DirProperty(element[0], value=property_value)) + + if element[1]['type'] == 'integer': + if 'value' in element[1]: + property_value = element[1].as_int('value') + else: + property_value = element[1].as_int('default') + property_control = wxpg.IntProperty(element[0], value=property_value) + if 'maximum' in element[1]: + property_control.SetAttribute('Max', element[1].as_int('maximum')) + if 'minimum' in element[1]: + property_control.SetAttribute('Min', element[1].as_int('minimum')) + property_control.SetAttribute('Wrap', True) + pg.Append(property_control) + pg.SetPropertyEditor(element[0], 'SpinCtrl') + + if element[1]['type'] == 'string': + if 'value' in element[1]: + property_value = element[1]['value'] + else: + property_value = element[1]['default'] + pg.Append(wxpg.StringProperty(element[0], value=property_value)) + + pg.Refresh() + + def OnReserved(self, event): + pass diff --git a/hooke/ui/gui/panel/propertyeditor.py b/hooke/ui/gui/panel/propertyeditor.py index edf0968..2d22e45 100644 --- a/hooke/ui/gui/panel/propertyeditor.py +++ b/hooke/ui/gui/panel/propertyeditor.py @@ -1,500 +1,277 @@ # Copyright """Property editor panel for Hooke. -""" -import sys -import os.path +wxPropertyGrid is `included in wxPython >= 2.9.1 `_. Until +then, we'll avoid it because of the *nix build problems. -import wx -import wx.propgrid as wxpg +This module hacks together a workaround to be used until 2.9.1 is +widely installed (or at least released ;). -# There are many comments and code fragments in here from the demo app. -# They should come in handy to expand the functionality in the future. +.. _included: + http://wxpropgrid.sourceforge.net/cgi-bin/index?page=download +""" -class Display (object): - property_descriptor = [] - def __init__(self): - pass +import wx.grid -class ValueObject (object): - def __init__(self): - pass +from . import Panel -class IntProperty2 (wxpg.PyProperty): - """This is a simple re-implementation of wxIntProperty. +def prop_from_argument(argument, curves=None, playlists=None): + """Convert a :class:`~hooke.command.Argument` to a :class:`Property`. """ - def __init__(self, label, name = wxpg.LABEL_AS_NAME, value=0): - wxpg.PyProperty.__init__(self, label, name) - self.SetValue(value) + type = argument.type + if type in ['driver']: # intentionally not handled (yet) + return None + if argument.count != 1: + raise NotImplementedError(argument) + kwargs = { + 'label':argument.name, + 'default':argument.default, + 'help':argument.help(), + } + # type consolidation + if type == 'file': + type = 'path' + # type handling + if type in ['string', 'bool', 'int', 'float', 'path']: + _class = globals()['%sProperty' % type.capitalize()] + return _class(**kwargs) + elif type in ['curve', 'playlist']: + if type == 'curve': + choices = curves # extracted from the current playlist + else: + choices = playlists + return ChoiceProperty(choices=choices, **kwargs) + raise NotImplementedError(argument.type) - def GetClassName(self): - return "IntProperty2" +def prop_from_setting(setting): + """Convert a :class:`~hooke.config.Setting` to a :class:`Property`. + """ + raise NotImplementedError() - def GetEditor(self): - return "TextCtrl" - def GetValueAsString(self, flags): - return str(self.GetValue()) +class Property (object): + def __init__(self, type, label, default, help=None): + self.type = type + self.label = label + self.default = default + self.help = help - def PyStringToValue(self, s, flags): - try: - v = int(s) - if self.GetValue() != v: - return v - except TypeError: - if flags & wxpg.PG_REPORT_ERROR: - wx.MessageBox("Cannot convert '%s' into a number."%s, "Error") - return False + def get_editor(self): + """Return a suitable grid editor. + """ + raise NotImplementedError() - def PyIntToValue(self, v, flags): - if (self.GetValue() != v): - return v + def get_renderer(self): + """Return a suitable grid renderer. + Returns `None` if no special renderer is required. + """ + return None -class PyFilesProperty(wxpg.PyArrayStringProperty): - def __init__(self, label, name = wxpg.LABEL_AS_NAME, value=[]): - wxpg.PyArrayStringProperty.__init__(self, label, name, value) - self.SetValue(value) + def string_for_value(self, value): + """Return a string representation of `value` for loading the table. + """ + return str(value) - def OnSetValue(self, v): - self.value = v - self.display = ', '.join(self.value) + def value_for_string(self, string): + """Return the value represented by `string`. + """ + return string - def GetValueAsString(self, argFlags): - return self.display - def PyStringToValue(self, s, flags): - return [a.strip() for a in s.split(',')] +class StringProperty (Property): + def __init__(self, **kwargs): + assert 'type' not in kwargs, kwargs + if 'default' not in kwargs: + kwargs['default'] = 0 + super(StringProperty, self).__init__(type='string', **kwargs) - def OnEvent(self, propgrid, ctrl, event): - if event.GetEventType() == wx.wxEVT_COMMAND_BUTTON_CLICKED: - # Show dialog to select a string, call DoSetValue and - # return True, if value changed. - return True + def get_editor(self): + return wx.grid.GridCellTextEditor() - return False + def get_renderer(self): + return wx.grid.GridCellStringRenderer() -class PyObjectPropertyValue: - """\ - Value type of our sample PyObjectProperty. We keep a simple dash-delimited - list of string given as argument to constructor. - """ - def __init__(self, s=None): - try: - self.ls = [a.strip() for a in s.split('-')] - except: - self.ls = [] - - def __repr__(self): - return ' - '.join(self.ls) - - -class PyObjectProperty(wxpg.PyProperty): - """\ - Another simple example. This time our value is a PyObject (NOTE: we can't - return an arbitrary python object in DoGetValue. It cannot be a simple - type such as int, bool, double, or string, nor an array or wxObject based. - Dictionary, None, or any user-specified Python object is allowed). +class BoolProperty (Property): + """A boolean property. + + Notes + ----- + Unfortunately, changing a boolean property takes two clicks: + + 1) create the editor + 2) change the value + + There are `ways around this`_, but it's not pretty. + + .. _ways around this: + http://wiki.wxpython.org/Change%20wxGrid%20CheckBox%20with%20one%20click """ - def __init__(self, label, name = wxpg.LABEL_AS_NAME, value=None): - wxpg.PyProperty.__init__(self, label, name) - self.SetValue(value) + def __init__(self, **kwargs): + assert 'type' not in kwargs, kwargs + if 'default' not in kwargs: + kwargs['default'] = True + super(BoolProperty, self).__init__(type='bool', **kwargs) - def GetClassName(self): - return self.__class__.__name__ + def get_editor(self): + return wx.grid.GridCellBoolEditor() - def GetEditor(self): - return "TextCtrl" + def get_renderer(self): + return wx.grid.GridCellBoolRenderer() - def GetValueAsString(self, flags): - return repr(self.GetValue()) + def string_for_value(self, value): + if value == True: + return '1' + return '' - def PyStringToValue(self, s, flags): - return PyObjectPropertyValue(s) + def value_for_string(self, string): + return string == '1' -class ShapeProperty(wxpg.PyEnumProperty): - """\ - Demonstrates use of OnCustomPaint method. - """ - def __init__(self, label, name = wxpg.LABEL_AS_NAME, value=-1): - wxpg.PyEnumProperty.__init__(self, label, name, ['Line','Circle','Rectangle'], [0,1,2], value) +class IntProperty (Property): + def __init__(self, **kwargs): + assert 'type' not in kwargs, kwargs + if 'default' not in kwargs: + kwargs['default'] = 0 + super(IntProperty, self).__init__(type='int', **kwargs) - def OnMeasureImage(self, index): - return wxpg.DEFAULT_IMAGE_SIZE + def get_editor(self): + return wx.grid.GridCellNumberEditor() - def OnCustomPaint(self, dc, rect, paint_data): - """\ - paint_data.m_choiceItem is -1 if we are painting the control, - in which case we need to get the drawn item using DoGetValue. - """ - item = paint_data.m_choiceItem - if item == -1: - item = self.DoGetValue() + def get_renderer(self): + return wx.grid.GridCellNumberRenderer() - dc.SetPen(wx.Pen(wx.BLACK)) - dc.SetBrush(wx.Brush(wx.BLACK)) + def value_for_string(self, string): + return int(string) - if item == 0: - dc.DrawLine(rect.x,rect.y,rect.x+rect.width,rect.y+rect.height) - elif item == 1: - half_width = rect.width / 2 - dc.DrawCircle(rect.x+half_width,rect.y+half_width,half_width-3) - elif item == 2: - dc.DrawRectangle(rect.x, rect.y, rect.width, rect.height) +class FloatProperty (Property): + def __init__(self, **kwargs): + assert 'type' not in kwargs, kwargs + if 'default' not in kwargs: + kwargs['default'] = 0.0 + super(FloatProperty, self).__init__(type='float', **kwargs) -class LargeImagePickerCtrl(wx.Window): - """\ - Control created and used by LargeImageEditor. - """ - def __init__(self): - pre = wx.PreWindow() - self.PostCreate(pre) - - def Create(self, parent, id_, pos, size, style = 0): - wx.Window.Create(self, parent, id_, pos, size, style | wx.BORDER_SIMPLE) - img_spc = size[1] - self.tc = wx.TextCtrl(self, -1, "", (img_spc,0), (2048,size[1]), wx.BORDER_NONE) - self.SetBackgroundColour(wx.WHITE) - self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM) - self.property = None - self.bmp = None - self.Bind(wx.EVT_PAINT, self.OnPaint) - - def OnPaint(self, event): - dc = wx.BufferedPaintDC(self) - - whiteBrush = wx.Brush(wx.WHITE) - dc.SetBackground(whiteBrush) - dc.Clear() - - bmp = self.bmp - if bmp: - dc.DrawBitmap(bmp, 2, 2) + def get_editor(self): + return wx.grid.GridCellFloatEditor() + + def get_renderer(self): + return wx.grid.GridCellFloatRenderer() + + def value_for_string(self, string): + return float(string) + + +class ChoiceProperty (Property): + def __init__(self, choices, **kwargs): + assert 'type' not in kwargs, kwargs + if 'default' in kwargs: + if kwargs['default'] not in choices: + choices.insert(0, kwargs['default']) else: - dc.SetPen(wx.Pen(wx.BLACK)) - dc.SetBrush(whiteBrush) - dc.DrawRectangle(2, 2, 64, 64) + kwargs['default'] = choices[0] + super(ChoiceProperty, self).__init__(type='choice', **kwargs) + self._choices = choices - def RefreshThumbnail(self): - """\ - We use here very simple image scaling code. - """ - if not self.property: - self.bmp = None - return + def get_editor(self): + choices = [self.string_for_value(c) for c in self._choices] + return wx.grid.GridCellChoiceEditor(choices=choices) - path = self.property.DoGetValue() + def get_renderer(self): + return None + #return wx.grid.GridCellChoiceRenderer() - if not os.path.isfile(path): - self.bmp = None - return + def string_for_value(self, value): + if hasattr(value, 'name'): + return value.name + return str(value) - image = wx.Image(path) - image.Rescale(64, 64) - self.bmp = wx.BitmapFromImage(image) + def value_for_string(self, string): + for choice in self._choices: + if self.string_for_value(choice) == string: + return choice + raise ValueError(string) - def SetProperty(self, property): - self.property = property - self.tc.SetValue(property.GetDisplayedString()) - self.RefreshThumbnail() - def SetValue(self, s): - self.RefreshThumbnail() - self.tc.SetValue(s) +class PathProperty (StringProperty): + """Simple file or path property. - def GetLastPosition(self): - return self.tc.GetLastPosition() + Currently there isn't a fancy file-picker popup. Perhaps in the + future. + """ + def __init__(self, **kwargs): + super(PathProperty, self).__init__(**kwargs) + self.type = 'path' -class LargeImageEditor(wxpg.PyEditor): - """\ - Double-height text-editor with image in front. +class PropertyPanel(Panel, wx.grid.Grid): + """UI to view/set config values and command argsuments. """ - def __init__(self): - wxpg.PyEditor.__init__(self) + def __init__(self, callbacks=None, **kwargs): + super(PropertyPanel, self).__init__( + name='property editor', callbacks=callbacks, **kwargs) + self._properties = [] + + self.CreateGrid(numRows=0, numCols=1) + self.SetColLabelValue(0, 'value') - def CreateControls(self, propgrid, property, pos, sz): - try: - h = 64 + 6 - x = propgrid.GetSplitterPosition() - x2 = propgrid.GetClientSize().x - bw = propgrid.GetRowHeight() - lipc = LargeImagePickerCtrl() - if sys.platform == 'win32': - lipc.Hide() - lipc.Create(propgrid, wxpg.PG_SUBID1, (x,pos[1]), (x2-x-bw,h)) - lipc.SetProperty(property) - # Hmmm.. how to have two-stage creation without subclassing? - #btn = wx.PreButton() - #pre = wx.PreWindow() - #self.PostCreate(pre) - #if sys.platform == 'win32': - # btn.Hide() - #btn.Create(propgrid, wxpg.PG_SUBID2, '...', (x2-bw,pos[1]), (bw,h), wx.WANTS_CHARS) - btn = wx.Button(propgrid, wxpg.PG_SUBID2, '...', (x2-bw,pos[1]), (bw,h), wx.WANTS_CHARS) - return (lipc, btn) - except: - import traceback - print traceback.print_exc() - - def UpdateControl(self, property, ctrl): - ctrl.SetValue(property.GetDisplayedString()) - - def DrawValue(self, dc, property, rect): - if not (property.GetFlags() & wxpg.PG_PROP_AUTO_UNSPECIFIED): - dc.DrawText( property.GetDisplayedString(), rect.x+5, rect.y ); - - def OnEvent(self, propgrid, ctrl, event): - if not ctrl: - return False - - evtType = event.GetEventType() - - if evtType == wx.wxEVT_COMMAND_TEXT_ENTER: - if propgrid.IsEditorsValueModified(): - return True - - elif evtType == wx.wxEVT_COMMAND_TEXT_UPDATED: - if not property.HasFlag(wxpg.PG_PROP_AUTO_UNSPECIFIED) or not ctrl or \ - ctrl.GetLastPosition() > 0: - - # We must check this since an 'empty' text event - # may be triggered when creating the property. - PG_FL_IN_SELECT_PROPERTY = 0x00100000 - if not (propgrid.GetInternalFlags() & PG_FL_IN_SELECT_PROPERTY): - event.Skip(); - event.SetId(propgrid.GetId()); - - propgrid.EditorsValueWasModified(); - - return False - - - def CopyValueFromControl(self, property, ctrl): - tc = ctrl.tc - res = property.SetValueFromString(tc.GetValue(),0) - # Changing unspecified always causes event (returning - # true here should be enough to trigger it). - if not res and property.IsFlagSet(wxpg.PG_PROP_AUTO_UNSPECIFIED): - res = True - - return res - - def SetValueToUnspecified(self, ctrl): - ctrl.tc.Remove(0,len(ctrl.tc.GetValue())); - - def SetControlStringValue(self, ctrl, txt): - ctrl.SetValue(txt) - - def OnFocus(self, property, ctrl): - ctrl.tc.SetSelection(-1,-1) - ctrl.tc.SetFocus() - - -class PropertyEditor(wx.Panel): - - def __init__(self, parent): - # Use the WANTS_CHARS style so the panel doesn't eat the Return key. - wx.Panel.__init__(self, parent, -1, style=wx.WANTS_CHARS, size=(160, 200)) - - sizer = wx.BoxSizer(wx.VERTICAL) - - self.pg = wxpg.PropertyGrid(self, style=wxpg.PG_SPLITTER_AUTO_CENTER|wxpg.PG_AUTO_SORT) - - # Show help as tooltips - self.pg.SetExtraStyle(wxpg.PG_EX_HELP_AS_TOOLTIPS) - - #pg.Bind(wxpg.EVT_PG_CHANGED, self.OnPropGridChange) - #pg.Bind(wxpg.EVT_PG_SELECTED, self.OnPropGridSelect) - #self.pg.Bind(wxpg.EVT_PG_RIGHT_CLICK, self.OnPropGridRightClick) - - # Needed by custom image editor - wx.InitAllImageHandlers() - - # - # Let's create a simple custom editor - # - # NOTE: Editor must be registered *before* adding a property that uses it. - self.pg.RegisterEditor(LargeImageEditor) - - ''' - # - # Add properties - # - - pg.Append( wxpg.PropertyCategory("1 - Basic Properties") ) - pg.Append( wxpg.StringProperty("String",value="Some Text") ) - pg.Append( wxpg.IntProperty("Int",value=100) ) - pg.Append( wxpg.FloatProperty("Float",value=100.0) ) - pg.Append( wxpg.BoolProperty("Bool",value=True) ) - pg.Append( wxpg.BoolProperty("Bool_with_Checkbox",value=True) ) - pg.SetPropertyAttribute("Bool_with_Checkbox", "UseCheckbox", True) - - pg.Append( wxpg.PropertyCategory("2 - More Properties") ) - pg.Append( wxpg.LongStringProperty("LongString",value="This is a\\nmulti-line string\\nwith\\ttabs\\nmixed\\tin.") ) - pg.Append( wxpg.DirProperty("Dir",value="C:\\Windows") ) - pg.Append( wxpg.FileProperty("File",value="C:\\Windows\\system.ini") ) - pg.Append( wxpg.ArrayStringProperty("ArrayString",value=['A','B','C']) ) - - pg.Append( wxpg.EnumProperty("Enum","Enum", - ['wxPython Rules','wxPython Rocks','wxPython Is The Best'], - [10,11,12],0) ) - pg.Append( wxpg.EditEnumProperty("EditEnum","EditEnumProperty",['A','B','C'],[0,1,2],"Text Not in List") ) - - pg.Append( wxpg.PropertyCategory("3 - Advanced Properties") ) - pg.Append( wxpg.DateProperty("Date",value=wx.DateTime_Now()) ) - pg.Append( wxpg.FontProperty("Font",value=self.GetFont()) ) - pg.Append( wxpg.ColourProperty("Colour",value=self.GetBackgroundColour()) ) - pg.Append( wxpg.SystemColourProperty("SystemColour") ) - pg.Append( wxpg.ImageFileProperty("ImageFile") ) - pg.Append( wxpg.MultiChoiceProperty("MultiChoice",choices=['wxWidgets','QT','GTK+']) ) - - pg.Append( wxpg.PropertyCategory("4 - Additional Properties") ) - pg.Append( wxpg.PointProperty("Point",value=self.GetPosition()) ) - pg.Append( wxpg.SizeProperty("Size",value=self.GetSize()) ) - pg.Append( wxpg.FontDataProperty("FontData") ) - pg.Append( wxpg.IntProperty("IntWithSpin",value=256) ) - pg.SetPropertyEditor("IntWithSpin","SpinCtrl") - pg.Append( wxpg.DirsProperty("Dirs",value=['C:/Lib','C:/Bin']) ) - pg.SetPropertyHelpString( "String", "String Property help string!" ) - pg.SetPropertyHelpString( "Dirs", "Dirs Property help string!" ) - - pg.SetPropertyAttribute( "File", wxpg.PG_FILE_SHOW_FULL_PATH, 0 ) - pg.SetPropertyAttribute( "File", wxpg.PG_FILE_INITIAL_PATH, "C:\\Program Files\\Internet Explorer" ) - pg.SetPropertyAttribute( "Date", wxpg.PG_DATE_PICKER_STYLE, wx.DP_DROPDOWN|wx.DP_SHOWCENTURY ) - - pg.Append( wxpg.PropertyCategory("5 - Custom Properties") ) - pg.Append( IntProperty2("IntProperty2", value=1024) ) - - pg.Append( ShapeProperty("ShapeProperty", value=0) ) - pg.Append( PyObjectProperty("PyObjectProperty") ) - - pg.Append( wxpg.ImageFileProperty("ImageFileWithLargeEditor") ) - pg.SetPropertyEditor("ImageFileWithLargeEditor", "LargeImageEditor") - - - pg.SetPropertyClientData( "Point", 1234 ) - if pg.GetPropertyClientData( "Point" ) != 1234: - raise ValueError("Set/GetPropertyClientData() failed") - - # Test setting unicode string - pg.GetPropertyByName("String").SetValue(u"Some Unicode Text") - - # - # Test some code that *should* fail (but not crash) - #try: - #a_ = pg.GetPropertyValue( "NotARealProperty" ) - #pg.EnableProperty( "NotAtAllRealProperty", False ) - #pg.SetPropertyHelpString( "AgaintNotARealProperty", "Dummy Help String" ) - #except: - #pass - #raise - - ''' - sizer.Add(self.pg, 1, wx.EXPAND) - self.SetSizer(sizer) - sizer.SetSizeHints(self) - - self.SelectedTreeItem = None - - def GetPropertyValues(self): - return self.pg.GetPropertyValues() - - def Initialize(self, properties): - pg = self.pg - pg.Clear() - - if properties: - for element in properties: - if element[1]['type'] == 'arraystring': - elements = element[1]['elements'] - if 'value' in element[1]: - property_value = element[1]['value'] - else: - property_value = element[1]['default'] - #retrieve individual strings - property_value = split(property_value, ' ') - #remove " delimiters - values = [value.strip('"') for value in property_value] - pg.Append(wxpg.ArrayStringProperty(element[0], value=values)) - - if element[1]['type'] == 'boolean': - if 'value' in element[1]: - property_value = element[1].as_bool('value') - else: - property_value = element[1].as_bool('default') - property_control = wxpg.BoolProperty(element[0], value=property_value) - pg.Append(property_control) - pg.SetPropertyAttribute(element[0], 'UseCheckbox', True) - - #if element[0] == 'category': - #pg.Append(wxpg.PropertyCategory(element[1])) - - if element[1]['type'] == 'color': - if 'value' in element[1]: - property_value = element[1]['value'] - else: - property_value = element[1]['default'] - property_value = eval(property_value) - pg.Append(wxpg.ColourProperty(element[0], value=property_value)) - - if element[1]['type'] == 'enum': - elements = element[1]['elements'] - if 'value' in element[1]: - property_value = element[1]['value'] - else: - property_value = element[1]['default'] - pg.Append(wxpg.EnumProperty(element[0], element[0], elements, [], elements.index(property_value))) - - if element[1]['type'] == 'filename': - if 'value' in element[1]: - property_value = element[1]['value'] - else: - property_value = element[1]['default'] - pg.Append(wxpg.FileProperty(element[0], value=property_value)) - - if element[1]['type'] == 'float': - if 'value' in element[1]: - property_value = element[1].as_float('value') - else: - property_value = element[1].as_float('default') - property_control = wxpg.FloatProperty(element[0], value=property_value) - pg.Append(property_control) - - if element[1]['type'] == 'folder': - if 'value' in element[1]: - property_value = element[1]['value'] - else: - property_value = element[1]['default'] - pg.Append(wxpg.DirProperty(element[0], value=property_value)) - - if element[1]['type'] == 'integer': - if 'value' in element[1]: - property_value = element[1].as_int('value') - else: - property_value = element[1].as_int('default') - property_control = wxpg.IntProperty(element[0], value=property_value) - if 'maximum' in element[1]: - property_control.SetAttribute('Max', element[1].as_int('maximum')) - if 'minimum' in element[1]: - property_control.SetAttribute('Min', element[1].as_int('minimum')) - property_control.SetAttribute('Wrap', True) - pg.Append(property_control) - pg.SetPropertyEditor(element[0], 'SpinCtrl') - - if element[1]['type'] == 'string': - if 'value' in element[1]: - property_value = element[1]['value'] - else: - property_value = element[1]['default'] - pg.Append(wxpg.StringProperty(element[0], value=property_value)) - - pg.Refresh() - - def OnReserved(self, event): - pass + self._last_tooltip = None + self.GetGridWindow().Bind(wx.EVT_MOTION, self._on_mouse_over) + + def _on_mouse_over(self, event): + """Enable tooltips. + """ + x,y = self.CalcUnscrolledPosition(event.GetPosition()) + col,row = self.XYToCell(x, y) + if col == -1 or row == -1: + msg = '' + else: + msg = self._properties[row].help or '' + if msg != self._last_tooltip: + self._last_tooltip = msg + event.GetEventObject().SetToolTipString(msg) + + def append_property(self, property): + if len([p for p in self._properties if p.label == property.label]) > 0: + raise ValueError(property) # property.label collision + self._properties.append(property) + row = len(self._properties) - 1 + self.AppendRows(numRows=1) + self.SetRowLabelValue(row, property.label) + self.SetCellEditor(row=row, col=0, editor=property.get_editor()) + r = property.get_renderer() + if r != None: + self.SetCellRenderer(row=row, col=0, renderer=r) + self.set_property(property.label, property.default) + + def remove_property(self, label): + row,property = self._property_by_label(label) + self._properties.pop(row) + self.DeleteRows(pos=row) + + def clear(self): + while(len(self._properties) > 0): + self.remove_property(self._properties[-1].label) + + def set_property(self, label, value): + row,property = self._property_by_label(label) + self.SetCellValue(row=row, col=0, s=property.string_for_value(value)) + + def get_property(self, label): + row,property = self._property_by_label(label) + string = self.GetCellValue(row=row, col=0) + return property.value_for_string(string) + + def get_values(self): + return dict([(p.label, self.get_property(p.label)) + for p in self._properties]) + + def _property_by_label(self, label): + props = [(i,p) for i,p in enumerate(self._properties) + if p.label == label] + assert len(props) == 1, props + row,property = props[0] + return (row, property) diff --git a/hooke/ui/gui/panel/propertyeditor2.py b/hooke/ui/gui/panel/propertyeditor2.py deleted file mode 100644 index 759441e..0000000 --- a/hooke/ui/gui/panel/propertyeditor2.py +++ /dev/null @@ -1,277 +0,0 @@ -# Copyright - -"""Property editor panel for Hooke. - -wxPropertyGrid is `included in wxPython >= 2.9.1 `_. Until -then, we'll avoid it because of the *nix build problems. - -This module hacks together a workaround to be used until 2.9.1 is -widely installed (or at least released ;). - -.. _included: - http://wxpropgrid.sourceforge.net/cgi-bin/index?page=download -""" - -import wx.grid - -from . import Panel - - -def prop_from_argument(argument, curves=None, playlists=None): - """Convert a :class:`~hooke.command.Argument` to a :class:`Property`. - """ - type = argument.type - if type in ['driver']: # intentionally not handled (yet) - return None - if argument.count != 1: - raise NotImplementedError(argument) - kwargs = { - 'label':argument.name, - 'default':argument.default, - 'help':argument.help(), - } - # type consolidation - if type == 'file': - type = 'path' - # type handling - if type in ['string', 'bool', 'int', 'float', 'path']: - _class = globals()['%sProperty' % type.capitalize()] - return _class(**kwargs) - elif type in ['curve', 'playlist']: - if type == 'curve': - choices = curves # extracted from the current playlist - else: - choices = playlists - return ChoiceProperty(choices=choices, **kwargs) - raise NotImplementedError(argument.type) - -def prop_from_setting(setting): - """Convert a :class:`~hooke.config.Setting` to a :class:`Property`. - """ - raise NotImplementedError() - - -class Property (object): - def __init__(self, type, label, default, help=None): - self.type = type - self.label = label - self.default = default - self.help = help - - def get_editor(self): - """Return a suitable grid editor. - """ - raise NotImplementedError() - - def get_renderer(self): - """Return a suitable grid renderer. - - Returns `None` if no special renderer is required. - """ - return None - - def string_for_value(self, value): - """Return a string representation of `value` for loading the table. - """ - return str(value) - - def value_for_string(self, string): - """Return the value represented by `string`. - """ - return string - - -class StringProperty (Property): - def __init__(self, **kwargs): - assert 'type' not in kwargs, kwargs - if 'default' not in kwargs: - kwargs['default'] = 0 - super(StringProperty, self).__init__(type='string', **kwargs) - - def get_editor(self): - return wx.grid.GridCellTextEditor() - - def get_renderer(self): - return wx.grid.GridCellStringRenderer() - - -class BoolProperty (Property): - """A boolean property. - - Notes - ----- - Unfortunately, changing a boolean property takes two clicks: - - 1) create the editor - 2) change the value - - There are `ways around this`_, but it's not pretty. - - .. _ways around this: - http://wiki.wxpython.org/Change%20wxGrid%20CheckBox%20with%20one%20click - """ - def __init__(self, **kwargs): - assert 'type' not in kwargs, kwargs - if 'default' not in kwargs: - kwargs['default'] = True - super(BoolProperty, self).__init__(type='bool', **kwargs) - - def get_editor(self): - return wx.grid.GridCellBoolEditor() - - def get_renderer(self): - return wx.grid.GridCellBoolRenderer() - - def string_for_value(self, value): - if value == True: - return '1' - return '' - - def value_for_string(self, string): - return string == '1' - - -class IntProperty (Property): - def __init__(self, **kwargs): - assert 'type' not in kwargs, kwargs - if 'default' not in kwargs: - kwargs['default'] = 0 - super(IntProperty, self).__init__(type='int', **kwargs) - - def get_editor(self): - return wx.grid.GridCellNumberEditor() - - def get_renderer(self): - return wx.grid.GridCellNumberRenderer() - - def value_for_string(self, string): - return int(string) - - -class FloatProperty (Property): - def __init__(self, **kwargs): - assert 'type' not in kwargs, kwargs - if 'default' not in kwargs: - kwargs['default'] = 0.0 - super(FloatProperty, self).__init__(type='float', **kwargs) - - def get_editor(self): - return wx.grid.GridCellFloatEditor() - - def get_renderer(self): - return wx.grid.GridCellFloatRenderer() - - def value_for_string(self, string): - return float(string) - - -class ChoiceProperty (Property): - def __init__(self, choices, **kwargs): - assert 'type' not in kwargs, kwargs - if 'default' in kwargs: - if kwargs['default'] not in choices: - choices.insert(0, kwargs['default']) - else: - kwargs['default'] = choices[0] - super(ChoiceProperty, self).__init__(type='choice', **kwargs) - self._choices = choices - - def get_editor(self): - choices = [self.string_for_value(c) for c in self._choices] - return wx.grid.GridCellChoiceEditor(choices=choices) - - def get_renderer(self): - return None - #return wx.grid.GridCellChoiceRenderer() - - def string_for_value(self, value): - if hasattr(value, 'name'): - return value.name - return str(value) - - def value_for_string(self, string): - for choice in self._choices: - if self.string_for_value(choice) == string: - return choice - raise ValueError(string) - - -class PathProperty (StringProperty): - """Simple file or path property. - - Currently there isn't a fancy file-picker popup. Perhaps in the - future. - """ - def __init__(self, **kwargs): - super(PathProperty, self).__init__(**kwargs) - self.type = 'path' - - -class PropertyPanel(Panel, wx.grid.Grid): - """UI to view/set config values and command argsuments. - """ - def __init__(self, callbacks=None, **kwargs): - super(PropertyPanel, self).__init__( - name='propertyeditor', callbacks=callbacks, **kwargs) - self._properties = [] - - self.CreateGrid(numRows=0, numCols=1) - self.SetColLabelValue(0, 'value') - - self._last_tooltip = None - self.GetGridWindow().Bind(wx.EVT_MOTION, self._on_mouse_over) - - def _on_mouse_over(self, event): - """Enable tooltips. - """ - x,y = self.CalcUnscrolledPosition(event.GetPosition()) - col,row = self.XYToCell(x, y) - if col == -1 or row == -1: - msg = '' - else: - msg = self._properties[row].help or '' - if msg != self._last_tooltip: - self._last_tooltip = msg - event.GetEventObject().SetToolTipString(msg) - - def append_property(self, property): - if len([p for p in self._properties if p.label == property.label]) > 0: - raise ValueError(property) # property.label collision - self._properties.append(property) - row = len(self._properties) - 1 - self.AppendRows(numRows=1) - self.SetRowLabelValue(row, property.label) - self.SetCellEditor(row=row, col=0, editor=property.get_editor()) - r = property.get_renderer() - if r != None: - self.SetCellRenderer(row=row, col=0, renderer=r) - self.set_property(property.label, property.default) - - def remove_property(self, label): - row,property = self._property_by_label(label) - self._properties.pop(row) - self.DeleteRows(pos=row) - - def clear(self): - while(len(self._properties) > 0): - self.remove_property(self._properties[-1].label) - - def set_property(self, label, value): - row,property = self._property_by_label(label) - self.SetCellValue(row=row, col=0, s=property.string_for_value(value)) - - def get_property(self, label): - row,property = self._property_by_label(label) - string = self.GetCellValue(row=row, col=0) - return property.value_for_string(string) - - def get_values(self): - return dict([(p.label, self.get_property(p.label)) - for p in self._properties]) - - def _property_by_label(self, label): - props = [(i,p) for i,p in enumerate(self._properties) - if p.label == label] - assert len(props) == 1, props - row,property = props[0] - return (row, property)