From 24d861377e6e59a30e2b6bfc7d4b6a9ddc9b44c5 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Thu, 29 Jul 2010 11:22:09 -0400 Subject: [PATCH] Added gui.panel.propertyeditor2 to work around *nix+wxPropertyGrid issues. The basic framework works, but only IntProperty is currently implemented. --- hooke/ui/gui/__init__.py | 9 +- hooke/ui/gui/panel/__init__.py | 1 + hooke/ui/gui/panel/propertyeditor2.py | 474 ++++++++++++++++++++++++++ 3 files changed, 482 insertions(+), 2 deletions(-) create mode 100644 hooke/ui/gui/panel/propertyeditor2.py diff --git a/hooke/ui/gui/__init__.py b/hooke/ui/gui/__init__.py index d9706f2..592e55e 100644 --- a/hooke/ui/gui/__init__.py +++ b/hooke/ui/gui/__init__.py @@ -20,7 +20,7 @@ import wx.aui as aui import wx.lib.evtmgr as evtmgr -# wxPropertyGrid included in wxPython >= 2.9.1, until then, see +# 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 @@ -148,7 +148,12 @@ class HookeFrame (wx.Frame): # WANTS_CHARS so the panel doesn't eat the Return key. # size=(160, 200) ), 'center'), - #('properties', panel.propertyeditor.PropertyEditor(self),'right'), + ('property', panel.PANELS['propertyeditor2']( + callbacks={}, + parent=self, + style=wx.WANTS_CHARS, + # WANTS_CHARS so the panel doesn't eat the Return key. + ), 'center'), # ('assistant', wx.TextCtrl( # parent=self, # pos=wx.Point(0, 0), diff --git a/hooke/ui/gui/panel/__init__.py b/hooke/ui/gui/panel/__init__.py index 50aa1dc..2457f5c 100644 --- a/hooke/ui/gui/panel/__init__.py +++ b/hooke/ui/gui/panel/__init__.py @@ -11,6 +11,7 @@ PANEL_MODULES = [ # 'playlist', # 'plot', # 'propertyeditor', + 'propertyeditor2', # 'results', # 'selection', # 'welcome', diff --git a/hooke/ui/gui/panel/propertyeditor2.py b/hooke/ui/gui/panel/propertyeditor2.py new file mode 100644 index 0000000..f361a37 --- /dev/null +++ b/hooke/ui/gui/panel/propertyeditor2.py @@ -0,0 +1,474 @@ +# 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_setting(setting): + """Convert a :class:`~hooke.config.Setting` to a :class:`Property`. + """ + raise NotImplementedError() + +def prop_from_argument(argument): + """Convert a :class:`~hooke.command.Argument` 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 wx.Window instance containing a suitable editor. + + Retains a reference to the returned editory as `._editor`. + """ + raise NotImplementedError() + + def value_string(self, value): + """Return a string representation of `value` for loading the table. + """ + return str(value) + + def get_value(self): + """Return the value of `._editor`. + """ + raise NotImplementedError() + + +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): + self._editor = wx.grid.GridCellNumberEditor() + return self._editor + + def get_value(self): + raise int(self._editor.GetValue()) + + +#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 PropertyPanel(Panel, wx.grid.Grid): + 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) + + self.append_property(IntProperty( + label='my int', + default=5, + help='help for my int', + )) + + def GetRowLabelValue(self, col=0): + """Retrieve the label for a particular column. + + Overrides the default labels, since 'SetRowLabelValue' seems + to be `ignored`_. + + .. _ignored: + http://wiki.wxpython.org/wxPyGridTableBase#Column.2BAC8-Row_Labels + """ + return self._column_labels[col] + + 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 + 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 == 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()) + self.set_property(property.label, property.default) + + def set_property(self, label, value): + props = [(i,p) for i,p in enumerate(self._properties) + if p.label == label] + assert len(props) == 1, props + row,property = props[0] + self.SetCellValue(row=row, col=0, s=property.value_string(value)) + + def remove_property(self, property): + pass + + def GetPropertyValues(self): + raise NotImplementedError() + return self.pg.GetPropertyValues() + + def Initialize(self, properties): + raise NotImplementedError() + 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): + raise NotImplementedError() + pass -- 2.26.2