#---------------------------------------------------------------------------- # Name: wx.lib.inspection # Purpose: A widget inspection tool that allows easy introspection of # all the live widgets and sizers in an application. # # Author: Robin Dunn # # Created: 26-Jan-2007 # Copyright: (c) 2007 by Total Control Software # Licence: wxWindows license # # Tags: py3-port, phoenix-port, documented #---------------------------------------------------------------------------- # NOTE: This class was originally based on ideas sent to the # wxPython-users mail list by Dan Eloff. See also # wx.lib.mixins.inspect for a class that can be mixed-in with wx.App # to provide Hot-Key access to the inspection tool. """ This modules provides the :class:`InspectionTool` and everything else needed to provide the Widget Inspection Tool (WIT). """ import wx import wx.py import wx.stc #import wx.aui as aui import wx.lib.agw.aui as aui import wx.lib.six as six import wx.lib.utils as utils import sys import inspect #---------------------------------------------------------------------------- class InspectionTool: """ The :class:`InspectionTool` is a singleton that manages creating and showing an :class:`InspectionFrame`. """ # Note: This is the Borg design pattern which ensures that all # instances of this class are actually using the same set of # instance data. See # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66531 __shared_state = {} def __init__(self): self.__dict__ = self.__shared_state if not hasattr(self, 'initialized'): self.initialized = False def Init(self, pos=wx.DefaultPosition, size=wx.Size(850,700), config=None, locals=None, app=None): """ Init is used to set some parameters that will be used later when the inspection tool is shown. Suitable defaults will be used for all of these parameters if they are not provided. :param `pos`: The default position to show the frame at :param `size`: The default size of the frame :param `config`: A :class:`Config` object to be used to store layout and other info to when the inspection frame is closed. This info will be restored the next time the inspection frame is used. :param `locals`: A dictionary of names to be added to the PyCrust namespace. :param `app`: A reference to the :class:`App` object. """ self._frame = None self._pos = pos self._size = size self._config = config self._locals = locals self._app = app if not self._app: self._app = wx.GetApp() self.initialized = True def Show(self, selectObj=None, refreshTree=False): """ Creates the inspection frame if it hasn't been already, and raises it if neccessary. :param `selectObj`: Pass a widget or sizer to have that object be preselected in widget tree. :param boolean `refreshTree`: rebuild the widget tree, default False """ if not self.initialized: self.Init() parent = self._app.GetTopWindow() if not selectObj: selectObj = parent if not self._frame: self._frame = InspectionFrame( parent=parent, pos=self._pos, size=self._size, config=self._config, locals=self._locals, app=self._app) if selectObj: self._frame.SetObj(selectObj) if refreshTree: self._frame.RefreshTree() self._frame.Show() if self._frame.IsIconized(): self._frame.Iconize(False) self._frame.Raise() #---------------------------------------------------------------------------- class InspectionFrame(wx.Frame): """ This class is the frame that holds the wxPython inspection tools. The toolbar and AUI splitters/floating panes are also managed here. The contents of the tool windows are handled by other classes. """ def __init__(self, wnd=None, locals=None, config=None, app=None, title="wxPython Widget Inspection Tool", *args, **kw): kw['title'] = title wx.Frame.__init__(self, *args, **kw) self.SetExtraStyle(wx.WS_EX_BLOCK_EVENTS) self.includeSizers = False self.started = False self.SetIcon(Icon.GetIcon()) self.MakeToolBar() panel = wx.Panel(self, size=self.GetClientSize()) # tell FrameManager to manage this frame self.mgr = aui.AuiManager(panel, aui.AUI_MGR_DEFAULT | aui.AUI_MGR_TRANSPARENT_DRAG | aui.AUI_MGR_ALLOW_ACTIVE_PANE) # make the child tools self.tree = InspectionTree(panel, size=(100,300)) self.info = InspectionInfoPanel(panel, style=wx.NO_BORDER, ) if not locals: locals = {} myIntroText = ( "Python %s on %s, wxPython %s\n" "NOTE: The 'obj' variable refers to the object selected in the tree." % (sys.version.split()[0], sys.platform, wx.version())) self.crust = wx.py.crust.Crust(panel, locals=locals, intro=myIntroText, showInterpIntro=False, style=wx.NO_BORDER, ) self.locals = self.crust.shell.interp.locals self.crust.shell.interp.introText = '' self.locals['obj'] = self.obj = wnd self.locals['app'] = app self.locals['wx'] = wx wx.CallAfter(self._postStartup) # put the chlid tools in AUI panes self.mgr.AddPane(self.info, aui.AuiPaneInfo().Name("info").Caption("Object Info"). CenterPane().CaptionVisible(True). CloseButton(False).MaximizeButton(True) ) self.mgr.AddPane(self.tree, aui.AuiPaneInfo().Name("tree").Caption("Widget Tree"). CaptionVisible(True).Left().Dockable(True).Floatable(True). BestSize((280,200)).CloseButton(False).MaximizeButton(True) ) self.mgr.AddPane(self.crust, aui.AuiPaneInfo().Name("crust").Caption("PyCrust"). CaptionVisible(True).Bottom().Dockable(True).Floatable(True). BestSize((400,200)).CloseButton(False).MaximizeButton(True) ) self.mgr.Update() if config is None: config = wx.Config('wxpyinspector') self.config = config self.Bind(wx.EVT_CLOSE, self.OnClose) if self.Parent: tlw = self.Parent.GetTopLevelParent() tlw.Bind(wx.EVT_CLOSE, self.OnClose) self.LoadSettings(self.config) self.crust.shell.lineNumbers = False self.crust.shell.setDisplayLineNumbers(False) self.crust.shell.SetMarginWidth(1, 0) def MakeToolBar(self): tbar = self.CreateToolBar(wx.TB_HORIZONTAL | wx.TB_FLAT | wx.TB_TEXT | wx.NO_BORDER ) tbar.SetToolBitmapSize((24,24)) refreshBmp = Refresh.GetBitmap() findWidgetBmp = Find.GetBitmap() showSizersBmp = ShowSizers.GetBitmap() expandTreeBmp = ExpandTree.GetBitmap() collapseTreeBmp = CollapseTree.GetBitmap() highlightItemBmp = HighlightItem.GetBitmap() evtWatcherBmp = EvtWatcher.GetBitmap() toggleFillingBmp = ShowFilling.GetBitmap() refreshTool = tbar.AddTool(-1, 'Refresh', refreshBmp, shortHelp = 'Refresh widget tree (F1)') findWidgetTool = tbar.AddTool(-1, 'Find', findWidgetBmp, shortHelp='Find new target widget. (F2) Click here and\nthen on another widget in the app.') showSizersTool = tbar.AddTool(-1, 'Sizers', showSizersBmp, shortHelp='Include sizers in widget tree (F3)', kind=wx.ITEM_CHECK) expandTreeTool = tbar.AddTool(-1, 'Expand', expandTreeBmp, shortHelp='Expand all tree items (F4)') collapseTreeTool = tbar.AddTool(-1, 'Collapse', collapseTreeBmp, shortHelp='Collapse all tree items (F5)') highlightItemTool = tbar.AddTool(-1, 'Highlight', highlightItemBmp, shortHelp='Attempt to highlight live item (F6)') evtWatcherTool = tbar.AddTool(-1, 'Events', evtWatcherBmp, shortHelp='Watch the events of the selected item (F7)') toggleFillingTool = tbar.AddTool(-1, 'Filling', toggleFillingBmp, shortHelp='Show PyCrust \'filling\' (F8)', kind=wx.ITEM_CHECK) tbar.Realize() self.Bind(wx.EVT_TOOL, self.OnRefreshTree, refreshTool) self.Bind(wx.EVT_TOOL, self.OnFindWidget, findWidgetTool) self.Bind(wx.EVT_TOOL, self.OnShowSizers, showSizersTool) self.Bind(wx.EVT_TOOL, self.OnExpandTree, expandTreeTool) self.Bind(wx.EVT_TOOL, self.OnCollapseTree, collapseTreeTool) self.Bind(wx.EVT_TOOL, self.OnHighlightItem, highlightItemTool) self.Bind(wx.EVT_TOOL, self.OnWatchEvents, evtWatcherTool) self.Bind(wx.EVT_TOOL, self.OnToggleFilling, toggleFillingTool) self.Bind(wx.EVT_UPDATE_UI, self.OnShowSizersUI, showSizersTool) self.Bind(wx.EVT_UPDATE_UI, self.OnWatchEventsUI, evtWatcherTool) self.Bind(wx.EVT_UPDATE_UI, self.OnToggleFillingUI, toggleFillingTool) tbl = wx.AcceleratorTable( [(wx.ACCEL_NORMAL, wx.WXK_F1, refreshTool.GetId()), (wx.ACCEL_NORMAL, wx.WXK_F2, findWidgetTool.GetId()), (wx.ACCEL_NORMAL, wx.WXK_F3, showSizersTool.GetId()), (wx.ACCEL_NORMAL, wx.WXK_F4, expandTreeTool.GetId()), (wx.ACCEL_NORMAL, wx.WXK_F5, collapseTreeTool.GetId()), (wx.ACCEL_NORMAL, wx.WXK_F6, highlightItemTool.GetId()), (wx.ACCEL_NORMAL, wx.WXK_F7, evtWatcherTool.GetId()), (wx.ACCEL_NORMAL, wx.WXK_F8, toggleFillingTool.GetId()), ]) self.SetAcceleratorTable(tbl) def _postStartup(self): if self.crust.ToolsShown(): self.crust.ToggleTools() self.UpdateInfo() self.started = True def OnClose(self, evt): evt.Skip() if not self: return self.SaveSettings(self.config) if hasattr(self, 'mgr'): self.mgr.UnInit() del self.mgr if self.Parent: tlw = self.Parent.GetTopLevelParent() tlw.Unbind(wx.EVT_CLOSE, handler=self.OnClose) def UpdateInfo(self): self.info.UpdateInfo(self.obj) def SetObj(self, obj): if self.obj is obj: return self.locals['obj'] = self.obj = obj self.UpdateInfo() if not self.tree.built: self.tree.BuildTree(obj, includeSizers=self.includeSizers) else: self.tree.SelectObj(obj) def HighlightCurrentItem(self): """ Draw a highlight rectangle around the item represented by the current tree selection. """ if not hasattr(self, 'highlighter'): self.highlighter = _InspectionHighlighter() self.highlighter.HighlightCurrentItem(self.tree) def RefreshTree(self): self.tree.BuildTree(self.obj, includeSizers=self.includeSizers) def OnRefreshTree(self, evt): self.RefreshTree() self.UpdateInfo() def OnFindWidget(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...") def OnCaptureLost(self, evt): self.Unbind(wx.EVT_LEFT_DOWN) self.Unbind(wx.EVT_MOUSE_CAPTURE_LOST) del self.finding def OnLeftDown(self, evt): self.ReleaseMouse() wnd, pt = wx.FindWindowAtPointer() if wnd is not None: self.SetObj(wnd) else: wx.Bell() self.OnCaptureLost(evt) def OnShowSizers(self, evt): self.includeSizers = not self.includeSizers self.RefreshTree() def OnExpandTree(self, evt): current = self.tree.GetSelection() self.tree.ExpandAll() self.tree.EnsureVisible(current) def OnCollapseTree(self, evt): current = self.tree.GetSelection() self.tree.CollapseAll() self.tree.EnsureVisible(current) self.tree.SelectItem(current) def OnHighlightItem(self, evt): self.HighlightCurrentItem() def OnWatchEvents(self, evt): item = self.tree.GetSelection() obj = self.tree.GetItemData(item) if isinstance(obj, wx.Window): import wx.lib.eventwatcher as ew watcher = ew.EventWatcher(self) watcher.watch(obj) watcher.Show() def OnWatchEventsUI(self, evt): item = self.tree.GetSelection() if item: obj = self.tree.GetItemData(item) evt.Enable(isinstance(obj, wx.Window)) def OnToggleFilling(self, evt): self.crust.ToggleTools() def OnShowSizersUI(self, evt): evt.Check(self.includeSizers) def OnToggleFillingUI(self, evt): if self.started: evt.Check(self.crust.ToolsShown()) def LoadSettings(self, config): self.crust.LoadSettings(config) self.info.LoadSettings(config) pos = wx.Point(config.ReadInt('Window/PosX', -1), config.ReadInt('Window/PosY', -1)) size = wx.Size(config.ReadInt('Window/Width', -1), config.ReadInt('Window/Height', -1)) self.SetSize(size) self.Move(pos) rect = utils.AdjustRectToScreen(self.GetRect()) self.SetRect(rect) perspective = config.Read('perspective', '') if perspective: try: self.mgr.LoadPerspective(perspective) except wx.PyAssertionError: # ignore bad perspective string errors pass self.includeSizers = config.ReadBool('includeSizers', False) def SaveSettings(self, config): self.crust.SaveSettings(config) self.info.SaveSettings(config) if not self.IsIconized() and not self.IsMaximized(): w, h = self.GetSize() config.WriteInt('Window/Width', w) config.WriteInt('Window/Height', h) px, py = self.GetPosition() config.WriteInt('Window/PosX', px) config.WriteInt('Window/PosY', py) perspective = self.mgr.SavePerspective() config.Write('perspective', perspective) config.WriteBool('includeSizers', self.includeSizers) #--------------------------------------------------------------------------- # should inspection frame (and children) be includeed in the tree? INCLUDE_INSPECTOR = True USE_CUSTOMTREECTRL = False if USE_CUSTOMTREECTRL: import wx.lib.agw.customtreectrl as CT TreeBaseClass = CT.CustomTreeCtrl else: TreeBaseClass = wx.TreeCtrl class InspectionTree(TreeBaseClass): """ All of the widgets in the app, and optionally their sizers, are loaded into this tree. """ def __init__(self, *args, **kw): #s = kw.get('style', 0) #kw['style'] = s | wx.TR_DEFAULT_STYLE | wx.TR_HIDE_ROOT TreeBaseClass.__init__(self, *args, **kw) self.roots = [] self.built = False self.Bind(wx.EVT_TREE_SEL_CHANGED, self.OnSelectionChanged) self.toolFrame = wx.GetTopLevelParent(self) if 'wxMac' in wx.PlatformInfo: self.SetWindowVariant(wx.WINDOW_VARIANT_SMALL) def BuildTree(self, startWidget, includeSizers=False, expandFrame=False): if self.GetCount(): self.DeleteAllItems() self.roots = [] self.built = False realRoot = self.AddRoot('Top-level Windows') for w in wx.GetTopLevelWindows(): if w is wx.GetTopLevelParent(self) and not INCLUDE_INSPECTOR: continue root = self._AddWidget(realRoot, w, includeSizers) self.roots.append(root) # Expand the subtree containing the startWidget, and select it. if not startWidget or not isinstance(startWidget, wx.Window): startWidget = wx.GetApp().GetTopWindow() if expandFrame: top = wx.GetTopLevelParent(startWidget) topItem = self.FindWidgetItem(top) if topItem: self.ExpandAllChildren(topItem) self.built = True self.SelectObj(startWidget) def _AddWidget(self, parentItem, widget, includeSizers): text = self.GetTextForWidget(widget) item = self.AppendItem(parentItem, text) self.SetItemData(item, widget) # Add the sizer and widgets in the sizer, if we're showing them widgetsInSizer = [] if includeSizers and widget.GetSizer() is not None: widgetsInSizer = self._AddSizer(item, widget.GetSizer()) # Add any children not in the sizer, or all children if we're # not showing the sizers for child in widget.GetChildren(): if (not child in widgetsInSizer and (not child.IsTopLevel() or isinstance(child, wx.PopupWindow))): self._AddWidget(item, child, includeSizers) return item def _AddSizer(self, parentItem, sizer): widgets = [] text = self.GetTextForSizer(sizer) item = self.AppendItem(parentItem, text) self.SetItemData(item, sizer) self.SetItemTextColour(item, "blue") for si in sizer.GetChildren(): if si.IsWindow(): w = si.GetWindow() self._AddWidget(item, w, True) widgets.append(w) elif si.IsSizer(): ss = si.GetSizer() widgets += self._AddSizer(item, ss) ss._parentSizer = sizer else: i = self.AppendItem(item, "Spacer") self.SetItemData(i, si) self.SetItemTextColour(i, "blue") return widgets def FindWidgetItem(self, widget): """ Find the tree item for a widget. """ for item in self.roots: found = self._FindWidgetItem(widget, item) if found: return found return None def _FindWidgetItem(self, widget, item): if self.GetItemData(item) is widget: return item child, cookie = self.GetFirstChild(item) while child: found = self._FindWidgetItem(widget, child) if found: return found child, cookie = self.GetNextChild(item, cookie) return None def GetTextForWidget(self, widget): """ Returns the string to be used in the tree for a widget """ if hasattr(widget, 'GetName'): return "%s (\"%s\")" % (widget.__class__.__name__, widget.GetName()) return widget.__class__.__name__ def GetTextForSizer(self, sizer): """ Returns the string to be used in the tree for a sizer """ return "%s" % sizer.__class__.__name__ def SelectObj(self, obj): item = self.FindWidgetItem(obj) if item: self.EnsureVisible(item) self.SelectItem(item) def OnSelectionChanged(self, evt): item = evt.GetItem() if item: obj = self.GetItemData(item) self.toolFrame.SetObj(obj) #--------------------------------------------------------------------------- class InspectionInfoPanel(wx.stc.StyledTextCtrl): """ Used to display information about the currently selected items. Currently just a read-only :class:`stc.StyledTextCtrl` with some plain text. Should probably add some styles to make things easier to read. """ def __init__(self, *args, **kw): wx.stc.StyledTextCtrl.__init__(self, *args, **kw) from wx.py.editwindow import FACES self.StyleSetSpec(wx.stc.STC_STYLE_DEFAULT, "face:%(mono)s,size:%(size)d,back:%(backcol)s" % FACES) self.StyleClearAll() self.SetReadOnly(True) self.SetMarginType(1, 0) self.SetMarginWidth(1, 0) self.SetSelForeground(True, wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHTTEXT)) self.SetSelBackground(True, wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHT)) def LoadSettings(self, config): zoom = config.ReadInt('View/Zoom/Info', 0) self.SetZoom(zoom) def SaveSettings(self, config): config.WriteInt('View/Zoom/Info', self.GetZoom()) def UpdateInfo(self, obj): st = [] if not obj: st.append("Item is None or has been destroyed.") elif isinstance(obj, wx.Window): st += self.FmtWidget(obj) elif isinstance(obj, wx.Sizer): st += self.FmtSizer(obj) elif isinstance(obj, wx.SizerItem): st += self.FmtSizerItem(obj) self.SetReadOnly(False) self.SetText('\n'.join(st)) self.SetReadOnly(True) def Fmt(self, name, value): if isinstance(value, six.string_types): return " %s = '%s'" % (name, value) else: return " %s = %s" % (name, value) def FmtWidget(self, obj): def _countChildren(children): count = 0 for child in children: if not child.IsTopLevel(): count += 1 count += _countChildren(child.GetChildren()) return count def _countAllChildren(children): count = 0 for child in children: count += 1 count += _countAllChildren(child.GetChildren()) return count count = len([c for c in obj.GetChildren() if not c.IsTopLevel()]) rcount = _countChildren(obj.GetChildren()) tlwcount = _countAllChildren(obj.GetChildren()) st = ["Widget:"] if hasattr(obj, 'GetName'): st.append(self.Fmt('name', obj.GetName())) st.append(self.Fmt('class', obj.__class__)) st.append(self.Fmt('bases', obj.__class__.__bases__)) st.append(self.Fmt('module', inspect.getmodule(obj))) if hasattr(obj, 'this'): st.append(self.Fmt('this', repr(obj.this))) st.append(self.Fmt('id', obj.GetId())) st.append(self.Fmt('style', obj.GetWindowStyle())) st.append(self.Fmt('pos', obj.GetPosition())) st.append(self.Fmt('size', obj.GetSize())) st.append(self.Fmt('minsize', obj.GetMinSize())) st.append(self.Fmt('bestsize', obj.GetBestSize())) st.append(self.Fmt('client size', obj.GetClientSize())) st.append(self.Fmt('virtual size',obj.GetVirtualSize())) st.append(self.Fmt('IsEnabled', obj.IsEnabled())) st.append(self.Fmt('IsShown', obj.IsShown())) st.append(self.Fmt('IsFrozen', obj.IsFrozen())) st.append(self.Fmt('fg color', obj.GetForegroundColour())) st.append(self.Fmt('bg color', obj.GetBackgroundColour())) st.append(self.Fmt('label', obj.GetLabel())) if hasattr(obj, 'GetTitle'): st.append(self.Fmt('title', obj.GetTitle())) if hasattr(obj, 'GetValue'): try: st.append(self.Fmt('value', obj.GetValue())) except Exception: pass st.append(' child count = %d (direct) %d (recursive) %d (include TLWs)' % (count, rcount, tlwcount)) if obj.GetContainingSizer() is not None: st.append('') sizer = obj.GetContainingSizer() st += self.FmtSizerItem(sizer.GetItem(obj)) return st def FmtSizerItem(self, obj): if obj is None: return ['SizerItem: None'] st = ['SizerItem:'] st.append(self.Fmt('proportion', obj.GetProportion())) st.append(self.Fmt('flag', FlagsFormatter(itemFlags, obj.GetFlag()))) st.append(self.Fmt('border', obj.GetBorder())) st.append(self.Fmt('pos', obj.GetPosition())) st.append(self.Fmt('size', obj.GetSize())) st.append(self.Fmt('minsize', obj.GetMinSize())) st.append(self.Fmt('ratio', obj.GetRatio())) st.append(self.Fmt('IsWindow', obj.IsWindow())) st.append(self.Fmt('IsSizer', obj.IsSizer())) st.append(self.Fmt('IsSpacer', obj.IsSpacer())) st.append(self.Fmt('IsShown', obj.IsShown())) if isinstance(obj, wx.GBSizerItem): st.append(self.Fmt('cellpos', obj.GetPos())) st.append(self.Fmt('cellspan', obj.GetSpan())) st.append(self.Fmt('endpos', obj.GetEndPos())) return st def FmtSizer(self, obj): st = ['Sizer:'] st.append(self.Fmt('class', obj.__class__)) if hasattr(obj, 'this'): st.append(self.Fmt('this', repr(obj.this))) st.append(self.Fmt('pos', obj.GetPosition())) st.append(self.Fmt('size', obj.GetSize())) st.append(self.Fmt('minsize', obj.GetMinSize())) if isinstance(obj, wx.BoxSizer): st.append(self.Fmt('orientation', FlagsFormatter(orientFlags, obj.GetOrientation()))) if isinstance(obj, wx.GridSizer): st.append(self.Fmt('cols', obj.GetCols())) st.append(self.Fmt('rows', obj.GetRows())) st.append(self.Fmt('vgap', obj.GetVGap())) st.append(self.Fmt('hgap', obj.GetHGap())) if isinstance(obj, wx.FlexGridSizer): st.append(self.Fmt('rowheights', obj.GetRowHeights())) st.append(self.Fmt('colwidths', obj.GetColWidths())) st.append(self.Fmt('flexdir', FlagsFormatter(orientFlags, obj.GetFlexibleDirection()))) st.append(self.Fmt('nonflexmode', FlagsFormatter(flexmodeFlags, obj.GetNonFlexibleGrowMode()))) if isinstance(obj, wx.GridBagSizer): st.append(self.Fmt('emptycell', obj.GetEmptyCellSize())) if hasattr(obj, '_parentSizer'): st.append('') st += self.FmtSizerItem(obj._parentSizer.GetItem(obj)) return st class FlagsFormatter(object): def __init__(self, d, val): self.d = d self.val = val def __str__(self): st = [] for k in self.d.keys(): if self.val & k: st.append(self.d[k]) if st: return '|'.join(st) else: return '0' orientFlags = { wx.HORIZONTAL : 'wx.HORIZONTAL', wx.VERTICAL : 'wx.VERTICAL', } itemFlags = { wx.TOP : 'wx.TOP', wx.BOTTOM : 'wx.BOTTOM', wx.LEFT : 'wx.LEFT', wx.RIGHT : 'wx.RIGHT', # wx.ALL : 'wx.ALL', wx.EXPAND : 'wx.EXPAND', # wx.GROW : 'wx.GROW', wx.SHAPED : 'wx.SHAPED', wx.STRETCH_NOT : 'wx.STRETCH_NOT', # wx.ALIGN_CENTER : 'wx.ALIGN_CENTER', wx.ALIGN_LEFT : 'wx.ALIGN_LEFT', wx.ALIGN_RIGHT : 'wx.ALIGN_RIGHT', wx.ALIGN_TOP : 'wx.ALIGN_TOP', wx.ALIGN_BOTTOM : 'wx.ALIGN_BOTTOM', wx.ALIGN_CENTER_VERTICAL : 'wx.ALIGN_CENTER_VERTICAL', wx.ALIGN_CENTER_HORIZONTAL : 'wx.ALIGN_CENTER_HORIZONTAL', wx.ADJUST_MINSIZE : 'wx.ADJUST_MINSIZE', wx.FIXED_MINSIZE : 'wx.FIXED_MINSIZE', } flexmodeFlags = { wx.FLEX_GROWMODE_NONE : 'wx.FLEX_GROWMODE_NONE', wx.FLEX_GROWMODE_SPECIFIED : 'wx.FLEX_GROWMODE_SPECIFIED', wx.FLEX_GROWMODE_ALL : 'wx.FLEX_GROWMODE_ALL', } #--------------------------------------------------------------------------- class _InspectionHighlighter(object): """ All the highlighting code. A separate class to help reduce the clutter in InspectionFrame. """ # should non TLWs be flashed too? Otherwise use a highlight rectangle flashAll = False color1 = 'red' # for widgets and sizers color2 = 'red' # for item boundaries in sizers color3 = '#00008B' # for items in sizers highlightTime = 3000 # how long to display the highlights # how to draw it useOverlay = 'wxMac' in wx.PlatformInfo def __init__(self): if self.useOverlay: self.overlay = wx.Overlay() def HighlightCurrentItem(self, tree): """ Draw a highlight rectangle around the item represented by the current tree selection. """ item = tree.GetSelection() obj = tree.GetItemData(item) if isinstance(obj, wx.Window): self.HighlightWindow(obj) elif isinstance(obj, wx.Sizer): self.HighlightSizer(obj) elif isinstance(obj, wx.SizerItem): # Spacer pItem = tree.GetItemParent(item) sizer = tree.GetItemData(pItem) self.HighlightSizerItem(obj, sizer) else: raise RuntimeError("unknown object type: %s" % obj.__class__.__name__) def HighlightWindow(self, win): rect = win.GetRect() tlw = win.GetTopLevelParent() if self.flashAll or tlw is win: self.FlickerTLW(win) return else: pos = self.FindHighlightPos(tlw, win.ClientToScreen((0,0))) rect.SetPosition(pos) self.DoHighlight(tlw, rect, self.color1) def HighlightSizerItem(self, item, sizer, penWidth=2): win = sizer.GetContainingWindow() tlw = win.GetTopLevelParent() rect = item.GetRect() pos = rect.GetPosition() pos = self.FindHighlightPos(tlw, win.ClientToScreen(pos)) rect.SetPosition(pos) if rect.width < 1: rect.width = 1 if rect.width < 1: rect.width = 1 self.DoHighlight(tlw, rect, self.color1, penWidth) def HighlightSizer(self, sizer): # first do the outline of the whole sizer like normal win = sizer.GetContainingWindow() tlw = win.GetTopLevelParent() pos = sizer.GetPosition() pos = self.FindHighlightPos(tlw, win.ClientToScreen(pos)) rect = wx.Rect(pos, sizer.GetSize()) dc, dco = self.DoHighlight(tlw, rect, self.color1) # Now highlight the actual items within the sizer. This may # get overdrawn by the code below for item boundaries, but if # there is border padding then this will help make it more # obvious. dc.SetPen(wx.Pen(self.color3, 1)) for item in sizer.GetChildren(): if item.IsShown(): if item.IsWindow(): r = item.GetWindow().GetRect() elif item.IsSizer(): p = item.GetSizer().GetPosition() s = item.GetSizer().GetSize() r = wx.Rect(p,s) else: continue r = self.AdjustRect(tlw, win, r) dc.DrawRectangle(r) # Next highlight the area allocated to each item in the sizer. # Each kind of sizer will need to be done a little # differently. dc.SetPen(wx.Pen(self.color2, 1)) if isinstance(sizer, wx.WrapSizer): for item in sizer.GetChildren(): ir = self.AdjustRect(tlw, win, item.Rect) dc.DrawRectangle(ir) # wx.BoxSizer, wx.StaticBoxSizer elif isinstance(sizer, wx.BoxSizer): # NOTE: we have to do some reverse-engineering here for # borders because the sizer and sizer item don't give us # enough information to know for sure where item # (allocated) boundaries are, just the boundaries of the # actual widgets. TODO: It would be nice to add something # to wx.SizerItem that would give us the full bounds, but # that will have to wait until 2.9... x, y = rect.GetPosition() if sizer.Orientation == wx.HORIZONTAL: y1 = y + rect.height for item in sizer.GetChildren(): ir = self.AdjustRect(tlw, win, item.Rect) x = ir.x if item.Flag & wx.LEFT: x -= item.Border dc.DrawLine(x, y, x, y1) if item.IsSizer(): dc.DrawRectangle(ir) if sizer.Orientation == wx.VERTICAL: x1 = x + rect.width for item in sizer.GetChildren(): ir = self.AdjustRect(tlw, win, item.Rect) y = ir.y if item.Flag & wx.TOP: y -= item.Border dc.DrawLine(x, y, x1, y) if item.IsSizer(): dc.DrawRectangle(ir) # wx.FlexGridSizer, wx.GridBagSizer elif isinstance(sizer, wx.FlexGridSizer): sizer.Layout() y = rect.y for rh in sizer.RowHeights[:-1]: y += rh dc.DrawLine(rect.x, y, rect.x+rect.width, y) y+= sizer.VGap dc.DrawLine(rect.x, y, rect.x+rect.width, y) x = rect.x for cw in sizer.ColWidths[:-1]: x += cw dc.DrawLine(x, rect.y, x, rect.y+rect.height) x+= sizer.HGap dc.DrawLine(x, rect.y, x, rect.y+rect.height) # wx.GridSizer elif isinstance(sizer, wx.GridSizer): # NOTE: More reverse engineering (see above.) This time we # need to determine what the sizer is using for row # heights and column widths. #rh = cw = 0 #for item in sizer.GetChildren(): # rh = max(rh, item.Size.height) # cw = max(cw, item.Size.width) cw = (rect.width - sizer.HGap*(sizer.Cols-1)) / sizer.Cols rh = (rect.height - sizer.VGap*(sizer.Rows-1)) / sizer.Rows y = rect.y for i in range(sizer.Rows-1): y += rh dc.DrawLine(rect.x, y, rect.x+rect.width, y) y+= sizer.VGap dc.DrawLine(rect.x, y, rect.x+rect.width, y) x = rect.x for i in range(sizer.Cols-1): x += cw dc.DrawLine(x, rect.y, x, rect.y+rect.height) x+= sizer.HGap dc.DrawLine(x, rect.y, x, rect.y+rect.height) # Anything else is probably a custom sizer, just highlight the items else: del dc, odc for item in sizer.GetChildren(): self.HighlightSizerItem(item, sizer, 1) def FindHighlightPos(self, tlw, pos): if self.useOverlay: # We'll be using a ClientDC in this case so adjust the # position accordingly pos = tlw.ScreenToClient(pos) return pos def AdjustRect(self, tlw, win, rect): pos = self.FindHighlightPos(tlw, win.ClientToScreen(rect.Position)) rect.Position = pos return wx.Rect(pos, rect.Size) def DoHighlight(self, tlw, rect, colour, penWidth=2): if not tlw.IsFrozen(): tlw.Freeze() if self.useOverlay: dc = wx.ClientDC(tlw) dco = wx.DCOverlay(self.overlay, dc) dco.Clear() else: dc = wx.ScreenDC() dco = None dc.SetPen(wx.Pen(colour, penWidth)) dc.SetBrush(wx.TRANSPARENT_BRUSH) drawRect = wx.Rect(*rect) dc.DrawRectangle(drawRect) drawRect.Inflate(2,2) if not self.useOverlay: pos = tlw.ScreenToClient(drawRect.GetPosition()) drawRect.SetPosition(pos) wx.CallLater(self.highlightTime, self.DoUnhighlight, tlw, drawRect) return dc, dco def DoUnhighlight(self, tlw, rect): if not tlw: return if tlw.IsFrozen(): tlw.Thaw() if self.useOverlay: dc = wx.ClientDC(tlw) dco = wx.DCOverlay(self.overlay, dc) dco.Clear() del dc, dco self.overlay.Reset() else: tlw.RefreshRect(rect) def FlickerTLW(self, tlw): """ Use a timer to alternate a TLW between shown and hidded state a few times. Use to highlight a TLW since drawing and clearing an outline is trickier. """ self.flickerCount = 0 tlw.Hide() self.cl = wx.CallLater(300, self._Toggle, tlw) def _Toggle(self, tlw): if tlw.IsShown(): tlw.Hide() self.cl.Restart() else: tlw.Show() self.flickerCount += 1 if self.flickerCount < 4: self.cl.Restart() #--------------------------------------------------------------------------- from wx.lib.embeddedimage import PyEmbeddedImage Refresh = PyEmbeddedImage( "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" "GXlKb/xX4wAAAABJRU5ErkJggg==") Find = PyEmbeddedImage( "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAABgRJ" "REFUSIm1lU1oG9sVx/9z5440+kBWJUvGDZESXuskZPMIwVaoybNp4niXEChdJPDAIWk+IGnA" "i1Ioz9208apk10WcZFMI3Zgugrww6cNxKcakdoK/ghU7k5EsW2PLljXfc2duFw/5uaRv1x4Y" "uHc4c3/38P+fM8D/OYTDm7Gxsd/4vv/H169fQ5IkAIDjODh16hSy2ey3t27d6geAJ0+eFDVN" "G1xYWEA4HAYAeJ6H3t5eUEp/f+PGjZHPSOPj48P37t1j+XyeAzh4QqEQLxQK/Pr1639v5V67" "dq3Y29t7kEMI4aIo8lwux2/fvs3Gx8d/28qlrYXv+18RQsTNzU129epVWigUUC6X8fz5c8zN" "zUEQBKuVu7a2Zs7MzOD06dO4c+cOJicnUavVMDs7ywRBoIyxfgB/+A8ApXS7Xq8jkUjQCxcu" "4MqVK1hbW8OrV6/w6dMndHV1fXHmzJmvCSGs2WyeePPmDU6ePImbN2+CUgpVVVEqleju7i4o" "pdufVSDLMhhj0DQNMzMz2Nragu/72N7ehizLLJ1Od3me91wQBKRSKSSTSW9+fl56/PgxFhcX" "IQgCNE2DbdsIhUL4DOC6LjjnIIRAFEXU63VYloUgCBAEAVUUJTBN0wGAWCwW5pxLtm1jdXUV" "mqYhnU4fGIMxdgAgrcWHDx+aiqJAFEVks1l4nodisQjHcdDT04NsNvuPYrEYLRaL0Ww2++rc" "uXMwDAMTExM4duwYGGNwXRfVahUrKysHABEA7t69+7u3b9/ekmU50t3dDV3XMTExgUqlAlmW" "cfbsWdi2Td+9e3cEwIWurq6vkslk29LSEmq1GjRNQz6fR0dHByRJgqqq06VS6eUBoLu7+2+r" "q6s/2traYslkkszOzkJRFMiyjFwux8LhMNF1PWGa5rl4PP6zeDze5rouDMNg9Xqd7O3tQRRF" "ZDIZqKqKcrncdv/+/a2pqalFCgDhcPhjpVL50jRNWigU0N/fj0uXLkFVVayvr9OFhYVSNBot" "p1KpPgAol8tTjUajI5/PnxgYGIAoitB1HdVqFe/fv/dyudxPG43GXwD8FQDw8OHDuVQqxQcG" "BnitVuOGYfD19XU+PDzM29raOIBhAJFDDZgEcLuvr48risKbzSbXNI2PjIxwWZZ5LpfjDx48" "WD5wESEElFLoug5VVRGJRFAqlaDressZDIB7qPE9AL7jOFBVFYZhYGNjA3t7e5AkCYIggBDy" "vU0dx3FM04Smadjc3IQsy1heXoZpmq1Z8ysAg4cA4wB+7DgOKpUKPM/DysoKdnZ2YJomJEmC" "4zguAIhjY2MjL168+DmAeKFQQGdnJ2zbRrVaRb1ex/Lyssc57+jp6fnJ8ePHkc/ncfTo0S/K" "5XI2kUh43d3douu6KJfLkCQJ7e3taDQaqFQq4qNHj2KUMfbN4uIiLl686J8/f16sVqtIJpNw" "HAecc9i2LeVyOQwODm7ruv4tgCAej/dVq9WsYRiSaZpwHAe6riMajeLy5ctgjPkvX75sZ4x9" "Q6anp8E5RyaTET3Pw87ODmzbxs7ODhhjoJSCEIL9/f1/jY6O/mJ0dPSXzWbzn5RSuK6Ler0O" "27bRaDSgKAosy0ImkxEBYHp6GqQlimVZYIyBEALHcUAIgSB897vgnINzHjqkQbg1VgRBgOM4" "EAQBoVAInufBsr4bvJIkodUHKJVKkGUZrutid3cXhmHA9338UFBKYRgGVldXEQqFYJomLMvC" "3NwcSqXSwVyirRuKoohUKoVYLIZMJoOPHz8iCALIsvxfQUEQgFKKzs5OdHR0QFVVbG9vgzEG" "y7K+t2kkEkEQBFBVNVhaWiLRaBTxeByKoiAIAtRqNT+dTosA2g8d3k4pRb1e9+fn58V0Og1V" "VdFsNsE5h6Ioge/7JBKJgFqWNX/kyJETGxsbkampKXDOEQTBQYmSJInxeBwAploAQsjrWCx2" "VpIkcXJyEr7vQ5Kkw7qRzs5Ox7KsZQEAhoaG5iKRyJctcQ5HIpFAo9H487Nnz+4cfj80NPSn" "RCLx6/39/c++kWUZtm2vPH369NQPivi/in8Df18XwyUA5+QAAAAASUVORK5CYII=") ShowSizers = PyEmbeddedImage( "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAABChJ" "REFUSIm1lU1oXFUUx3/33nn3zbx5mcm0M5OZxGlNAn6XUqlRKIIWKrXqRlCLOwvitrooqKDg" "RhcirkSpigSECl0WBa104UKoSIoLS8TWlja1NTOTyUzz5s37uNdFWpumJSktHriry//8zj38" "z7nwP4dY5/7BUqn0quu62621i9cJhSjGcTzTarUOAr/dFn1sbGy/N+SdksgoQ2bh2mFBQpTz" "vdP1ev3AWjkya10qpe4yPbPZ3GeUecEMswQoYK4I3w+wzWiz3qgbtw0AQoHoswWfNxyYLYE2" "8GcVfr8IzU5gjOnfCWA5YuCvHPw0CRkLhGDjW5LeGiAFWjGcaYKyUHLAwvoeWQcgpczZjM3z" "A3CiD5fPAlikFXRicNy8lNJbK4das/A0VdKVJd/4oWqJZhSGVcJUeH3n5JA/NGe1OhEG/W+i" "KJq9LUAURe1KuVJQrrqnn4YVrXXkOE5gFCpf8NteLvdts9k8AgRXJDf0bC1ArlgsTiVJsqfX" "6+1K03RQLpenfd//tdvtbh0MBvdaaxe01pcGg8FFlq1wU8jNoqiUeq5Wqx3SWlutdWvTpk3v" "ANuBbY1G423Xdecdx7EjIyOHlVLPA6X1kl4lK6XU7rGxsU+UUkue54XlcvlL4LEVL36kUql8" "ns/nl6SUwejo6EGl1DNcM81/r7ihRfl8fmehUHil2Wzu1Vr3Xdc9lCTJZ4PB4DhXzAlcAOa0" "1koIMdntdqdKpZInhGjHcXx6ZT4FjDQajR21Wm2L7/sPJ0nyYr/ff1ZKOV8ul2eTJDnS6XSO" "sjwN4mp1cRw3s9lssVQq1cIwVEmSbNVa+9VqNTsyMjLh+/744uJiT3ied8BxnJcx6QMS0wkH" "UaEfJe6w4mc8v5AINWOS+KMgCGZWVuZ53taMlPtRaqovTRAv9LaTdQIn5y0oYzZkkaejOJ6m" "Xq8fk0IkCGVxci2UnsfNz1MatSDtUN7rT0xMvLa6lePj4/v8wlAfsCVUuJFMZxh1eQMqyIFF" "irRerx/LGGMCYa3ioV1f8el30/w4k8V178bNfMjHL3mcPREA0U1MES3ZNK2R6Z9katpgkpBU" "jFLovcnJRz8QF54wxgTXVoUQl9iBzz/bniQlT39JIdecQywICfEwQwFkd4KqdAmDS8gKEMLK" "XZTaOVImsbyOpM1QXiPkmgBAZBApeOEsS5OSjD8gJYsG6AFkhBA5C3D6+G72vudTGYXg8gYQ" "0J6DDB4sK67LLISjhXQ6xLm3+OXxU6RuBGxEM0sPLFkhRC5jjDmntD5D2N4jDr8LsMCV+bCQ" "t8Xhs0mStFYD4jhu55B/LEpx//vi/A5gieWdJLBoV+m2MeacAJ6uVqt7pZQNa+11v5MQohAE" "wdFut/sFcH4VY9T3/X2+7z9lre2t0mXTNP17fn7+6/V6fMfxL1klnkaQRVDaAAAAAElFTkSu" "QmCC") ShowFilling = PyEmbeddedImage( "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAZ5J" "REFUSIntlc9KAkEcxz+tu7OxLRSuGUXtIfUpukW9QJcO3nqELoLXLr2Bt47dRBAqgxJ6BqMw" "PJe0FEliaLNrFxV1zXatY9/TMDPf72eG3/yBf/2guaH2DrALrADtGfME8AKUgCsAdWhwX1XV" "bSnlMtCZEaApivrqeXJxEmBDSrl+dHQoNjf3OD9/xjSDJ5vmMre3Z1xeHi8AG/3+YcAH8GEY" "SbG6uoVtP2IYwQFLS2s8PT0MciYBALi5uUfTrrEsB88LDtB1C027A+h+N6cAvBUK+e6surg4" "6wLvvSwAlHFKu/0ZfNkBvD6AEGJmwCSvrwaNRgOAZrMZKtw0zYF3KiCXy1Eul2m1WqEAhmFQ" "q9VgrMg+QKVSoVqt4oU5QoCiKHQ6/vvpA2QyGdLpNPV6PRQgHo+Tz+fJZrPTAYlEgmQyiW3b" "oQBCCFKpFIy+b36ArusDQ1j1vCM18B3TaDQaOrgvy7Jgyg7mgflisQiA4zihwmOxGKVSCUDv" "ZTFOO/mL5zoSiby6rnsFHMDoDk6llA6//HBc1+1/OP8Kpi8497f1tG0HzQAAAABJRU5ErkJg" "gg==") Icon = PyEmbeddedImage( "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABHNCSVQICAgIfAhkiAAAALhJ" "REFUWIXtl80SgyAMhHeR99Y+eJseLAdHbJM0TjiwN2dI+MgfQpYFmSqpu0+AEQDqrwXyeorH" "McvCEIBdm3F7/fr0FKgBRFaIrHkAdykdQFmEGm2HL233BAIAYmxYEqjePo9SBYBvBKppclDz" "prMcqAhbAtknJx+3AKRHgGhnv4iApQY+jtSWpOY27BnifNt5uyk9BekAoZNwl21yDBSBi/63" "yOMiLAXaf8AuwP9n94vzaTYBsgHeht4lXXmb7yQAAAAASUVORK5CYII=") CollapseTree = PyEmbeddedImage( "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAf9J" "REFUSIm9lD9r20AYhx9Fxe5SKDIynVJodFMaChkC6eBCJ+WWdulSb1o6RR8gH6AfQF5aSksX" "aQ0eajwVkinO1MFk0YXg0uLSISZDoQhcddAf5MR241buD4670/tyD+/vXh0sWdqUb1vAQ2Bj" "Suwb0E7Xx38DaAKPgTuAnJLfSSEAn4DWIoAm8ByQruti2zYAlmUBoJSi2+3ieV4R1v0TJANs" "AS8Ap9PpYFkWUSSuJFcqIUopAKSUAO+A18yxS0/nZ8AD13WFbdtEkWB9Hep1ODqC0QgMA8bj" "GqYJhmGgaRq9Xm8I3AY+zgKspPMGIIuHZ6qn4/Q02UeRIIpEZqEkua+ZWpkXLEM3ipvEe9C0" "ad2bqN+P89zr6P9WoJRidVXQ78e55/U09h1YW5vMTTWcB8i66B7wq1arie1ti/G4hmEknfNl" "BD8Kh1cqIbp+ThAEVKtVBoNBCziZBfjX/wDgEHg0C7D0O8gs+grcAm76vi80TcM04eJCYZqg" "6+ecnR0TBAGu6+L7PlJKhAgJQ+6SvF/vrwNsAm+BD0B8eTQajXztOMT7HnFrL48fpGNCizzX" "Q5IXdDfdN/Y92Hna5s2rJ+y+zPPm3skiOoCkip+f23Fr70o1pWgCkoGKkKV3URnKqyjaxTKs" "omCX4+SQ0pS1aew4xJub5VZwGZQdfv83yOfTR/iA1xwAAAAASUVORK5CYII=") ExpandTree = PyEmbeddedImage( "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAepJ" "REFUSIm1lDFr20AYhp+ri+iSxUUhW4dIU9wlQ4YOKnRSj4K7Z9NSCFg/ID+gP0BZWkpLF3kN" "Giq0pkOpMxWSTFIIFIqHQCFbUQnXQTpFslXXptYLx90nvdzD9913Bx1LtHzbA54Aj1v+nQFf" "yvXpMoD7M/E+8AzYAmSLP66BbSBcBTACXED6vo/rugBYlgVAlmUkSSKDIND+rXJeCNEl2gNe" "AV4cx1iWRZ7bc2bDSMmyDAApJcAH4C0LytUr5wPA8n3fdl2XPLfZ2YHNTbi+vjPf3j7ENKHf" "7yOEYDKZTIHfwNe/Ae7V0pX1zbUGA8FgILi8LOI8t8lzW5dQ0t4Mc4DO1ADoA724ACEEQtx1" "8XBYZDLrXQnQhRoA3SEAUaSIItWIz89Vm3e6DOAMiJMkwTBSALa3i6Gl14aRYhgpSZLgOA7A" "t0WA/70HAJ+Bp//KoDPpi/YD2AAehGFoCyEwTbi5yTBN6PV+cnV1yng8xvd9wjBESoltp6Qp" "jyjer4/LAPeB98AnQM0Ox3GqteehjgPU0WH1/6QcDa3yXE8pDnRUxs5xAM9fRrx7M2T0uvIt" "PJNVdAJFFr++R+rocC6btagB0aA6pPMuWoeqLOrlootSUSuX51WQtUm3qfI81O7uejOYBenN" "X/wBVz/ONKbGYPkAAAAASUVORK5CYII=") HighlightItem = PyEmbeddedImage( "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAQZJ" "REFUSInFlTGSgjAUhv8XuIRl9ga0XgmuoI5abwfH8Ai22GlJZ0otuQD5t3DZ1V1CwgzgP8OQ" "QCZfkv+9FxEVYUrFbYO2oW+wqEiGAtRzhyRIQh9eH+RXAMBmvfIuohcwheLnTnZ6vM3NjAaQ" "1mTahvrAHwCzj+BJVI83sesHAMjRM3OVgNkFm/WK292+EzKvB86zr5Lu76b2AubdAbqMda0+" "UOIqFdY2lKMHYGrw06DL3Tbrxzmi/Iq0JNLyO/Pxm/Uze/BXVRIUKajvKM6AXuh/kfjeHTC7" "TAdw1RfahmlJFOewgtjvQY/0QgeNe3MUOVQsw2/OwQBRkQy5Op2lYixN7sEXVhRd4PXVHvwA" "AAAASUVORK5CYII=") EvtWatcher = PyEmbeddedImage( "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")