Skip to content

Commit

Permalink
[STUDIO] Integrate menu functionality with the studio
Browse files Browse the repository at this point in the history
  • Loading branch information
ObaraEmmanuel committed Nov 1, 2024
1 parent cc4b88a commit 1ea139e
Show file tree
Hide file tree
Showing 24 changed files with 1,143 additions and 81 deletions.
2 changes: 1 addition & 1 deletion formation/handlers/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def get_layout_handler(parent_node, parent):

def handle(widget, config, **kwargs):
parent = kwargs.get("parent")
if parent is None or isinstance(widget, (tk.Tk, tk.Toplevel)):
if parent is None or isinstance(widget, (tk.Tk, tk.Toplevel, tk.Menu)):
return
parent_node = kwargs.get("parent_node")
layout = get_layout_handler(parent_node, parent)
Expand Down
22 changes: 21 additions & 1 deletion formation/handlers/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,30 @@ def handle(cls, widget, config, **kwargs):
handle_method(**{prop: builder._get_var(str(properties[prop])) or ''})


class WidgetHandler:
widget_props = (
"menu",
)

@classmethod
def handle(cls, widget, config, **kwargs):
builder = kwargs.get("builder")
properties = kwargs.get("extra_config", {})
handle_method = kwargs.get("handle_method", getattr(widget, "config", None))

if handle_method is None:
# no way to assign the variable so just stop here
return
for prop in properties:
# defer assignments to the builder
builder._deferred_props.append((prop, properties[prop], handle_method))


_common_redirect = {
**{prop: image for prop in image.image_props},
**{prop: VariableHandler for prop in VariableHandler.variable_props},
**{prop: command for prop in command.command_props},
**{prop: WidgetHandler for prop in WidgetHandler.widget_props}
}


Expand Down Expand Up @@ -61,7 +81,7 @@ def handle_method(**conf):

class AttrHandler:
_ignore = (
"layout", "menu"
"layout"
)

_redirect = _common_redirect
Expand Down
62 changes: 44 additions & 18 deletions formation/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@
tk.Tk,
)

_menu_containers = (
tk.Menubutton,
ttk.Menubutton,
tk.Toplevel,
tk.Tk,
)

_menu_item_types = (
tk.CASCADE,
tk.COMMAND,
Expand All @@ -58,8 +65,21 @@
tk.RADIOBUTTON,
)

_canvas_item_types = (
"Arc",
"Bitmap",
"Image",
"Line",
"Oval",
"Polygon",
"Rectangle",
"Text",
"Window"
)

_ignore_tags = (
*_menu_item_types,
*_canvas_item_types,
"event",
"grid",
"meta",
Expand Down Expand Up @@ -139,11 +159,11 @@ class MenuLoaderAdapter(BaseLoaderAdapter):
def load(cls, node, builder, parent):
cls._load_required_fields(node)
widget = BaseLoaderAdapter.load(node, builder, parent)
cls._menu_load(node, builder, None, widget)
cls._menu_load(node, builder, widget)
return widget

@classmethod
def _menu_load(cls, node, builder, menu=None, widget=None):
def _menu_load(cls, node, builder, menu):
for sub_node in node:
if sub_node.type in _ignore_tags and sub_node.type not in _menu_item_types or sub_node.is_var():
continue
Expand All @@ -160,13 +180,12 @@ def _menu_load(cls, node, builder, menu=None, widget=None):
menu.add(sub_node.type)
index = menu.index(tk.END)
dispatch_to_handlers(menu, attrib, **kwargs, menu=menu, index=index)
elif cls._get_class(sub_node) == tk.Menu:
obj_class = cls._get_class(sub_node)
menu_obj = obj_class(widget)
if widget:
widget.configure(menu=menu_obj)
dispatch_to_handlers(menu_obj, attrib, **kwargs)
elif menu:
continue

obj_class = cls._get_class(sub_node)
if issubclass(obj_class, tk.Menu):
menu_obj = obj_class(menu)
if menu:
menu.add(tk.CASCADE, menu=menu_obj)
index = menu.index(tk.END)
dispatch_to_handlers(
Expand Down Expand Up @@ -194,7 +213,7 @@ class CanvasLoaderAdapter(BaseLoaderAdapter):
def load(cls, node, builder, parent):
canvas = BaseLoaderAdapter.load(node, builder, parent)
for sub_node in node:
if sub_node.type in _ignore_tags or sub_node.is_var():
if sub_node.type not in _canvas_item_types:
continue
# just additional options that may be needed down the line
kwargs = {
Expand Down Expand Up @@ -237,10 +256,7 @@ class Builder:
"""

_adapter_map = {
tk.Menubutton: MenuLoaderAdapter,
ttk.Menubutton: MenuLoaderAdapter,
tk.Tk: MenuLoaderAdapter,
tk.Toplevel: MenuLoaderAdapter,
tk.Menu: MenuLoaderAdapter,
tk.Canvas: CanvasLoaderAdapter,
# Add custom adapters here
}
Expand All @@ -258,6 +274,7 @@ def __init__(self, parent, **kwargs):
path = kwargs.get("path")
self._path = path if path is None else os.path.abspath(path)
self._meta = {}
self._deferred_props = []

if kwargs.get("node"):
self.load_node(kwargs.get("node"))
Expand All @@ -270,6 +287,13 @@ def __init__(self, parent, **kwargs):
self.load_path(self._path)

Meth.call_deferred(self)
self._apply_deferred_props()

def _apply_deferred_props(self):
for prop, value, handle_method in self._deferred_props:
value = getattr(self, value, None)
if value:
handle_method(**{prop: value})

def _arg_parser(self, a, t):
if t == "image":
Expand Down Expand Up @@ -342,15 +366,17 @@ def _load_meta(self, node, builder):
def _load_widgets(self, node, builder, parent):
adapter = self._get_adapter(BaseLoaderAdapter._get_class(node))
widget = adapter.load(node, builder, parent)
if not isinstance(widget, _containers):
# We don't need to load child tags of non-container widgets
if isinstance(widget, tk.Menu):
# old-style menu format, so assign it to parent "menu" attribute
if node.attrib.get("name") is None:
# old-style menu format, so assign it to parent "menu" attribute
if isinstance(parent, _menu_containers):
parent.configure(menu=widget)
return widget
for sub_node in node:
if sub_node.is_var() or sub_node.type in _ignore_tags:
# ignore variables and non widgets
continue
if BaseLoaderAdapter._get_class(sub_node) == tk.Menu:
continue
self._load_widgets(sub_node, builder, widget)
return widget

Expand Down
1 change: 1 addition & 0 deletions formation/tests/samples/all_legacy.xml
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,5 @@
layout:height="30" layout:x="484" layout:y="446"/>
<tkinter.Menubutton name="Menubutton_1" attr:padx="5" attr:pady="4" attr:text="Menubutton_1" layout:width="80"
layout:height="30" layout:x="477" layout:y="323"/>
<tkinter.Menu name="Menu_1" attr:activeborderwidth="1" attr:borderwidth="1" attr:cursor="" attr:font="{Segoe UI} 9" attr:tearoff="0"/>
</tkinter.Frame>
60 changes: 60 additions & 0 deletions formation/tests/samples/menu.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?xml version='1.0' encoding='utf-8'?>
<tkinter.Tk xmlns:attr="http://www.hoversetformationstudio.com/styles/" xmlns:layout="http://www.hoversetformationstudio.com/layouts/" xmlns:menu="http://www.hoversetformationstudio.com/menu" name="tk_1" attr:menu="menu_3" attr:layout="place" layout:width="302" layout:height="301" layout:x="30" layout:y="30">
<meth name="title">
<arg value="title"/>
</meth>
<meth name="geometry">
<arg value="302x268+0+0"/>
</meth>
<meta major="7" minor="0" name="version"/>
<tkinter.StringVar attr:name="str_var" attr:value=""/>
<tkinter.Menu name="menu_3" attr:activeborderwidth="1" attr:borderwidth="1" attr:cursor="" attr:font="{Segoe UI} 9">
<tkinter.Menu attr:activeborderwidth="1" attr:borderwidth="1" attr:font="{Segoe UI} 9" attr:tearoff="0" menu:compound="none" menu:label="cascade_3" menu:state="normal">
<command menu:compound="none" menu:label="command_11" menu:state="normal"/>
<command menu:compound="none" menu:label="command_14" menu:state="normal"/>
</tkinter.Menu>
<tkinter.Menu attr:activeborderwidth="1" attr:borderwidth="1" attr:font="{Segoe UI} 9" attr:tearoff="0" menu:compound="none" menu:label="cascade_4" menu:state="normal">
<command menu:compound="none" menu:label="command_12" menu:state="normal"/>
<command menu:compound="none" menu:label="command_15" menu:state="normal"/>
</tkinter.Menu>
<tkinter.Menu attr:activeborderwidth="1" attr:borderwidth="1" attr:font="{Segoe UI} 9" attr:tearoff="0" menu:compound="none" menu:label="cascade_5" menu:state="normal">
<command menu:compound="none" menu:label="command_13" menu:state="normal"/>
<command menu:compound="none" menu:label="command_16" menu:state="normal"/>
</tkinter.Menu>
</tkinter.Menu>
<tkinter.Menubutton name="menubutton_1" attr:menu="menu_1" attr:padx="5" attr:pady="4" attr:text="menubutton_1" layout:width="166" layout:height="25" layout:x="69" layout:y="36" layout:bordermode="outside">
<tkinter.Menu name="menu_1" attr:activeborderwidth="1" attr:borderwidth="1" attr:font="{Segoe UI} 9" attr:tearoff="0">
<command menu:background="green" menu:compound="none" menu:label="command_1" menu:state="normal"/>
<command menu:compound="none" menu:label="command_2" menu:state="normal"/>
<command menu:compound="none" menu:label="command_3" menu:state="disabled"/>
<checkbutton menu:variable="str_var" menu:compound="none" menu:label="check button_1" menu:state="normal"/>
<radiobutton menu:value="radio button_1" menu:variable="" menu:compound="none" menu:label="radio button_1" menu:state="normal"/>
<separator/>
<tkinter.Menu attr:activeborderwidth="1" attr:borderwidth="1" attr:font="{Segoe UI} 9" attr:tearoff="0" menu:compound="none" menu:label="cascade_1" menu:state="normal">
<command menu:compound="none" menu:label="command_4" menu:state="normal"/>
<checkbutton menu:compound="none" menu:label="check button_2" menu:state="normal"/>
<radiobutton menu:value="radio button_2" menu:variable="" menu:compound="none" menu:label="radio button_2" menu:state="normal"/>
<separator/>
<tkinter.Menu attr:activeborderwidth="1" attr:background="red" attr:borderwidth="1" attr:font="{Segoe UI} 9" attr:tearoff="0" menu:compound="none" menu:label="cascade_2" menu:state="normal">
<command menu:compound="none" menu:label="command_5" menu:state="normal"/>
<command menu:compound="none" menu:label="command_6" menu:state="normal"/>
</tkinter.Menu>
</tkinter.Menu>
</tkinter.Menu>
</tkinter.Menubutton>
<tkinter.ttk.Menubutton name="menubutton_2" attr:menu="menu_2" attr:text="menubutton_2" layout:width="180" layout:height="25" layout:x="68" layout:y="91" layout:bordermode="outside">
<tkinter.Menu name="menu_2" attr:activeborderwidth="1" attr:borderwidth="1" attr:font="{Segoe UI} 9" attr:foreground="grey20" attr:postcommand="test_command" attr:tearoffcommand="test_command" attr:title="menu_2">
<command menu:command="test_command" menu:compound="none" menu:label="command_7" menu:state="normal"/>
<command menu:compound="none" menu:label="command_8" menu:state="normal"/>
<command menu:compound="none" menu:label="command_9" menu:state="normal"/>
<command menu:compound="none" menu:label="command_10" menu:state="normal"/>
</tkinter.Menu>
</tkinter.ttk.Menubutton>
<tkinter.ttk.Menubutton name="menubutton_3" attr:text="menubutton_3" layout:width="185" layout:height="25" layout:x="63" layout:y="149" layout:bordermode="outside">
<tkinter.Menu attr:activeborderwidth="1" attr:borderwidth="1" attr:font="{Segoe UI} 9">
<command menu:compound="none" menu:label="command_1" menu:state="normal"/>
<command menu:compound="none" menu:label="command_2" menu:state="normal"/>
<command menu:compound="none" menu:label="command_3" menu:state="normal"/>
</tkinter.Menu>
</tkinter.ttk.Menubutton>
</tkinter.Tk>
2 changes: 1 addition & 1 deletion formation/tests/support.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
tk_supported = {
tk.Button, tk.Checkbutton, tk.Label, tk.Menubutton, tk.Scrollbar,
tk.Canvas, tk.Frame, tk.LabelFrame, tk.Listbox, tk.PanedWindow,
tk.Entry, tk.Message, tk.Radiobutton, tk.Scale, tk.Spinbox, tk.Text
tk.Entry, tk.Message, tk.Radiobutton, tk.Scale, tk.Spinbox, tk.Text, tk.Menu
}

ttk_supported = {
Expand Down
62 changes: 62 additions & 0 deletions formation/tests/test_menu.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import tkinter
import unittest

from formation import AppBuilder
from formation.tests.support import get_resource


class MenuTestCase(unittest.TestCase):
builder = None

@classmethod
def setUpClass(cls) -> None:
cls.builder = AppBuilder(path=get_resource("menu.xml"))

@classmethod
def tearDownClass(cls) -> None:
cls.builder._app.destroy()

def test_command(self):
pass

def test_menubutton_legacy(self):
mb1 = self.builder.menubutton_1
self.assertEqual(mb1.nametowidget(mb1.cget("menu")), self.builder.menu_1)

def test_menubutton_native(self):
mb2 = self.builder.menubutton_2
self.assertEqual(mb2.nametowidget(mb2.cget("menu")), self.builder.menu_2)

def test_toplevel(self):
tk1 = self.builder.tk_1
self.assertEqual(tk1.nametowidget(tk1.cget("menu")), self.builder.menu_3)

def test_menuitem_config(self):
m1 = self.builder.menu_1
self.assertEqual(str(m1.entrycget(0, "background")), "green")
self.assertEqual(m1.entrycget(1, "label"), "command_2")
self.assertEqual(str(m1.entrycget(2, "state")), "disabled")
self.assertEqual(str(m1.entrycget(3, "variable")), str(self.builder.str_var))

def test_cascade_menu_config(self):
m1 = self.builder.menu_1
m2 = m1.nametowidget(m1.entrycget(6, "menu"))
m3 = m2.nametowidget(m2.entrycget(4, "menu"))
self.assertEqual(m3.cget("tearoff"), 0)
self.assertEqual(str(m3.cget("background")), "red")

def test_menu_config(self):
m2 = self.builder.menu_2
self.assertEqual(str(m2.cget("foreground")), "grey20")
self.assertEqual(m2.cget("title"), "menu_2")

def test_command_binding(self):
self.builder.connect_callbacks(self)
m2 = self.builder.menu_2
self.assertNotEqual(m2.cget("postcommand"), '')
self.assertNotEqual(m2.cget("tearoffcommand"), '')
self.assertNotEqual(m2.entrycget(1, "command"), '')

def test_old_format_backwards_compatibility(self):
mb3 = self.builder.menubutton_3
self.assertIsInstance(mb3.nametowidget(mb3.cget("menu")), tkinter.Menu)
1 change: 1 addition & 0 deletions hoverset/ui/themes/default.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
primaryaccent: #3a3a3a;
accent: #3d8aff;
secondary1: #3dff3d;
secondary2: #ff743d;

}
.spinbox{
Expand Down
2 changes: 1 addition & 1 deletion hoverset/ui/themes/light.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
primary: #202020;
primaryaccent: #e0e0e0;
accent: #228b22;
secondary1: #224c8c;
secondary1: #8c227e;

}
.spinbox{
Expand Down
3 changes: 2 additions & 1 deletion hoverset/ui/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -1340,7 +1340,8 @@ def on_configure(self, *_):
def clear_children(self):
# Unmap all children from the frame
for child in self.body.winfo_children():
child.pack_forget()
if hasattr(child, "pack_forget"):
child.pack_forget()

def _detect_change(self, flag=True):
# Lets set up the frame to listen to changes in size and update the scrollbars
Expand Down
Loading

0 comments on commit 1ea139e

Please sign in to comment.