import tkinter as tk import tkinter.ttk as ttk import tkinter.messagebox as messagebox from typing import Dict, List, Callable, Tuple from gui.framsutils.framsProperty import Property, PropertyCallback, propertyToTkinter from gui.framsutils.FramsInterface import FramsInterface, InterfaceType from gui.widgets.ToolTip import CreateToolTip from gui.utils import windowHideAndMaximize, windowShowAndSetGeometry ''' BUG: when you open this window during the creature loading for render (commThread in glFrame) and modify the list of creatures (with buttons) or parts (with editing genotype), it can get stuck on the semaphore or throw an exception when exiting the window and returning to creature loading. SOLUTION: open window only when the semaphore is released. ''' class PropertyWindow(tk.Toplevel): def __init__(self, parent, title: str, posX, dataCallback: Callable[[None], List[Property]], propUpdateCallback: Callable[[str, str], None], getError: Callable[[None], str or None], frams: FramsInterface, semaphore = None, onUpdate: Callable[[None], None] = None): super().__init__(parent) self.parent = parent #self.transient(parent) self.protocol("WM_DELETE_WINDOW", self._dismiss) self.title(title) self.propUpdateCallback: Callable[[str, str], None] = propUpdateCallback self.dataCallback: Callable[[None], List[Property]] = dataCallback self.getError: Callable([None], str or None) = getError self.onUpdate: Callable([None], None) = onUpdate self.frams: FramsInterface = frams #MAIN SECTION frame_main = tk.Frame(master=self) notebook_notebook = ttk.Notebook(frame_main) frames: Dict[str, tk.Frame] = {} frames_idx: Dict[str, int] = {} data = self.dataCallback() self.callbacks: List[PropertyCallback] = [] self.semaphore = semaphore for prop in data: if "group" not in prop.p: prop.p["group"] = "other" if prop.p["group"] not in frames: frames[prop.p["group"]] = tk.Frame(master=frame_main) frames_idx[prop.p["group"]] = 0 frames[prop.p["group"]].columnconfigure(0, weight=0, minsize=0) frames[prop.p["group"]].columnconfigure(1, weight=1, minsize=0) notebook_notebook.add(frames[prop.p["group"]], text=prop.p["group"]) widget, callback = propertyToTkinter(prop, frames[prop.p["group"]]) if widget: label = tk.Label(master=frames[prop.p["group"]], text=prop.p["name"], anchor='w') if prop.p["type"][0] == 'p': label.text = "" label.grid(row=frames_idx[prop.p["group"]], column=0, sticky="NSEW") if issubclass(type(widget), tk.Checkbutton): widget.grid(row=frames_idx[prop.p["group"]], column=1, sticky="NSW") else: widget.grid(row=frames_idx[prop.p["group"]], column=1, sticky="NSEW") if callback: self.callbacks.append(callback) if prop.p["help"] != "": CreateToolTip(label, prop.p["help"]) CreateToolTip(widget, prop.p["help"]) if issubclass(type(widget), tk.Text) and prop.p["type"][0] == 's': frames[prop.p["group"]].rowconfigure(frames_idx[prop.p["group"]], weight=1, minsize=0) else: frames[prop.p["group"]].rowconfigure(frames_idx[prop.p["group"]], weight=0, minsize=0) frames_idx[prop.p["group"]] += 1 notebook_notebook.grid(row=0, column=0, sticky="NSEW") #sometimes it freezes here frame_main.columnconfigure(0, weight=1, minsize=0) frame_main.rowconfigure(0, weight=1, minsize=0) #CONTROL BUTTONS frame_buttons = tk.Frame(master=frame_main) self.button_refresh = tk.Button(master=frame_buttons, text="Refresh", command=self.refreshButtonCommand) self.button_cancel = tk.Button(master=frame_buttons, text="Cancel", command=self.cancelButtonCommand) self.button_apply = tk.Button(master=frame_buttons, text="Apply", command=self.applyButtonCommand) self.button_ok = tk.Button(master=frame_buttons, text="OK", command=self.okButtonCommand) self.button_refresh.grid(row=0, column=0, sticky="E") self.button_cancel.grid(row=0, column=1, sticky="E") self.button_apply.grid(row=0, column=2, sticky="E") self.button_ok.grid(row=0, column=3, sticky="E") frame_buttons.columnconfigure(0, weight=1, minsize=0) frame_buttons.columnconfigure(1, weight=1, minsize=0) frame_buttons.columnconfigure(2, weight=1, minsize=0) frame_buttons.columnconfigure(3, weight=1, minsize=0) frame_buttons.rowconfigure(0, weight=1, minsize=0) frame_buttons.grid(row=1, column=0, sticky="SE") frame_main.rowconfigure(1, weight=0, minsize=0) frame_main.grid(row=0, column=0, sticky="NSEW") self.columnconfigure(0, weight=1, minsize=0) self.rowconfigure(0, weight=1, minsize=0) def parseGeometryString(geometry: str) -> Tuple[int, int, int, int]: widthxrest = geometry.split('x') heightxy = widthxrest[1].split('+') maxwidth = int(widthxrest[0]) maxheight = int(heightxy[0]) x = int(heightxy[1]) y = int(heightxy[2]) return maxwidth, maxheight, x, y self.update_idletasks() width, height, x, y = parseGeometryString(self.winfo_geometry()) windowHideAndMaximize(self) rootx = self.winfo_rootx() maxWidth, maxHeight, x, y = parseGeometryString(self.winfo_geometry()) x = (maxWidth - width) / 2 y = (maxHeight - height) / 2 windowShowAndSetGeometry(self, "+%d+%d" % (int(rootx + x), int(y))) self.maxsize(maxWidth, maxHeight) self.grab_set() def refreshButtonCommand(self): data = self.dataCallback() for d in data: prop = next((x for x in self.callbacks if d.p["id"] == x.id), None) if prop: prop.updateValue(d) def cancelButtonCommand(self): self._dismiss() def applyButtonCommand(self): if self.propUpdateCallback: updated = False with self.semaphore: for c in self.callbacks: if c.changed(): self.propUpdateCallback(c.id, c.rawValue(self.frams.interfaceType == InterfaceType.SOCKET)) updated = True error = self.getError() if error: messagebox.showerror("Framsticks error", error) self.refreshButtonCommand() if updated and self.onUpdate: self.onUpdate() def okButtonCommand(self): self.applyButtonCommand() self.cancelButtonCommand() def _dismiss(self): self.grab_release() self.destroy()