dabo Commit Revision 6430 Date: 2011-02-12 09:18:39 -0800 (Sat, 12 Feb 2011) Author: Ed Trac: http://trac.dabodev.com/changeset/6430
Changed: U trunk/dabo/dApp.py A trunk/dabo/icons/collapse_tree.png A trunk/dabo/icons/event_watcher.png A trunk/dabo/icons/expand_tree.png A trunk/dabo/icons/find_object.png A trunk/dabo/icons/highlight_item.png A trunk/dabo/icons/refresh_tree.png A trunk/dabo/icons/show_sizers.png U trunk/dabo/ui/uiwx/dBaseMenuBar.py U trunk/dabo/ui/uiwx/dFormMixin.py A trunk/dabo/ui/uiwx/object_inspector.py U trunk/dabo/ui/uiwx/uiApp.py Log: First pass at creating an Object Inspector, which is largely modeled on the wxPython Widget Inspection Tool (http://wiki.wxpython.org/Widget%20Inspection%20Tool). It largely works as expected, but I'm sure there will be cases that need cleaning up. To invoke it, I've added a menu item to dBaseMenuBar that can be triggered by Ctrl-Shift-I. Right now the form sections are arranged using splitters, because the form is cdxml-based. This allowed me to rapidly prototype it, but I would prefer to use dDockForm instead, which unfortunately is not yet supported in the Class Designer. Feedback welcome!! Diff: Modified: trunk/dabo/dApp.py =================================================================== --- trunk/dabo/dApp.py 2011-02-12 16:51:17 UTC (rev 6429) +++ trunk/dabo/dApp.py 2011-02-12 17:18:39 UTC (rev 6430) @@ -1181,6 +1181,8 @@ self.uiApp.onCmdWin(evt) def onDebugWin(self, evt): self.uiApp.onDebugWin(evt) + def onObjectInspectorWin(self, evt): + self.uiApp.onObjectInspectorWin(evt) def onWinClose(self, evt): self.uiApp.onWinClose(evt) def onFileExit(self, evt): @@ -1709,7 +1711,7 @@ FormsToOpen = property(_getFormsToOpen, _setFormsToOpen, None, _("""List of forms to open after App instantiation. (list of form class references)""")) formsToOpen = FormsToOpen ## backwards-compatibility - + HomeDirectory = property(_getHomeDirectory, _setHomeDirectory, None, _("""Specifies the application's home directory. (string) Added: trunk/dabo/icons/collapse_tree.png =================================================================== (Binary files differ) Property changes on: trunk/dabo/icons/collapse_tree.png ___________________________________________________________________ Name: svn:mime-type + application/octet-stream Added: trunk/dabo/icons/event_watcher.png =================================================================== (Binary files differ) Property changes on: trunk/dabo/icons/event_watcher.png ___________________________________________________________________ Name: svn:mime-type + application/octet-stream Added: trunk/dabo/icons/expand_tree.png =================================================================== (Binary files differ) Property changes on: trunk/dabo/icons/expand_tree.png ___________________________________________________________________ Name: svn:mime-type + application/octet-stream Added: trunk/dabo/icons/find_object.png =================================================================== (Binary files differ) Property changes on: trunk/dabo/icons/find_object.png ___________________________________________________________________ Name: svn:mime-type + application/octet-stream Added: trunk/dabo/icons/highlight_item.png =================================================================== (Binary files differ) Property changes on: trunk/dabo/icons/highlight_item.png ___________________________________________________________________ Name: svn:mime-type + application/octet-stream Added: trunk/dabo/icons/refresh_tree.png =================================================================== (Binary files differ) Property changes on: trunk/dabo/icons/refresh_tree.png ___________________________________________________________________ Name: svn:mime-type + application/octet-stream Added: trunk/dabo/icons/show_sizers.png =================================================================== (Binary files differ) Property changes on: trunk/dabo/icons/show_sizers.png ___________________________________________________________________ Name: svn:mime-type + application/octet-stream Modified: trunk/dabo/ui/uiwx/dBaseMenuBar.py =================================================================== --- trunk/dabo/ui/uiwx/dBaseMenuBar.py 2011-02-12 16:51:17 UTC (rev 6429) +++ trunk/dabo/ui/uiwx/dBaseMenuBar.py 2011-02-12 17:18:39 UTC (rev 6430) @@ -43,7 +43,12 @@ OnHit=app.onDebugWin, ItemID="file_debugwin", help=_("Open up a debug output window")) self.Parent.debugMenuItem.Checked = True - + + self.Parent.inspectorMenuItem = self.append(_("Object &Inspector"), HotKey="Ctrl+Shift+I", + OnHit=app.onObjectInspectorWin, bmp="%s/apps/system-search.png" % iconPath, + ItemID="file_inspectorwin", menutype="check", + help=_("Open up the object inspector")) + prmpt = _("Close Windo&w") self.Parent.closeWindowMenuItem = self.append(prmpt, HotKey="Ctrl+W", OnHit=app.onWinClose, ItemID="file_close", @@ -177,16 +182,6 @@ self.helpMenu = self.appendMenu(HelpMenu(self, MenuID="base_help")) -# Trying to expose menu atts as menubar atts. Not sure if this is a good idea yet... -# def __getattr__(self, att): -# ret = None -# for menu in self.Children: -# ret = getattr(menu, att) -# if ret: -# break -# if not ret: -# raise AttributeError -# return ret if __name__ == "__main__": app = dabo.dApp() Modified: trunk/dabo/ui/uiwx/dFormMixin.py =================================================================== --- trunk/dabo/ui/uiwx/dFormMixin.py 2011-02-12 16:51:17 UTC (rev 6429) +++ trunk/dabo/ui/uiwx/dFormMixin.py 2011-02-12 17:18:39 UTC (rev 6430) @@ -231,9 +231,11 @@ def __onIdle(self, evt): if self.__needOutlineRedraw or self._alwaysDrawSizerOutlines: + self.refresh() for sz in self.SizersToOutline: + win = sz.getContainingWindow() try: - sz.drawOutline(self, recurse=self._recurseOutlinedSizers, + sz.drawOutline(win, recurse=self._recurseOutlinedSizers, drawChildren=self._drawSizerChildren) except AttributeError: # Will happen if sz is None @@ -265,6 +267,10 @@ app.uiForms.remove(self) + def forceSizerOutline(self): + self.__needOutlineRedraw = True + + def activeControlValid(self): """ Force the control-with-focus to fire its KillFocus code. Added: trunk/dabo/ui/uiwx/object_inspector.py =================================================================== --- trunk/dabo/ui/uiwx/object_inspector.py (rev 0) +++ trunk/dabo/ui/uiwx/object_inspector.py 2011-02-12 17:18:39 UTC (rev 6430) @@ -0,0 +1,447 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import dabo +dabo.ui.loadUI("wx") + + +# 'InspectorFormClass' is defined at the bottom + +inspector_source = '''<?xml version="1.0" encoding="mac-roman" standalone="no"?> +<dForm Name="dForm" Caption="Dabo Object Inspector" SaveRestorePosition="False" Height="750" Width="850" designerClass="DesForm"> + <code> + <addkids><![CDATA[ +def addkids(self, obj, node): + if self._showSizers: + try: + kid = obj.Sizer + except AttributeError: + kid = None + if kid: + snode = node.appendChild(self.sizer_repr(kid)) + snode.Object = kid + snode.ForeColor = "blue" + self.addkids(kid, snode) + return + try: + kids = obj.Children + except AttributeError: + # Not a dabo obj + return + for kid in kids: + if self.exclude(kid): + continue + nodeColor = None + if isinstance(kid, wx._controls.ScrollBar): + continue + if isinstance(obj, dabo.ui.dSizerMixin): + kid = obj.getItem(kid) + if isinstance(kid, dabo.ui.dSizerMixin): + txt = self.sizer_repr(kid) + nodeColor = "blue" + else: + try: + txt = kid.Name + except AttributeError: + if isinstance(kid, wx.Size): + txt = "Spacer %s" % kid + nodeColor = "darkred" + else: + txt = "%s" % kid + txt = "%s (%s)" % (txt, self.cls_repr(kid.__class__)) + knode = node.appendChild(txt) + if nodeColor is not None: + knode.ForeColor = nodeColor + knode.Object = kid + self.addkids(kid, knode) +]]> + </addkids> + <clearHighlight><![CDATA[ +def clearHighlight(self): + if not self: + return + current = time.time() + for expiration in self._highlights.keys(): + if expiration > current: + continue + toClear = self._highlights.pop(expiration) + frm = toClear["targetForm"] + if toClear["type"] == "drawing": + try: + frm.removeDrawnObject(toClear["drawingToClear"]) + except ValueError: + pass + frm.forceSizerOutline() + else: + sz = toClear["outlinedSizer"] + frm.removeFromOutlinedSizers(sz) + frm._alwaysDrawSizerOutlines = toClear["drawSetting"] + sz.outlineStyle = toClear["outlineStyle"] + sz.outlineWidth = toClear["outlineWidth"] + frm.clear() +]]> + </clearHighlight> + <onCollapseTree><![CDATA[ +def onCollapseTree(self, evt): + self.objectTree.collapseCurrentNode() +]]> + </onCollapseTree> + <afterInit><![CDATA[ +def afterInit(self): + self.BasePrefKey = "object_inspector" + self.PreferenceManager = dabo.dPref(key=self.BasePrefKey) + self._highlights = {} +]]> + </afterInit> + <_setShowSizers><![CDATA[ +def _setShowSizers(self, val): + self._showSizers = val +]]> + </_setShowSizers> + <importStatements><![CDATA[ +import time +import wx +from dabo.dLocalize import _ +]]> + </importStatements> + <formatName><![CDATA[ +def formatName(self, obj): + if not obj: + return "< -dead object- >" + try: + cap = obj.Caption + except AttributeError: + cap = "" + try: + cls = obj.BaseClass + except AttributeError: + cls = obj.__class__ + classString = "%s" % cls + shortClass = classString.replace("'", "").replace(">", "").split(".")[-1] + if cap: + ret = "%s (\\"%s\\")" % (shortClass, cap) + else: + try: + ret = "%s (%s)" % (obj.Name, shortClass) + except AttributeError: + ret = "%s (%s)" % (obj, cls) + return ret +]]> + </formatName> + <onToggleSizers><![CDATA[ +def onToggleSizers(self, evt): + self._showSizers = not self._showSizers + self.createObjectTree() +]]> + </onToggleSizers> + <showPropVals><![CDATA[ +def showPropVals(self, obj): + rows = [] + props = [] + try: + props = obj.getPropertyList(onlyDabo=True) + except AttributeError: + for c in obj.__class__.__mro__: + if c is dabo.lib.propertyHelperMixin.PropertyHelperMixin: + # Don't list properties lower down (e.g., from wxPython): + break + for item in dir(c): + if item[0].isupper(): + if item in c.__dict__: + if type(c.__dict__[item]) == property: + if props.count(item) == 0: + props.append(item) + for prop in props: + if prop == "ShowColumnLabels": + # Avoid the deprecation warning + continue + try: + val = getattr(obj, prop) + except (AttributeError, TypeError, dabo.ui.assertionException): + # write-only or otherwise unavailable + continue + if prop.startswith("Dynamic") and val is None: + continue + if val is None: + val = self.Application.NoneDisplay + elif isinstance(val, basestring): + val = "'%s'" % val + elif isinstance(val, dabo.dObject.dObject): + try: + val = "'%s'" % self.formatName(val) + except StandardError, e: + pass + rows.append({"prop": prop, "val": val}) + ds = dabo.db.dDataSet(rows) + self.infoGrid.DataSet = ds +]]> + </showPropVals> + <sizer_repr><![CDATA[ +def sizer_repr(self, sz): + """Returns an informative representation for a sizer""" + if isinstance(sz, dabo.ui.dGridSizer): + ret = "dGridSizer (%s x %s)" % (sz.HighRow, sz.HighCol) + elif isinstance(sz, dabo.ui.dBorderSizer): + ret = "dBorderSizer (%s, '%s')" % (sz.Orientation, sz.Caption) + else: + ret = "dSizer (%s)" % sz.Orientation + return ret +]]> + </sizer_repr> + <_getShowSizers><![CDATA[ +def _getShowSizers(self): + try: + return self._showSizers + except AttributeError: + return None +]]> + </_getShowSizers> + <exclude><![CDATA[ +def exclude(self, obj): + isFloat = (isinstance(obj, dabo.ui.dDialog) and + hasattr(obj, "Above") and hasattr(obj, "Owner")) + return isFloat or (obj is self) +]]> + </exclude> + <onHighlightItem><![CDATA[ +def onHighlightItem(self, evt): + obj = self.objectTree.Selection.Object + try: + frm = obj.Form + except AttributeError: + return + # Remove the highlight after 3 seconds + expires = time.time() + 3 + entry = self._highlights[expires] = {} + entry["targetForm"] = frm + + if isinstance(obj, dabo.ui.dSizerMixin): + entry["type"] = "sizer" + frm.addToOutlinedSizers(obj) + frm.refresh() + entry["outlinedSizer"] = obj + entry["drawSetting"] = frm._alwaysDrawSizerOutlines + entry["outlineStyle"] = obj.outlineStyle + obj.outlineStyle = "dot" + entry["outlineWidth"] = obj.outlineWidth + obj.outlineWidth = 4 + frm._alwaysDrawSizerOutlines = True + else: + entry["type"] = "drawing" + x, y = obj.formCoordinates() + entry["drawingToClear"] = frm.drawRectangle(x-3, y-3, obj.Width+6, obj.Height+6, + penWidth=3, penColor="magenta") +]]> + </onHighlightItem> + <onFindObject><![CDATA[ +def onFindObject(self, evt): + self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) + self.Bind(wx.EVT_MOUSE_CAPTURE_LOST, self.OnCaptureLost) + self.CaptureMouse() + self.finding = wx.BusyInfo("Click on any widget in the app...") +]]> + </onFindObject> + <OnCaptureLost><![CDATA[ +def OnCaptureLost(self, evt): + self.Unbind(wx.EVT_LEFT_DOWN) + self.Unbind(wx.EVT_MOUSE_CAPTURE_LOST) + del self.finding +]]> + </OnCaptureLost> + <afterInitAll><![CDATA[ +def afterInitAll(self): + objnote = "NOTE: The 'obj' variable refers to the object selected in the tree." + intro = "%s\\n%s" % (dabo.ui.getSystemInfo(), objnote) + self.shell = dabo.ui.dShell._Shell(self.shellPanel, showInterpIntro=False, + introText=intro) + self.shell.interp.locals['self'] = self + sz = self.shellPanel.Sizer = dabo.ui.dBorderSizer(self.shellPanel, Caption="Interactive Interpreter") + sz.append1x(self.shell) + dabo.ui.callEvery(250, self.clearHighlight) + + tb = self.ToolBar = dabo.ui.dToolBar(self, ShowCaptions=True) + self.refreshButton = self.appendToolBarButton(name="Refresh", pic="refresh_tree.png", + toggle=False, tip=_("Re-create the object tree"), + OnHit=self.onRefreshTree) + self.findButton = self.appendToolBarButton(name="Find", pic="find_object.png", + toggle=False, tip=_("Find an object in your app in the tree"), + OnHit=self.onFindObject) + self.showSizersButton = self.appendToolBarButton(name="Show Sizers", pic="show_sizers.png", + toggle=True, tip=_("Show/Hide sizers in the object hierarchy"), + OnHit=self.onToggleSizers) + self.expandButton = self.appendToolBarButton(name="Expand", pic="expand_tree.png", + toggle=False, tip=_("Expand this node and all nodes under it."), + OnHit=self.onExpandTree) + self.collapseButton = self.appendToolBarButton(name="Collapse", pic="collapse_tree.png", + toggle=False, tip=_("Collapse this node and all nodes under it."), + OnHit=self.onCollapseTree) + self.highlightButton = self.appendToolBarButton(name="Highlight", pic="highlight_item.png", + toggle=False, tip=_("Highlight the selected node in your app."), + OnHit=self.onHighlightItem) + self.layout() +]]> + </afterInitAll> + <onRefreshTree><![CDATA[ +def onRefreshTree(self, evt): + self.createObjectTree() +]]> + </onRefreshTree> + <onExpandTree><![CDATA[ +def onExpandTree(self, evt): + self.objectTree.expandCurrentNode() +]]> + </onExpandTree> + <cls_repr><![CDATA[ +def cls_repr(self, cls): + """Returns a readable representation for a class""" + txt = "%s" % cls + prfx, clsname, suff = txt.split("'") + return clsname +]]> + </cls_repr> + <OnLeftDown><![CDATA[ +def OnLeftDown(self, evt): + self.ReleaseMouse() + wnd = wx.FindWindowAtPointer() + if wnd is not None: + self.objectTree.showObject(wnd) + else: + dabo.ui.beep() + self.OnCaptureLost(evt) +]]> + </OnLeftDown> + <object_selected><![CDATA[ +def object_selected(self, obj): + self.shell.interp.locals['obj'] = obj + self.shellPanel.Sizer.Caption = "'obj' is %s" % self.formatName(obj) + self.showPropVals(obj) +]]> + </object_selected> + <createObjectTree><![CDATA[ +def createObjectTree(self): + tree = self.objectTree + try: + currObj = tree.Selection.Object + currForm = currObj.Form + except AttributeError: + # Nothing selected yet + currObj = currForm = None + tree.clear() + root = tree.setRootNode("Top Level Windows") + for win in self.Application.uiForms: + if self.exclude(win): + continue + winNode = root.appendChild(self.formatName(win)) + winNode.Object = win + self.addkids(win, winNode) + root.expand() + if currObj: + nd = tree.nodeForObject(currObj) + if not nd: + nd = tree.nodeForObject(currForm) + nd.Selected = True +]]> + </createObjectTree> + </code> + + <properties> + <ShowSizers> + <comment>Determines if sizers are displayed in the object hierarchy</comment> + <defaultValue>False</defaultValue> + <deller>None</deller> + <getter>_getShowSizers</getter> + <propName>ShowSizers</propName> + <defaultType>boolean</defaultType> + <setter>_setShowSizers</setter> + </ShowSizers> + </properties> + + <dSizer SlotCount="1" designerClass="LayoutSizer" Orientation="Vertical"> + <dSplitter SashPosition="380" sizerInfo="{'VAlign': 'Middle'}" designerClass="controlMix" Split="True" Orientation="Horizontal"> + <dPanel designerClass="MixedSplitterPanel" Name="dPanel2"> + <dSizer SlotCount="1" designerClass="LayoutSizer" Orientation="Vertical"> + <dSplitter SashPosition="322" sizerInfo="{'VAlign': 'Middle'}" designerClass="controlMix" Split="True" Orientation="Vertical"> + <dPanel designerClass="MixedSplitterPanel" Name="dPanel2"> + <dSizer SlotCount="1" designerClass="LayoutSizer" Orientation="Vertical"> + <dTreeView RegID="objectTree" sizerInfo="{'VAlign': 'Middle'}" designerClass="controlMix"> + <code> + <showObject><![CDATA[ +def showObject(self, obj): + nd = self.nodeForObject(obj) + if nd: + nd.Selected = True + self.showNode(nd) + else: + dabo.ui.stop("Couldn't find object: %s" % obj) +]]> + </showObject> + <onTreeSelection><![CDATA[ +def onTreeSelection(self, evt): + self.Form.object_selected(self.Selection.Object) +]]> + </onTreeSelection> + <expandCurrentNode><![CDATA[ +def expandCurrentNode(self): + self.expandBranch(self.Selection) +]]> + </expandCurrentNode> + <collapseCurrentNode><![CDATA[ +def collapseCurrentNode(self): + self.collapseBranch(self.Selection) +]]> + </collapseCurrentNode> + </code> + + <dNode Caption="This is the root" designerClass="controlMix"> + <dNode Caption="First Child" designerClass="controlMix"></dNode> + <dNode Caption="Second Child" designerClass="controlMix"> + <dNode Caption="Grandkid #1" designerClass="controlMix"></dNode> + <dNode Caption="Grandkid #2" designerClass="controlMix"> + <dNode Caption="Great-Grandkid #1" designerClass="controlMix"></dNode> + </dNode> + <dNode Caption="Grandkid #3" designerClass="controlMix"></dNode> + </dNode> + <dNode Caption="Third Child" designerClass="controlMix"></dNode> + </dNode> + </dTreeView> + </dSizer> + </dPanel> + <dPanel designerClass="MixedSplitterPanel" Name="dPanel1"> + <dSizer SlotCount="1" designerClass="LayoutSizer" Orientation="Vertical"> + <dGrid ColumnCount="2" RegID="infoGrid" SelectionMode="Row" designerClass="controlMix" sizerInfo="{}"> + <code> + <onGridMouseLeftClick><![CDATA[ +def onGridMouseLeftClick(self, evt): + def later(): + ds = self.DataSet + row = ds[self.CurrentRow] + prop = row["prop"] + self.Form.PreferenceManager.excluded_props.setValue(prop, True) + lds = list(ds) + lds.remove(row) + self.DataSet = dabo.db.dDataSet(lds) + + if evt.altDown: + dabo.ui.callAfterInterval(250, later) +]]> + </onGridMouseLeftClick> + </code> + + <dColumn Width="169" Caption="Property" HorizontalAlignment="Right" DataField="prop" designerClass="controlMix" Order="0"></dColumn> + <dColumn Caption="Value" DataField="val" designerClass="controlMix" Order="10" Width="399"></dColumn> + </dGrid> + </dSizer> + </dPanel> + </dSplitter> + </dSizer> + </dPanel> + <dPanel designerClass="MixedSplitterPanel" Name="dPanel1"> + <dSizer SlotCount="1" designerClass="LayoutSizer" Orientation="Vertical"> + <dPanel RegID="shellPanel" sizerInfo="{'VAlign': 'Middle'}" AlwaysResetSizer="True" designerClass="controlMix"></dPanel> + </dSizer> + </dPanel> + </dSplitter> + </dSizer> +</dForm> +''' + +InspectorFormClass = dabo.ui.createClass(inspector_source) Property changes on: trunk/dabo/ui/uiwx/object_inspector.py ___________________________________________________________________ Name: svn:eol-style + native Modified: trunk/dabo/ui/uiwx/uiApp.py =================================================================== --- trunk/dabo/ui/uiwx/uiApp.py 2011-02-12 16:51:17 UTC (rev 6429) +++ trunk/dabo/ui/uiwx/uiApp.py 2011-02-12 17:18:39 UTC (rev 6430) @@ -157,6 +157,8 @@ wx.InitAllImageHandlers() # Set up the debug logging self.debugWindow = None + # Set up the object inspector + self.inspectorWindow = None log = logging.getLogger("Debug") class DebugLogHandler(logging.Handler): def emit(self, record): @@ -507,6 +509,10 @@ self.toggleDebugWindow(self.ActiveForm) + def onObjectInspectorWin(self, evt): + self.toggleInspectorWindow(self.ActiveForm) + + def showCommandWindow(self, context=None): """Display a command window for debugging.""" if context is None: @@ -558,9 +564,30 @@ except AttributeError: #Either no such item, or not a valid reference pass - + def toggleInspectorWindow(self, context=None): + """Display the object inspector window.""" + if context is None: + context = self.ActiveForm + if not self.inspectorWindow: +# loc = os.path.dirname(dabo.ui.uiwx.__file__) +# pth = os.path.join(loc, "inspector.cdxml") +# self.inspectorWindow = dabo.ui.createForm(pth, parent=context, show=False) + from object_inspector import InspectorFormClass + self.inspectorWindow = InspectorFormClass(parent=context) + insp = self.inspectorWindow + insp.createObjectTree() + insp.Visible = not insp.Visible + try: + mb = context.MenuBar + mb.inspectorMenuItem.Checked = insp.Visible + mb.Refresh() + except AttributeError: + # Either no such item, or not a valid reference + pass + + def onWinClose(self, evt): """Close the topmost window, if any.""" if self.ActiveForm: _______________________________________________ Post Messages to: [email protected] Subscription Maintenance: http://leafe.com/mailman/listinfo/dabo-dev Searchable Archives: http://leafe.com/archives/search/dabo-dev This message: http://leafe.com/archives/byMID/[email protected]
