Compare commits

...
Sign in to create a new pull request.

2 commits

6 changed files with 231 additions and 0 deletions

View file

@ -0,0 +1,74 @@
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.selection: Optional[str] = None
self.selection_index: Optional[int] = None
self.delete_button: Optional[ttk.Button] = None
self.toggle_button: Optional[ttk.Button] = 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)
self.list.listbox.bind("<<ListboxSelect>>", self.list_select)
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)
self.delete_button = ttk.Button(
frame, text="Delete", command=self.click_delete, state=tk.DISABLED
)
self.delete_button.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
self.toggle_button = ttk.Button(
frame, text="Toggle", command=self.click_toggle, state=tk.DISABLED
)
self.toggle_button.grid(row=0, column=2, sticky=tk.EW)
def click_add(self) -> None:
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 list_select(self, event: tk.Event) -> None:
self.selection_index = self.list.listbox.curselection()
if not self.selection_index:
self.selection = None
state = tk.DISABLED
else:
self.selection = self.list.listbox.get(self.selection_index)
state = tk.NORMAL
self.toggle_button.config(state=state)
self.delete_button.config(state=state)
def click_delete(self) -> None:
self.app.canvas.layers.delete_layer(self.selection)
self.list.listbox.delete(self.selection_index)
self.list.listbox.event_generate("<<ListboxSelect>>")
def click_toggle(self) -> None:
self.app.canvas.layers.toggle_layer(self.selection)

View file

@ -0,0 +1,49 @@
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.bind("<Return>", lambda e: self.destroy())
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()

View file

@ -25,6 +25,7 @@ from core.gui.graph.edges import (
create_edge_token, create_edge_token,
) )
from core.gui.graph.enums import GraphMode, ScaleOption 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.node import CanvasNode
from core.gui.graph.shape import Shape from core.gui.graph.shape import Shape
from core.gui.graph.shapeutils import ShapeType, is_draw_shape, is_marker 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") super().__init__(master, highlightthickness=0, background="#cccccc")
self.app: "Application" = app self.app: "Application" = app
self.core: "CoreClient" = core self.core: "CoreClient" = core
self.layers = CanvasLayers(self)
self.mode: GraphMode = GraphMode.SELECT self.mode: GraphMode = GraphMode.SELECT
self.annotation_type: Optional[ShapeType] = None self.annotation_type: Optional[ShapeType] = None
self.selection: Dict[int, int] = {} self.selection: Dict[int, int] = {}

View file

@ -0,0 +1,94 @@
import tkinter as tk
from functools import partial
from typing import TYPE_CHECKING, Dict, Iterable, Set
from core.gui import themes
if TYPE_CHECKING:
from core.gui.app import Application
from core.gui.graph.graph import CanvasGraph
class LayersMenu(tk.Menu):
def __init__(self, master: tk.BaseWidget, app: "Application", item: int) -> None:
super().__init__(master)
themes.style_menu(self)
self.app: "Application" = app
self.item: int = item
self.buttons: Dict[str, tk.BooleanVar] = {}
self.draw()
def draw(self) -> None:
for name in self.app.canvas.layers.names():
value = self.app.canvas.layers.in_layer(name, self.item)
var = tk.BooleanVar(value=value)
self.buttons[name] = var
self.add_checkbutton(
label=name, variable=var, command=partial(self.click_layer, name)
)
def click_layer(self, name):
value = self.buttons[name].get()
if value:
self.app.canvas.layers.add_item(name, self.item)
else:
self.app.canvas.layers.delete_item(name, self.item)
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
for item in items:
self.canvas.itemconfig(item, state=tk.NORMAL)
def in_layer(self, name: str, item: int) -> bool:
return item in self.layers.get(name, set())
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.itemconfig(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.itemconfig(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
for item in items:
self.canvas.itemconfig(item, state=tk.NORMAL)
else:
self.hidden.add(name)
for item in items:
self.canvas.itemconfig(item, state=tk.HIDDEN)
def all_hidden(self) -> Set[int]:
items = set()
for name in self.hidden:
items |= self.layers[name]
return items

View file

@ -19,6 +19,7 @@ from core.gui.dialogs.wlanconfig import WlanConfigDialog
from core.gui.frames.node import NodeInfoFrame from core.gui.frames.node import NodeInfoFrame
from core.gui.graph import tags from core.gui.graph import tags
from core.gui.graph.edges import CanvasEdge, CanvasWirelessEdge from core.gui.graph.edges import CanvasEdge, CanvasWirelessEdge
from core.gui.graph.layers import LayersMenu
from core.gui.graph.tooltip import CanvasTooltip from core.gui.graph.tooltip import CanvasTooltip
from core.gui.images import ImageEnum from core.gui.images import ImageEnum
from core.gui.nodeutils import ANTENNA_SIZE, NodeUtils from core.gui.nodeutils import ANTENNA_SIZE, NodeUtils
@ -271,6 +272,9 @@ class CanvasNode:
edit_menu.add_command(label="Copy", command=self.canvas_copy) edit_menu.add_command(label="Copy", command=self.canvas_copy)
edit_menu.add_command(label="Delete", command=self.canvas_delete) edit_menu.add_command(label="Delete", command=self.canvas_delete)
self.context.add_cascade(label="Edit", menu=edit_menu) self.context.add_cascade(label="Edit", menu=edit_menu)
layer_menu = LayersMenu(self.context, self.app, self.id)
self.context.add_cascade(label="Layer", menu=layer_menu)
self.context.tk_popup(event.x_root, event.y_root) self.context.tk_popup(event.x_root, event.y_root)
def click_cut(self) -> None: def click_cut(self) -> None:

View file

@ -9,6 +9,7 @@ from PIL.ImageTk import PhotoImage
from core.api.grpc import core_pb2 from core.api.grpc import core_pb2
from core.gui.dialogs.colorpicker import ColorPickerDialog from core.gui.dialogs.colorpicker import ColorPickerDialog
from core.gui.dialogs.layers import LayersDialog
from core.gui.dialogs.runtool import RunToolDialog from core.gui.dialogs.runtool import RunToolDialog
from core.gui.graph import tags from core.gui.graph import tags
from core.gui.graph.enums import GraphMode from core.gui.graph.enums import GraphMode
@ -237,6 +238,9 @@ class Toolbar(ttk.Frame):
"Annotation Tools", "Annotation Tools",
radio=True, radio=True,
) )
self.design_frame.create_button(
self.annotation_enum, self.show_layers, "Layers"
)
def draw_runtime_frame(self) -> None: def draw_runtime_frame(self) -> None:
self.runtime_frame = ButtonBar(self, self.app) self.runtime_frame = ButtonBar(self, self.app)
@ -256,6 +260,10 @@ class Toolbar(ttk.Frame):
ImageEnum.RUN, self.click_run_button, "Run Tool" 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: def draw_node_picker(self) -> None:
self.hide_marker() self.hide_marker()
self.app.canvas.mode = GraphMode.NODE self.app.canvas.mode = GraphMode.NODE