Skip to content

Commit

Permalink
Merge branch 'feature-customtkinter'
Browse files Browse the repository at this point in the history
  • Loading branch information
ObaraEmmanuel committed May 1, 2024
2 parents 1497b6c + 38797b3 commit 89160a9
Show file tree
Hide file tree
Showing 22 changed files with 1,511 additions and 72 deletions.
2 changes: 1 addition & 1 deletion formation/formats/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
from formation.formats._json import JSONFormat

FORMATS = (
JSONFormat,
XMLFormat,
JSONFormat
)


Expand Down
2 changes: 2 additions & 0 deletions formation/formats/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ def _flatten(self, dictionary):
continue
dictionary[k] = self._flatten(dictionary[k])
return dict(dictionary)
if isinstance(dictionary, (list, tuple, set)):
return list(map(self._flatten, dictionary))
return str(dictionary)


Expand Down
52 changes: 46 additions & 6 deletions formation/formats/_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,32 @@
from formation.formats._base import BaseFormat, Node


class ArrayEncoder(json.JSONEncoder):
def encode(self, obj):
def hint_arrays(item):
if isinstance(item, tuple):
return {'__tuple__': True, 'items': item}
if isinstance(item, set):
return {"__set__": True, "items": item}
if isinstance(item, list):
return [hint_arrays(e) for e in item]
if isinstance(item, dict):
return {key: hint_arrays(value) for key, value in item.items()}
else:
return item

return super(ArrayEncoder, self).encode(hint_arrays(obj))


def hinted_array_hook(obj):
if '__tuple__' in obj:
return tuple(obj['items'])
if '__set__' in obj:
return set(obj['items'])
else:
return obj


class JSONFormat(BaseFormat):
extensions = ["json"]
name = "JSON"
Expand All @@ -17,13 +43,25 @@ def _load_node(self, parent, data: dict) -> Node:
self._load_node(node, child)
return node

def can_serialize(self, val):
try:
json.dumps(val)
return True
except (TypeError, OverflowError):
return False

def _normalize(self, attrib, stringify=False):
for key in attrib:
if isinstance(attrib[key], dict):
attrib[key] = self._normalize(attrib[key], stringify)
else:
if stringify or not isinstance(attrib[key], (int, float, bool, type(None))):
attrib[key] = str(attrib[key])
if stringify:
if isinstance(attrib[key], (list, tuple, set)):
attrib[key] = " ".join(map(str, attrib[key]))
else:
attrib[key] = str(attrib[key])
elif isinstance(attrib[key], (list, tuple, set)):
attrib[key] = type(attrib[key])(map(lambda x: str(x) if not self.can_serialize(x) else x, attrib[key]))
return attrib

def _to_dict(self, node: Node) -> dict:
Expand All @@ -39,11 +77,11 @@ def _to_dict(self, node: Node) -> dict:
def load(self):
if self.path:
with open(self.path, "rb") as file:
json_dat = json.load(file)
json_dat = json.load(file, object_hook=hinted_array_hook)
else:
json_dat = json.loads(self.data)
self._root = self._load_node(None, json_dat)
return self._root
json_dat = json.loads(self.data, object_hook=hinted_array_hook)
self.root = self._load_node(None, json_dat)
return self.root

def generate(self, **kw):
self._use_strings = kw.get("stringify_values", True)
Expand All @@ -56,4 +94,6 @@ def generate(self, **kw):
if kw.get("pretty_print"):
indent = kw.get("indent", "")
opt["indent"] = kw.get("indent_count", 4) if indent == "" else indent
if not self._use_strings:
return ArrayEncoder(**opt).encode(dict_data)
return json.dumps(dict_data, **opt)
2 changes: 2 additions & 0 deletions formation/formats/_xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ def _generate_node(self, parent, node: Node):
for attrib in ns:
attr = "{{{}}}{}".format(namespaces.get(key), attrib)
x_node.attrib[attr] = str(ns[attrib])
elif isinstance(node.attrib[key], (list, tuple, set)):
x_node.attrib[key] = " ".join(map(str, node.attrib[key]))
else:
x_node.attrib[key] = str(node.attrib[key])

Expand Down
4 changes: 2 additions & 2 deletions formation/handlers/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@
def set_grid(widget, _=None, **options):
for opt in list(options):
if opt in ("width", "height"):
widget[opt] = options.pop(opt)
widget.configure(**{opt: options.pop(opt)})
widget.grid(**options)


def set_pack(widget, _=None, **options):
for opt in list(options):
if opt in ("width", "height"):
widget[opt] = options.pop(opt)
widget.configure(**{opt: options.pop(opt)})
widget.pack(**options)


Expand Down
2 changes: 1 addition & 1 deletion formation/handlers/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ class AttrHandler:
@classmethod
def handle(cls, widget, config, **kwargs):
attributes = config.get("attr", {})
handle_method = kwargs.get("handle_method", widget.config)
handle_method = kwargs.get("handle_method", widget.configure)
# update handle method just in case it was missing
kwargs.update(handle_method=handle_method)
direct_config = {}
Expand Down
11 changes: 6 additions & 5 deletions formation/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from formation.meth import Meth
from formation.handlers.image import parse_image
from formation.handlers.scroll import apply_scroll_config
from formation.utils import is_class_toplevel, is_class_root
import formation

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -93,7 +94,7 @@ def load(cls, node, builder, parent):
if obj_class == ttk.PanedWindow and "orient" in config.get("attr", {}):
orient = config["attr"].pop("orient")
obj = obj_class(parent, orient=orient)
elif obj_class == tk.Tk:
elif is_class_root(obj_class):
obj = obj_class()
else:
obj = obj_class(parent)
Expand Down Expand Up @@ -333,8 +334,8 @@ 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 widget.__class__ not in _containers:
# We dont need to load child tags of non-container widgets
if not isinstance(widget, _containers):
# We don't need to load child tags of non-container widgets
return widget
for sub_node in node:
if sub_node.is_var() or sub_node.type in _ignore_tags:
Expand Down Expand Up @@ -519,8 +520,8 @@ def _load_node(self, root_node):
if self._app is None:
# no external parent app provided
obj_class = BaseLoaderAdapter._get_class(root_node)
if obj_class not in (tk.Toplevel, tk.Tk):
# widget is not toplevel so we spin up a toplevel parent for it
if not is_class_toplevel(obj_class):
# widget is not toplevel, so we spin up a toplevel parent for it
self._parent = self._app = tk.Tk(*self._toplevel_args)
else:
# use external app as parent
Expand Down
34 changes: 29 additions & 5 deletions formation/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,25 @@
# ======================================================================= #
# Copyright (C) 2022 Hoverset Group. #
# ======================================================================= #
import tkinter as tk


def is_class_toplevel(cls):
if cls in (tk.Toplevel, tk.Tk):
return True
for base in cls.__bases__:
if is_class_toplevel(base):
return True
return False


def is_class_root(cls):
if cls == tk.Tk:
return True
for base in cls.__bases__:
if is_class_root(base):
return True
return False


class CustomPropertyMixin:
Expand Down Expand Up @@ -77,32 +96,37 @@ def set_title(self, value):
"""
prop_info = {}

def _resolve_getter(self, prop):
val = getattr(self, prop)
return val if not callable(val) else val()

def configure(self, cnf=None, **kw):
if isinstance(cnf, str):
if cnf in self.prop_info:
p = self.prop_info[cnf]
return (
p["name"], p["name"], p["name"].title(),
p["default"],
getattr(self, p["getter"]))
self._resolve_getter(p["getter"]),
)
else:
return super().configure(cnf)

if cnf is None and not kw:
cnf = super().configure()
cnf = super().configure() or {}
prp = self.prop_info.values()
cnf.update({p["name"]: (
p["name"], p["name"], p["name"].title(),
p["default"],
getattr(self, p["getter"])) for p in prp})
self._resolve_getter(p["getter"])) for p in prp})
return cnf

cnf = cnf or {}
cnf.update(kw)
customs = cnf.keys() & self.prop_info.keys()
for key in customs:
getattr(self, self.prop_info[key]["setter"])(cnf.pop(key))
super().configure(cnf)
super().configure(**cnf)

config = configure

Expand All @@ -113,7 +137,7 @@ def keys(self):

def cget(self, key):
if key in self.prop_info:
return getattr(self, self.prop_info[key]["getter"])
return self._resolve_getter(self.prop_info[key]["getter"])
return super().cget(key)

__getitem__ = cget
Expand Down
63 changes: 52 additions & 11 deletions hoverset/ui/panels.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from PIL import ImageTk
import logging
from tkinter import font

from hoverset.ui.widgets import *
from hoverset.ui.icons import get_icon_image
Expand Down Expand Up @@ -468,7 +469,21 @@ def pick(self, value):
def on_change(self, func, *args, **kwargs):
self._on_change = lambda val: func(val, *args, **kwargs)

def get(self):
def get_tuple(self):
res = [
self._font.get(), self._size.get()
]
if self._bold.get():
res.append('bold')
if self._italic.get():
res.append('italic')
if self._underline.get():
res.append('underline')
if self._strike.get():
res.append('overstrike')
return tuple(res)

def get_str(self):
extra = []
if self._bold.get():
extra.append('bold')
Expand All @@ -481,23 +496,49 @@ def get(self):
extra = ' '.join(extra)
return f"{{{self._font.get()}}} {abs(self._size.get() or 0)} {{{extra}}}"

def get(self):
return self.get_str()

@suppress_change
def set(self, value):
if not value:
for i in (self._italic, self._bold, self._underline, self._strike):
i.set(False)
return
try:
font_obj = FontStyle(self, value)
except Exception:
logging.error("Font exception")
family = None
if isinstance(value, (tuple, list)):
family, size, *_ = value
weight = "bold" if "bold" in value else "normal"
slant = "italic" if "italic" in value else "roman"
underline = "underline" if "underline" in value else False
strike = "overstrike" if "overstrike" in value else False
elif isinstance(value, str):
try:
font_obj = font.Font(self, value)
except Exception:
logging.error("Font exception")
return
value = font_obj
elif not isinstance(value, font.Font):
return
self._font.set(font_obj.cget("family"))
self._size.set(font_obj.cget("size"))
self._italic.set(True if font_obj.cget("slant") == "italic" else False)
self._bold.set(True if font_obj.cget("weight") == "bold" else False)
self._underline.set(font_obj.cget("underline"))
self._strike.set(font_obj.cget("overstrike"))

if isinstance(value, font.Font):
family, size, weight, slant, underline, strike = (
value.cget("family"),
value.cget("size"),
value.cget("weight"),
value.cget("slant"),
value.cget("underline"),
value.cget("overstrike")
)

if family is not None:
self._font.set(family)
self._size.set(size)
self._italic.set(True if slant == "italic" else False)
self._bold.set(True if weight == "bold" else False)
self._underline.set(True if underline == "underline" else False)
self._strike.set(True if strike == "overstrike" else False)


if __name__ == "__main__":
Expand Down
23 changes: 23 additions & 0 deletions studio/external/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import os
import logging

from studio.external._base import FeatureNotAvailableError

from hoverset.util.execution import import_path


def init_externals(studio):

external = os.path.dirname(__file__)
for module_path in os.listdir(external):
if module_path.endswith(".py") and not module_path.startswith("_"):
try:
module = import_path(os.path.join(external, module_path))
if hasattr(module, "init"):
module.init(studio)
except FeatureNotAvailableError:
# feature is probably not installed
pass
except Exception as e:
logging.error(f"Failed to load external module {module_path}: {e}")
continue
3 changes: 3 additions & 0 deletions studio/external/_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class FeatureNotAvailableError(Exception):
"""Exception raised when a feature is not available."""
pass
Loading

0 comments on commit 89160a9

Please sign in to comment.