Skip to content

Commit

Permalink
[DEBUG] Implement canvas item enumeration and configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
ObaraEmmanuel committed Feb 10, 2025
1 parent e8dc055 commit d49729b
Show file tree
Hide file tree
Showing 5 changed files with 284 additions and 26 deletions.
2 changes: 1 addition & 1 deletion studio/debugtools/debugger.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ def handle_msg(self, msg):
widget.deleted = True
if event == "<<SelectionChanged>>":
self.elements.element_pane.on_widget_tap(
widget, msg.payload["event_obj"]
widget, msg.payload["event_obj"], msg.payload.get("data")
)
suppress = True
if event == "<<WidgetModified>>":
Expand Down
139 changes: 138 additions & 1 deletion studio/debugtools/defs.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from dataclasses import dataclass, field

import studio.lib.menu as menu_lib
import studio.lib.canvas as canvas_lib
from studio.debugtools.common import get_studio_equiv, get_root_id


Expand Down Expand Up @@ -74,6 +75,93 @@ def __init__(self, event):
self.y_root = event.y_root


class RemoteCanvasItem:
DEF_OVERRIDES = dict(canvas_lib.CANVAS_PROPERTIES)
_item_type_map = {
"arc": canvas_lib.Arc,
"bitmap": canvas_lib.Bitmap,
"image": canvas_lib.Image,
"line": canvas_lib.Line,
"oval": canvas_lib.Oval,
"polygon": canvas_lib.Polygon,
"rectangle": canvas_lib.Rectangle,
"text": canvas_lib.Text,
"window": canvas_lib.Window,
}

def __init__(self, canvas, item_id):
self.canvas = canvas
self.item_id = item_id
self._attr_cache = None
self._name = None
self._equiv_class = None
self.deleted = False
self._dbg_node = None
self.extra_items = []
self._class = RemoteCanvasItem

@property
def id(self):
return f"{self.canvas.id}!{self.item_id}"

@property
def equiv_class(self):
if self._equiv_class is None:
self._equiv_class = self._item_type_map.get(self.type(), canvas_lib.CanvasItem)
return self._equiv_class

@property
def name(self):
if self._name is None:
self._name = f"{self.type()}_{self.item_id}"
return self._name

def _call(self, meth, *args, **kwargs):
return self.canvas.debugger.transmit(
Message(
"WIDGET",
payload={
"id": self.canvas.id,
"root": self.canvas.root,
"meth": meth,
"args": (self.item_id, *args),
"kwargs": kwargs,
}
), response=True
)

def configure(self, **kwargs):
ret = self._call("itemconfigure", **kwargs)
if not kwargs and isinstance(ret, dict):
self._attr_cache = {k: v[-1] if isinstance(v, (tuple, list, set)) else v for k, v in ret.items()}
return ret

config = configure

def cget(self, key):
if self._attr_cache is not None:
if key in self._attr_cache:
return self._attr_cache[key]
return self._call("itemcget", key)

__getitem__ = cget

def __setitem__(self, key, value):
return self._call("itemconfigure", key, value)

def invalidate_conf(self):
self._attr_cache = None

def type(self):
return self._call("type")

def winfo_children(self):
return []

def winfo_ismapped(self):
return False


class RemoteMenuItem:

_pool = []
Expand All @@ -96,6 +184,7 @@ def __init__(self, menu, index):
self._attr_cache = None
self._class = RemoteMenuItem
self.menu_items = []
self.extra_items = []

@property
def id(self):
Expand Down Expand Up @@ -158,7 +247,7 @@ def __setitem__(self, key, value):
return self._call("entryconfigure", key, value)

def invalidate_conf(self):
pass
self._attr_cache = None

def type(self):
return self._call("type")
Expand Down Expand Up @@ -197,6 +286,7 @@ def __init__(self, id_, debugger, root=0):
self._prop_map = {}
self._attr_cache = None
self._menu_items = None
self._canvas_items = None
self.deleted = False
self.root = root
self.equiv_class = get_studio_equiv(self)
Expand Down Expand Up @@ -332,6 +422,53 @@ def menu_items(self):
self._init_menu_items()
return self._menu_items

def _init_canvas_items(self):
self._canvas_items = {i: RemoteCanvasItem(self, i) for i in self.find_all()}

def get_canvas_item_from_id(self, item_id):
if self._canvas_items is None:
self._init_canvas_items()
return self._canvas_items.get(item_id)

def delete_canvas_ids(self, *ids):
if self._canvas_items is None:
return []
removed = []
for id in ids:
item = self._canvas_items.pop(int(id), None)
if item:
item.deleted = True
removed.append(item)
return removed

def add_canvas_item(self, id):
if not self._canvas_items:
self._canvas_items = {}
if id in self._canvas_items:
return self._canvas_items[id]
item = RemoteCanvasItem(self, id)
self._canvas_items[id] = item
return item

@property
def canvas_items(self):
if self._class != tkinter.Canvas:
return []
if self._canvas_items is None:
self._init_canvas_items()
return list(self._canvas_items.values())

@property
def extra_items(self):
if self._class == tkinter.Menu:
return self.menu_items
if self._class == tkinter.Canvas:
return self.canvas_items
return []

def find_all(self):
return self._call("find_all")

def keys(self):
return self._call("keys")

Expand Down
99 changes: 76 additions & 23 deletions studio/debugtools/element_pane.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from hoverset.ui.icons import get_icon_image
from hoverset.ui.widgets import Button, ToggleButton, Label
from studio.debugtools.defs import RemoteWidget, RemoteMenuItem
from studio.debugtools.defs import RemoteWidget
from studio.feature.component_tree import ComponentTreeView
from studio.i18n import _
from studio.ui.tree import MalleableTreeView
Expand Down Expand Up @@ -49,7 +49,7 @@ def loaded(self):
def update_preload_status(self, added):
if self._loaded or self.widget.deleted:
return
if added or self.widget.winfo_children() or self.widget.menu_items:
if added or self.widget.winfo_children() or self.widget.extra_items:
# widget can expand
self._set_expander(self.COLLAPSED_ICON)
else:
Expand All @@ -58,9 +58,7 @@ def update_preload_status(self, added):
def extract_name(self, widget):
if isinstance(widget, RemoteWidget):
return str(widget._name).strip("!")
if isinstance(widget, RemoteMenuItem):
return widget.name or "-"
return 'root'
return widget.name or "-"

def load(self):
# lazy loading
Expand All @@ -71,14 +69,14 @@ def load(self):
if getattr(child, "_dbg_ignore", False):
continue
self.add_as_node(widget=child).update_preload_status(False)
if self.widget._class == tkinter.Menu:
for item in self.widget.menu_items:
if item._dbg_node:
self.add(item._dbg_node)
self.set_widget(item)
item._dbg_node.update_preload_status(False)
else:
self.add_as_node(widget=item).update_preload_status(False)

for item in self.widget.extra_items:
if item._dbg_node:
self.add(item._dbg_node)
self.set_widget(item)
item._dbg_node.update_preload_status(False)
else:
self.add_as_node(widget=item).update_preload_status(False)
self._loaded = True

def expand(self):
Expand Down Expand Up @@ -158,6 +156,8 @@ def __init__(self, master, debugger):
self.debugger.bind("<<MenuItemModified>>", self.on_menu_item_modified, add=True)
self.debugger.bind("<<MenuItemAdded>>", self.on_menu_item_added)
self.debugger.bind("<<MenuItemRemoved>>", self.on_menu_items_removed)
self.debugger.bind("<<CanvasItemCreated>>", self.on_canvas_item_created, add=True)
self.debugger.bind("<<CanvasItemsDeleted>>", self.on_canvas_items_deleted, add=True)

@property
def selected(self):
Expand All @@ -166,14 +166,20 @@ def selected(self):
def on_select(self):
self.debugger.selection.set(map(lambda node: node.widget, self._tree.get()))

def on_widget_tap(self, widget, event):
def on_widget_tap(self, widget, event, data=None):
self._select_btn.toggle()
if widget:
# bring debugger to front
self.debugger.attributes('-topmost', True)
self.debugger.attributes('-topmost', False)
self.debugger.focus_force()
node = self._tree.expand_to(widget)
if widget._class == tkinter.Canvas and data:
item_id = int(data)
node.expand()
item = widget.get_canvas_item_from_id(item_id)
node = item._dbg_node

if node:
self._tree.see(node)
node.select(event)
Expand Down Expand Up @@ -206,10 +212,14 @@ def on_menu_item_added(self, event):
widget, root, index = event.user_data.split(" ")
widget = self.debugger.widget_from_id(widget, int(root))
parent_node = getattr(widget, "_dbg_node", None)
if not parent_node or not parent_node.loaded:
if not parent_node:
return

item = widget._add_menu_item(int(index))
if not parent_node.loaded:
parent_node.update_preload_status(True)
return

node = parent_node.add_as_node(widget=item) if item._dbg_node is None else item._dbg_node
if not item._dbg_node:
# remove node if it was just created
Expand All @@ -225,18 +235,22 @@ def on_menu_items_removed(self, event):
parent_node = getattr(widget, "_dbg_node", None)

items = widget._remove_menu_items(int(index1), int(index2))
if not parent_node:
return

if not parent_node.loaded:
parent_node.update_preload_status(False)
return

had_selection = False
for item in items:
if parent_node.loaded:
if item._dbg_node in self.selected:
had_selection = True
self._tree.toggle_from_selection(item._dbg_node)
parent_node.remove(item._dbg_node)
else:
parent_node.update_preload_status(False)
if item._dbg_node in self.selected:
had_selection = True
self._tree.toggle_from_selection(item._dbg_node)
parent_node.remove(item._dbg_node)

if had_selection and not self.selected:
self._tree.see(self.debugger.root._dbg_node)
self._tree.see(parent_node)

def on_widget_modified(self, _):
if self.debugger.active_widget not in self.selected:
Expand All @@ -252,6 +266,45 @@ def on_menu_item_modified(self, event):
if item._dbg_node:
item._dbg_node.set_widget(item)

def on_canvas_item_created(self, event):
widget, root, id = event.user_data.split(" ")
widget = self.debugger.widget_from_id(widget, int(root))
parent_node = getattr(widget, "_dbg_node", None)
if not parent_node:
return

item = widget.add_canvas_item(int(id))
if not parent_node.loaded:
parent_node.update_preload_status(True)
return

node = parent_node.add_as_node(widget=item)
node.set_widget(item)
item._dbg_node = node

def on_canvas_items_deleted(self, event):
widget, root, *ids = event.user_data.split(" ")
widget = self.debugger.widget_from_id(widget, int(root))
parent_node = getattr(widget, "_dbg_node", None)
items = widget.delete_canvas_ids(*ids)

if not parent_node:
return

if not parent_node.loaded:
parent_node.update_preload_status(False)
return

had_selection = False
for item in items:
if item._dbg_node in self.selected:
had_selection = True
self._tree.toggle_from_selection(item._dbg_node)
parent_node.remove(item._dbg_node)

if had_selection and not self.selected:
self._tree.see(parent_node)

def on_widget_map(self, _):
pass

Expand Down
Loading

0 comments on commit d49729b

Please sign in to comment.