diff --git a/daemon/core/gui/dialogs/layers.py b/daemon/core/gui/dialogs/layers.py new file mode 100644 index 00000000..fc72899a --- /dev/null +++ b/daemon/core/gui/dialogs/layers.py @@ -0,0 +1,60 @@ +import tkinter as tk +from tkinter import messagebox, ttk +from typing import TYPE_CHECKING, Optional + +from core.gui.dialogs.dialog import Dialog +from core.gui.dialogs.simple import SimpleStringDialog +from core.gui.themes import PADX, PADY +from core.gui.widgets import ListboxScroll + +if TYPE_CHECKING: + from core.gui.app import Application + + +class LayersDialog(Dialog): + def __init__(self, app: "Application") -> None: + super().__init__(app, "Canvas Layers", modal=False) + self.list: Optional[ListboxScroll] = None + self.draw() + + def draw(self) -> None: + self.top.columnconfigure(0, weight=1) + self.list = ListboxScroll(self.top) + self.list.grid(sticky=tk.EW, pady=PADY) + for name in self.app.canvas.layers.names(): + self.list.listbox.insert(tk.END, name) + frame = ttk.Frame(self.top) + frame.grid(sticky=tk.EW) + for i in range(3): + frame.columnconfigure(i, weight=1) + button = ttk.Button(frame, text="Add", command=self.click_add) + button.grid(row=0, column=0, sticky=tk.EW, padx=PADX) + button = ttk.Button(frame, text="Delete", command=self.click_delete) + button.grid(row=0, column=1, sticky=tk.EW, padx=PADX) + button = ttk.Button(frame, text="Toggle", command=self.click_toggle) + button.grid(row=0, column=2, sticky=tk.EW) + + def click_add(self): + name = SimpleStringDialog(self, self.app, "Add Layer", "Layer Name").ask() + if name: + result = self.app.canvas.layers.add_layer(name) + if result: + self.list.listbox.insert(tk.END, name) + else: + messagebox.showerror( + "Add Layer", f"Duplicate Layer: {name}", parent=self + ) + + def click_delete(self): + selection = self.list.listbox.curselection() + if not selection: + return + name = self.list.listbox.get(selection) + print(name) + + def click_toggle(self): + selection = self.list.listbox.curselection() + if not selection: + return + name = self.list.listbox.get(selection) + print(name) diff --git a/daemon/core/gui/dialogs/simple.py b/daemon/core/gui/dialogs/simple.py new file mode 100644 index 00000000..0068f398 --- /dev/null +++ b/daemon/core/gui/dialogs/simple.py @@ -0,0 +1,48 @@ +import tkinter as tk +from tkinter import ttk +from typing import TYPE_CHECKING, Optional + +from core.gui.dialogs.dialog import Dialog +from core.gui.themes import PADX, PADY + +if TYPE_CHECKING: + from core.gui.app import Application + + +class SimpleStringDialog(Dialog): + def __init__( + self, master: tk.BaseWidget, app: "Application", title: str, prompt: str + ): + super().__init__(app, title, master=master) + self.prompt: str = prompt + self.value = tk.StringVar() + self.entry: Optional[ttk.Entry] = None + self.canceled = False + self.draw() + + def draw(self) -> None: + self.top.columnconfigure(0, weight=1) + label = ttk.Label(self.top, text=self.prompt) + label.grid(sticky=tk.EW, pady=PADY) + entry = ttk.Entry(self.top, textvariable=self.value) + entry.grid(stick=tk.EW, pady=PADY) + entry.focus_set() + frame = ttk.Frame(self.top) + frame.grid(sticky=tk.EW) + for i in range(2): + frame.columnconfigure(i, weight=1) + button = ttk.Button(frame, text="Submit", command=self.destroy) + button.grid(row=0, column=0, sticky=tk.EW, padx=PADX) + button = ttk.Button(frame, text="Cancel", command=self.click_cancel) + button.grid(row=0, column=1, sticky=tk.EW) + + def click_cancel(self): + self.canceled = True + self.destroy() + + def ask(self) -> Optional[str]: + self.show() + if self.canceled: + return None + else: + return self.value.get() diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 9cb3b109..b4d89d88 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -25,6 +25,7 @@ from core.gui.graph.edges import ( create_edge_token, ) from core.gui.graph.enums import GraphMode, ScaleOption +from core.gui.graph.layers import CanvasLayers from core.gui.graph.node import CanvasNode from core.gui.graph.shape import Shape from core.gui.graph.shapeutils import ShapeType, is_draw_shape, is_marker @@ -62,6 +63,7 @@ class CanvasGraph(tk.Canvas): super().__init__(master, highlightthickness=0, background="#cccccc") self.app: "Application" = app self.core: "CoreClient" = core + self.layers = CanvasLayers(self) self.mode: GraphMode = GraphMode.SELECT self.annotation_type: Optional[ShapeType] = None self.selection: Dict[int, int] = {} diff --git a/daemon/core/gui/graph/layers.py b/daemon/core/gui/graph/layers.py new file mode 100644 index 00000000..ba8031b0 --- /dev/null +++ b/daemon/core/gui/graph/layers.py @@ -0,0 +1,58 @@ +import tkinter as tk +from typing import TYPE_CHECKING, Dict, Iterable, Set + +if TYPE_CHECKING: + from core.gui.graph.graph import CanvasGraph + + +class CanvasLayers: + def __init__(self, canvas: "CanvasGraph"): + self.canvas: "CanvasGraph" = canvas + self.layers: Dict[str, Set[int]] = {} + self.hidden: Set[str] = set() + + def names(self) -> Iterable[str]: + return self.layers.keys() + + def add_layer(self, name: str) -> bool: + if name in self.layers: + return False + else: + self.layers[name] = set() + return True + + def delete_layer(self, name: str) -> None: + items = self.layers.pop(name, set()) + hidden_items = self.all_hidden() + items -= hidden_items + self.canvas.config(items, state=tk.NORMAL) + + def add_item(self, name: str, item: int) -> None: + if name in self.layers: + self.layers[name].add(item) + if name in self.hidden: + self.canvas.config(item, state=tk.HIDDEN) + + def delete_item(self, name: str, item: int) -> None: + if name in self.layers: + self.layers[name].remove(item) + hidden_items = self.all_hidden() + if item not in hidden_items: + self.canvas.config(item, state=tk.NORMAL) + + def toggle_layer(self, name: str) -> None: + items = self.layers[name] + if name in self.hidden: + self.hidden.remove(name) + hidden_items = self.all_hidden() + items -= hidden_items + self.canvas.config(items, state=tk.NORMAL) + else: + self.hidden.add(name) + self.canvas.config(items, state=tk.HIDDEN) + + def all_hidden(self) -> Set[int]: + items = set() + for name in self.hidden: + items |= self.layers[name] + return items diff --git a/daemon/core/gui/toolbar.py b/daemon/core/gui/toolbar.py index 406a88ca..0dd3c083 100644 --- a/daemon/core/gui/toolbar.py +++ b/daemon/core/gui/toolbar.py @@ -9,6 +9,7 @@ from PIL.ImageTk import PhotoImage from core.api.grpc import core_pb2 from core.gui.dialogs.colorpicker import ColorPickerDialog +from core.gui.dialogs.layers import LayersDialog from core.gui.dialogs.runtool import RunToolDialog from core.gui.graph import tags from core.gui.graph.enums import GraphMode @@ -237,6 +238,9 @@ class Toolbar(ttk.Frame): "Annotation Tools", radio=True, ) + self.design_frame.create_button( + self.annotation_enum, self.show_layers, "Layers" + ) def draw_runtime_frame(self) -> None: self.runtime_frame = ButtonBar(self, self.app) @@ -256,6 +260,10 @@ class Toolbar(ttk.Frame): ImageEnum.RUN, self.click_run_button, "Run Tool" ) + def show_layers(self) -> None: + dialog = LayersDialog(self.app) + dialog.show() + def draw_node_picker(self) -> None: self.hide_marker() self.app.canvas.mode = GraphMode.NODE