From 777f2e14841da42b55a6c803d9351eaeb62cbf67 Mon Sep 17 00:00:00 2001 From: mikee47 Date: Mon, 12 Feb 2024 14:59:56 +0000 Subject: [PATCH] Basic image support --- Tools/ged/ged.py | 180 ++++++++++++++++++++++++++---------------- Tools/ged/item.py | 9 +-- Tools/ged/resource.py | 39 ++++++++- 3 files changed, 149 insertions(+), 79 deletions(-) diff --git a/Tools/ged/ged.py b/Tools/ged/ged.py index 67804e8b..096b1087 100644 --- a/Tools/ged/ged.py +++ b/Tools/ged/ged.py @@ -143,6 +143,10 @@ def draw_text(self, rect, text, halign, valign): y0 += (y1 - y2) // 2 self.canvas.coords(id, x0, y0) + def draw_image(self, rect, image): + x0, y0, x1, y1 = self.handler.tk_bounds(rect) + self.canvas.create_image(x0, y0, image=self.handler.tk_image(image), anchor=tk.NW, tags=self.tags) + def union(r1: Rect, r2: Rect): x = min(r1.x, r2.x) @@ -169,66 +173,39 @@ def get_handle_pos(r: Rect, elem: Element): }.get(elem) -class FontAssets(list): +class ResourceList(list): def __init__(self): super().__init__() self.clear() - def clear(self): - super().clear() - tk_def = FontAssets.tk_default().configure() - font = Font(name='default', family = tk_def['family']) - self.default = font - self.append(font) - - def get(self, font_name, default=None): + def get(self, name, default=None): try: - return next(font for font in self if font.name == font_name) + return next(r for r in self if r.name == name) except StopIteration: return default - @staticmethod - def tk_default(): - return tkinter.font.nametofont('TkDefaultFont') - - @staticmethod - def families(): - # Not all fonts are listed by TK, so include the 'guaranteed supported' ones - font_families = list(tk.font.families()) - tk_def = FontAssets.tk_default().configure() - font_families += ['Courier', 'Times', 'Helvetica', tk_def['family']] - font_families = list(set(font_families)) - return sorted(font_families, key=str.lower) - def names(self): - return [font.name for font in self] + return [r.name for r in self] def asdict(self): - return dict( - (font.name, { - 'family': font_def.family, - 'size': font_def.size, - }) for font in self) + return dict((r.name, r.asdict()) for r in self) - def load(self, font_defs): + def load(self, res_class, res_dict): self.clear() - for name, font_def in font_defs.items(): - font = Font(name=name, family=font_def['family'], size=font_def['size']) - self.append(font) - -font_assets = None + for name, rdef in res_dict.items(): + r = res_class(name=name) + for a, v in rdef.items(): + setattr(r, a, v) + self.append(r) -class ImageAssets(list): - def __init__(self): - super().__init__() - self.clear() - - def get(self, image_name, default=None): - try: - return next(font for font in self if font.name == font_name) - except StopIteration: - return default +class FontAssets(ResourceList): + def clear(self): + super().clear() + tk_def = FontAssets.tk_default().configure() + font = Font(name='default', family = tk_def['family']) + self.default = font + self.append(font) @staticmethod def tk_default(): @@ -243,21 +220,15 @@ def families(): font_families = list(set(font_families)) return sorted(font_families, key=str.lower) - def names(self): - return [font.name for font in self] + def load(self, res_dict): + super().load(Font, res_dict) - def asdict(self): - return dict( - (font.name, { - 'family': font_def.family, - 'size': font_def.size, - }) for font in self) +font_assets = None - def load(self, font_defs): - self.clear() - for name, font_def in font_defs.items(): - font = Font(name=name, family=font_def['family'], size=font_def['size']) - self.append(font) + +class ImageAssets(ResourceList): + def load(self, res_dict): + super().load(Image, res_dict) image_assets = None @@ -344,6 +315,9 @@ def tk_font(self, font_name: str): font = font_assets.get(font_name, font_assets.default) return tkinter.font.Font(family=font.family, size=-font.size*self.scale) + def tk_image(self, image_name: str): + return image_assets.get(image_name).get_tk_image(self.scale) + def clear(self): self.display_list.clear() self.sel_items.clear() @@ -572,6 +546,7 @@ def add_item(itemtype): 'R': 'FilledRect', 'e': 'Ellipse', 'E': 'FilledEllipse', + 'i': 'Image', 't': 'Text', 'b': 'Button', }.get(c) @@ -580,8 +555,9 @@ def add_item(itemtype): class Editor: - def __init__(self, root, title): + def __init__(self, root, title, field_prefix): self.frame = ttk.LabelFrame(root, text=title) + self.field_prefix = field_prefix self.frame.columnconfigure(1, weight=1) self.is_updating = False self.on_value_changed = None @@ -605,13 +581,11 @@ def add_control(self, ctrl, row): def text_from_name(name: str): words = re.findall(r'[A-Z]?[a-z]+|[A-Z]+(?=[A-Z]|$)', name) return ' '.join(words).lower() - # return re.sub("([a-z])([A-Z])", "\g<0> \g<1>", name) - # return name.replace('_', ' ').capitalize() def add_field(self, name: str, ctrl, var_type=tk.StringVar): row = len(self.fields) self.add_label(self.text_from_name(name), row) - var = var_type(name=name) + var = var_type(name=self.field_prefix+name) ctrl.configure(textvariable=var) var.trace_add('write', self.tk_value_changed) self.add_control(ctrl, row) @@ -635,7 +609,7 @@ def add_check_fields(self, title, names: list): self.add_control(frame, row) frame.grid(column=0, columnspan=2) for name in names: - var = tk.BooleanVar(name=name) + var = tk.BooleanVar(name=self.field_prefix+name) var.trace_add('write', self.tk_value_changed) ctrl = ttk.Checkbutton(frame, variable=var, text=self.text_from_name(name)) ctrl.pack(side=tk.LEFT) @@ -646,12 +620,13 @@ def tk_value_changed(self, name1, name2, op): if self.is_updating: return """See 'trace add variable' in TCL docs""" - var, _ = self.fields[name1] + name = name1[len(self.field_prefix):] + var, _ = self.fields[name] if var is None: return try: # print(f'value_changed:"{name1}", "{name2}", "{op}", "{var.get()}"') - self.value_changed(name1, var.get()) + self.value_changed(name, var.get()) except tk.TclError: # Can happen whilst typing if conversion fails, empty strings, etc. pass @@ -671,14 +646,14 @@ def get_value(self, name): class ProjectEditor(Editor): def __init__(self, root): - super().__init__(root, 'Project') + super().__init__(root, 'Project', 'proj-') for name in ['width', 'height', 'scale', 'grid_alignment']: self.add_entry_field(name, tk.IntVar) class PropertyEditor(Editor): def __init__(self, root): - super().__init__(root, 'Properties') + super().__init__(root, 'Properties', 'prop-') def set_field(self, name=str, values=list, options=list, callback=None): value_list = values + [o for o in options if o not in values] @@ -701,7 +676,7 @@ def handler(evt, callback=callback, var=var): class FontEditor(Editor): def __init__(self, root): - super().__init__(root, 'Font') + super().__init__(root, 'Font', 'font-') self.add_combo_field('name') self.add_combo_field('family', font_assets.families()) self.add_entry_field('size', tk.IntVar) @@ -709,7 +684,6 @@ def __init__(self, root): self.update() def value_changed(self, name, value): - print(f'value_changed: "{name}", "{value}"') if name == 'name': font = font_assets.get(value) if font: @@ -745,6 +719,55 @@ def select(self, font): self.is_updating = False +class ImageEditor(Editor): + def __init__(self, root): + super().__init__(root, 'Image', 'img-') + self.add_combo_field('name') + self.add_combo_field('source') + self.add_combo_field('format') + self.add_entry_field('width', tk.IntVar) + self.add_entry_field('height', tk.IntVar) + self.update() + + def value_changed(self, name, value): + if name == 'name': + image = image_assets.get(value) + if image: + self.select(image) + return + image_name = self.get_value('name') + # print(f'value_changed: "{name}", "{value}", "{font_name}"') + image = image_assets.get(image_name) + if image is None: + return + setattr(image, name, value) + self.update() + super().value_changed(name, value) + + def update(self): + for image in image_assets: + image.get_tk_image() + image = self.get_value('name') + image_names = image_assets.names() + _, c = self.fields['name'] + c.configure(values=image_names) + if not image and image_assets: + image = image_assets[0] + self.select(image) + + def select(self, image): + if isinstance(image, str): + image = image_assets.get(image) + if not image: + return + self.is_updating = True + try: + for k, v in dataclasses.asdict(image).items(): + self.set_value(k, v) + finally: + self.is_updating = False + + def run(): PROJECT_EXT = '.ged' PROJECT_FILTER = [('GED Project', '*' + PROJECT_EXT)] @@ -792,6 +815,8 @@ def fileClear(): handler.clear() font_assets.clear() font_editor.update() + image_assets.clear() + image_editor.update() def fileAddRandom(count=10): display_list = [] @@ -831,6 +856,8 @@ def fileLoad(): data = json_load(filename) font_assets.load(data['fonts']) font_editor.update() + image_assets.load(data['images']) + image_editor.update() handler.display_list = [] display_list = dl_deserialise(data['layout']) handler.clear() @@ -844,6 +871,7 @@ def fileSave(): filename += PROJECT_EXT data = { 'fonts': font_assets.asdict(), + 'images': image_assets.asdict(), 'layout': dl_serialise(handler.display_list), } json_save(data, filename) @@ -938,6 +966,8 @@ def sel_changed(full_change: bool): values = list(values) if name == 'font': options = font_assets.names() + elif name == 'image': + options = image_assets.names() elif isinstance(values[0], Color): options = sorted(colormap.keys()) def select_color(var): @@ -961,6 +991,18 @@ def font_value_changed(name, value): font_editor.on_value_changed = font_value_changed font_editor.frame.pack(side=tk.TOP, expand=True, fill=tk.X, ipady=4) + # Images + global image_assets + image_assets = ImageAssets() + image_editor = ImageEditor(edit_frame) + def image_value_changed(name, value): + print(f'image_value_changed("{name}", "{value}")') + handler.redraw() + image_editor.on_value_changed = image_value_changed + image_editor.frame.pack(side=tk.TOP, expand=True, fill=tk.X, ipady=4) + + print('image_assets', image_assets.names()) + # tk.mainloop() diff --git a/Tools/ged/item.py b/Tools/ged/item.py index bb2acdee..19c362b9 100644 --- a/Tools/ged/item.py +++ b/Tools/ged/item.py @@ -120,13 +120,8 @@ class GImage(GItem): image: str = '' def draw(self, c): - c.color = str(self.color) - c.line_width = self.line_width - - if self.radius > 1: - c.draw_rounded_rect(self, self.radius) - else: - c.draw_rect(self) + if self.image: + c.draw_image(self, self.image) @dataclass diff --git a/Tools/ged/resource.py b/Tools/ged/resource.py index 599c8d9c..a9673c15 100644 --- a/Tools/ged/resource.py +++ b/Tools/ged/resource.py @@ -1,15 +1,48 @@ from dataclasses import dataclass +import PIL.Image, PIL.ImageTk, PIL.ImageOps @dataclass -class Font: +class Resource: name: str = '' + + def asdict(self): + d = dataclasses.asdict(self) + del d['name'] + return d + + +@dataclass +class Font(Resource): family: str = '' size: int = 12 # style: list[str] For now, assume all styles are available @dataclass -class Image: - name: str = '' +class Image(Resource): source: str = '' + format: str = '' + width: int = 0 + height:int = 0 + tk_image = None + def get_tk_image(self, scale: int = 1): + w, h = self.width * scale, self.height * scale + tki = self.tk_image + if tki and (tki.width != w or tki.height != h): + tki = None + if tki is None: + img = PIL.Image.open(self.source) + if w == 0: + if h == 0: + w, h = img.width, img.height + else: + w = img.width * h / img.height + elif h == 0: + h = img.height * w / img.width + if w != img.width or h != img.height: + img = img.resize((w, h)) + self.width = w // scale + self.height = h // scale + tki = self.tk_image = PIL.ImageTk.PhotoImage(img) + return tki