import logging import tkinter as tk from copy import deepcopy from pathlib import Path from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple from PIL import Image from PIL.ImageTk import PhotoImage from core.api.grpc.wrappers import Interface, Link from core.gui import appconfig from core.gui import nodeutils as nutils from core.gui.dialogs.shapemod import ShapeDialog from core.gui.graph import tags from core.gui.graph.edges import EDGE_WIDTH, CanvasEdge from core.gui.graph.enums import GraphMode, ScaleOption from core.gui.graph.node import CanvasNode, ShadowNode from core.gui.graph.shape import Shape from core.gui.graph.shapeutils import ShapeType, is_draw_shape, is_marker logger = logging.getLogger(__name__) if TYPE_CHECKING: from core.gui.app import Application from core.gui.graph.manager import CanvasManager from core.gui.coreclient import CoreClient ZOOM_IN: float = 1.1 ZOOM_OUT: float = 0.9 MOVE_NODE_MODES: Set[GraphMode] = {GraphMode.NODE, GraphMode.SELECT} MOVE_SHAPE_MODES: Set[GraphMode] = {GraphMode.ANNOTATION, GraphMode.SELECT} BACKGROUND_COLOR: str = "#cccccc" class CanvasGraph(tk.Canvas): def __init__( self, master: tk.BaseWidget, app: "Application", manager: "CanvasManager", core: "CoreClient", _id: int, dimensions: Tuple[int, int], ) -> None: super().__init__(master, highlightthickness=0, background=BACKGROUND_COLOR) self.id: int = _id self.app: "Application" = app self.manager: "CanvasManager" = manager self.core: "CoreClient" = core self.selection: Dict[int, int] = {} self.select_box: Optional[Shape] = None self.selected: Optional[int] = None self.nodes: Dict[int, CanvasNode] = {} self.shadow_nodes: Dict[int, ShadowNode] = {} self.shapes: Dict[int, Shape] = {} self.shadow_core_nodes: Dict[int, ShadowNode] = {} # map wireless/EMANE node to the set of MDRs connected to that node self.wireless_network: Dict[int, Set[int]] = {} self.drawing_edge: Optional[CanvasEdge] = None self.rect: Optional[int] = None self.shape_drawing: bool = False self.current_dimensions: Tuple[int, int] = dimensions self.ratio: float = 1.0 self.offset: Tuple[int, int] = (0, 0) self.cursor: Tuple[int, int] = (0, 0) self.to_copy: List[CanvasNode] = [] # background related self.wallpaper_id: Optional[int] = None self.wallpaper: Optional[Image.Image] = None self.wallpaper_drawn: Optional[PhotoImage] = None self.wallpaper_file: str = "" self.scale_option: tk.IntVar = tk.IntVar(value=1) self.adjust_to_dim: tk.BooleanVar = tk.BooleanVar(value=False) # bindings self.setup_bindings() # draw base canvas self.draw_canvas() self.draw_grid() def draw_canvas(self, dimensions: Tuple[int, int] = None) -> None: if self.rect is not None: self.delete(self.rect) if not dimensions: dimensions = self.manager.default_dimensions self.current_dimensions = dimensions self.rect = self.create_rectangle( 0, 0, *dimensions, outline="#000000", fill="#ffffff", width=1, tags="rectangle", ) self.configure(scrollregion=self.bbox(tk.ALL)) def setup_bindings(self) -> None: """ Bind any mouse events or hot keys to the matching action """ self.bind("", self.copy_selected) self.bind("", self.paste_selected) self.bind("", self.cut_selected) self.bind("", self.delete_selected) self.bind("", self.hide_selected) self.bind("", self.click_press) self.bind("", self.click_release) self.bind("", self.click_motion) self.bind("", self.delete_selected) self.bind("", self.ctrl_click) self.bind("", self.double_click) self.bind("", self.zoom) self.bind("", lambda e: self.zoom(e, ZOOM_IN)) self.bind("", lambda e: self.zoom(e, ZOOM_OUT)) self.bind("", lambda e: self.scan_mark(e.x, e.y)) self.bind("", lambda e: self.scan_dragto(e.x, e.y, gain=1)) def get_shadow(self, node: CanvasNode) -> ShadowNode: shadow_node = self.shadow_core_nodes.get(node.core_node.id) if not shadow_node: shadow_node = ShadowNode(self.app, self, node) return shadow_node def get_actual_coords(self, x: float, y: float) -> Tuple[float, float]: actual_x = (x - self.offset[0]) / self.ratio actual_y = (y - self.offset[1]) / self.ratio return actual_x, actual_y def get_scaled_coords(self, x: float, y: float) -> Tuple[float, float]: scaled_x = (x * self.ratio) + self.offset[0] scaled_y = (y * self.ratio) + self.offset[1] return scaled_x, scaled_y def inside_canvas(self, x: float, y: float) -> Tuple[bool, bool]: x1, y1, x2, y2 = self.bbox(self.rect) valid_x = x1 <= x <= x2 valid_y = y1 <= y <= y2 return valid_x and valid_y def valid_position(self, x1: int, y1: int, x2: int, y2: int) -> Tuple[bool, bool]: valid_topleft = self.inside_canvas(x1, y1) valid_bottomright = self.inside_canvas(x2, y2) return valid_topleft and valid_bottomright def draw_grid(self) -> None: """ Create grid. """ width, height = self.width_and_height() width = int(width) height = int(height) for i in range(0, width, 27): self.create_line(i, 0, i, height, dash=(2, 4), tags=tags.GRIDLINE) for i in range(0, height, 27): self.create_line(0, i, width, i, dash=(2, 4), tags=tags.GRIDLINE) self.tag_lower(tags.GRIDLINE) self.tag_lower(self.rect) def canvas_xy(self, event: tk.Event) -> Tuple[float, float]: """ Convert window coordinate to canvas coordinate """ x = self.canvasx(event.x) y = self.canvasy(event.y) return x, y def get_selected(self, event: tk.Event) -> int: """ Retrieve the item id that is on the mouse position """ x, y = self.canvas_xy(event) overlapping = self.find_overlapping(x, y, x, y) selected = None for _id in overlapping: if self.drawing_edge and self.drawing_edge.id == _id: continue elif _id in self.nodes: selected = _id elif _id in self.shapes: selected = _id elif _id in self.shadow_nodes: selected = _id return selected def click_release(self, event: tk.Event) -> None: """ Draw a node or finish drawing an edge according to the current graph mode """ logger.debug("click release") x, y = self.canvas_xy(event) if not self.inside_canvas(x, y): return if self.manager.mode == GraphMode.ANNOTATION: self.focus_set() if self.shape_drawing: shape = self.shapes[self.selected] shape.shape_complete(x, y) self.shape_drawing = False elif self.manager.mode == GraphMode.SELECT: self.focus_set() if self.select_box: x0, y0, x1, y1 = self.coords(self.select_box.id) inside = [ x for x in self.find_enclosed(x0, y0, x1, y1) if "node" in self.gettags(x) or "shape" in self.gettags(x) ] for i in inside: self.select_object(i, True) self.select_box.disappear() self.select_box = None else: self.focus_set() self.selected = self.get_selected(event) logger.debug( "click release selected(%s) mode(%s)", self.selected, self.manager.mode ) if self.manager.mode == GraphMode.EDGE: self.handle_edge_release(event) elif self.manager.mode == GraphMode.NODE: self.add_node(x, y) elif self.manager.mode == GraphMode.PICKNODE: self.manager.mode = GraphMode.NODE self.selected = None def handle_edge_release(self, _event: tk.Event) -> None: # not drawing edge return if not self.drawing_edge: return edge = self.drawing_edge self.drawing_edge = None # edge dst must be a node logger.debug("current selected: %s", self.selected) dst_node = self.nodes.get(self.selected) if not dst_node: edge.delete() return # check if node can be linked if not edge.src.is_linkable(dst_node): edge.delete() return # finalize edge creation edge.drawing(dst_node.position()) edge.complete(dst_node) def select_object(self, object_id: int, choose_multiple: bool = False) -> None: """ create a bounding box when a node is selected """ if not choose_multiple: self.clear_selection() # draw a bounding box if node hasn't been selected yet if object_id not in self.selection: x0, y0, x1, y1 = self.bbox(object_id) selection_id = self.create_rectangle( (x0 - 6, y0 - 6, x1 + 6, y1 + 6), activedash=True, dash="-", tags=tags.SELECTION, ) self.selection[object_id] = selection_id else: selection_id = self.selection.pop(object_id) self.delete(selection_id) def clear_selection(self) -> None: """ Clear current selection boxes. """ for _id in self.selection.values(): self.delete(_id) self.selection.clear() def move_selection(self, object_id: int, x_offset: float, y_offset: float) -> None: select_id = self.selection.get(object_id) if select_id is not None: self.move(select_id, x_offset, y_offset) def delete_selected_objects(self, _event: tk.Event = None) -> None: edges = set() nodes = [] for object_id in self.selection: # delete selection box selection_id = self.selection[object_id] self.delete(selection_id) # delete node and related edges if object_id in self.nodes: canvas_node = self.nodes.pop(object_id) # delete related edges while canvas_node.edges: edge = canvas_node.edges.pop() if edge in edges: continue edges.add(edge) edge.delete() # delete node canvas_node.delete() nodes.append(canvas_node) # delete shape if object_id in self.shapes: shape = self.shapes.pop(object_id) shape.delete() self.selection.clear() self.core.deleted_canvas_nodes(nodes) def hide_selected(self, _event: tk.Event = None) -> None: for object_id in self.selection: # delete selection box selection_id = self.selection[object_id] self.delete(selection_id) # hide node and related edges if object_id in self.nodes: canvas_node = self.nodes[object_id] canvas_node.hide() def show_hidden(self) -> None: for node in self.nodes.values(): if node.hidden: node.show() def zoom(self, event: tk.Event, factor: float = None) -> None: if not factor: factor = ZOOM_IN if event.delta > 0 else ZOOM_OUT event.x, event.y = self.canvasx(event.x), self.canvasy(event.y) self.scale(tk.ALL, event.x, event.y, factor, factor) self.configure(scrollregion=self.bbox(tk.ALL)) self.ratio *= float(factor) self.offset = ( self.offset[0] * factor + event.x * (1 - factor), self.offset[1] * factor + event.y * (1 - factor), ) logger.debug("ratio: %s", self.ratio) logger.debug("offset: %s", self.offset) self.app.statusbar.set_zoom(self.ratio) if self.wallpaper: self.redraw_wallpaper() def click_press(self, event: tk.Event) -> None: """ Start drawing an edge if mouse click is on a node """ x, y = self.canvas_xy(event) if not self.inside_canvas(x, y): return self.cursor = x, y selected = self.get_selected(event) logger.debug("click press(%s): %s", self.cursor, selected) x_check = self.cursor[0] - self.offset[0] y_check = self.cursor[1] - self.offset[1] logger.debug("click press offset(%s, %s)", x_check, y_check) is_node = selected in self.nodes if self.manager.mode == GraphMode.EDGE and is_node: node = self.nodes[selected] self.drawing_edge = CanvasEdge(self.app, node) self.organize() if self.manager.mode == GraphMode.ANNOTATION: if is_marker(self.manager.annotation_type): r = self.app.toolbar.marker_frame.size.get() self.create_oval( x - r, y - r, x + r, y + r, fill=self.app.toolbar.marker_frame.color, outline="", tags=(tags.MARKER, tags.ANNOTATION), state=self.manager.show_annotations.state(), ) return if selected is None: shape = Shape(self.app, self, self.manager.annotation_type, x, y) self.selected = shape.id self.shape_drawing = True self.shapes[shape.id] = shape if selected is not None: if selected not in self.selection: if selected in self.shapes: shape = self.shapes[selected] self.select_object(shape.id) self.selected = selected elif selected in self.nodes: node = self.nodes[selected] self.select_object(node.id) self.selected = selected logger.debug( "selected node(%s), coords: (%s, %s)", node.core_node.name, node.core_node.position.x, node.core_node.position.y, ) elif selected in self.shadow_nodes: shadow_node = self.shadow_nodes[selected] self.select_object(shadow_node.id) self.selected = selected logger.debug( "selected shadow node(%s), coords: (%s, %s)", shadow_node.node.core_node.name, shadow_node.node.core_node.position.x, shadow_node.node.core_node.position.y, ) else: if self.manager.mode == GraphMode.SELECT: shape = Shape(self.app, self, ShapeType.RECTANGLE, x, y) self.select_box = shape self.clear_selection() def ctrl_click(self, event: tk.Event) -> None: # update cursor location x, y = self.canvas_xy(event) if not self.inside_canvas(x, y): return self.cursor = x, y # handle multiple selections logger.debug("control left click: %s", event) selected = self.get_selected(event) if ( selected not in self.selection and selected in self.shapes or selected in self.nodes ): self.select_object(selected, choose_multiple=True) def click_motion(self, event: tk.Event) -> None: x, y = self.canvas_xy(event) if not self.inside_canvas(x, y): if self.select_box: self.select_box.delete() self.select_box = None if is_draw_shape(self.manager.annotation_type) and self.shape_drawing: shape = self.shapes.pop(self.selected) shape.delete() self.shape_drawing = False return x_offset = x - self.cursor[0] y_offset = y - self.cursor[1] self.cursor = x, y if self.manager.mode == GraphMode.EDGE and self.drawing_edge is not None: self.drawing_edge.drawing(self.cursor) if self.manager.mode == GraphMode.ANNOTATION: if is_draw_shape(self.manager.annotation_type) and self.shape_drawing: shape = self.shapes[self.selected] shape.shape_motion(x, y) return elif is_marker(self.manager.annotation_type): r = self.app.toolbar.marker_frame.size.get() self.create_oval( x - r, y - r, x + r, y + r, fill=self.app.toolbar.marker_frame.color, outline="", tags=(tags.MARKER, tags.ANNOTATION), ) return if self.manager.mode == GraphMode.EDGE: return # move selected objects if self.selection: for selected_id in self.selection: if self.manager.mode in MOVE_SHAPE_MODES and selected_id in self.shapes: shape = self.shapes[selected_id] shape.motion(x_offset, y_offset) elif self.manager.mode in MOVE_NODE_MODES and selected_id in self.nodes: node = self.nodes[selected_id] node.motion(x_offset, y_offset, update=self.core.is_runtime()) elif ( self.manager.mode in MOVE_NODE_MODES and selected_id in self.shadow_nodes ): shadow_node = self.shadow_nodes[selected_id] shadow_node.motion(x_offset, y_offset) else: if self.select_box and self.manager.mode == GraphMode.SELECT: self.select_box.shape_motion(x, y) def double_click(self, event: tk.Event) -> None: selected = self.get_selected(event) if selected is not None and selected in self.shapes: shape = self.shapes[selected] dialog = ShapeDialog(self.app, shape) dialog.show() def add_node(self, x: float, y: float) -> None: if self.selected is not None and self.selected not in self.shapes: return actual_x, actual_y = self.get_actual_coords(x, y) core_node = self.core.create_node( actual_x, actual_y, self.manager.node_draw.node_type, self.manager.node_draw.model, ) if not core_node: return core_node.canvas = self.id node = CanvasNode(self.app, self, x, y, core_node, self.manager.node_draw.image) self.nodes[node.id] = node self.core.set_canvas_node(core_node, node) def width_and_height(self) -> Tuple[int, int]: """ retrieve canvas width and height in pixels """ x0, y0, x1, y1 = self.coords(self.rect) canvas_w = abs(x0 - x1) canvas_h = abs(y0 - y1) return canvas_w, canvas_h def get_wallpaper_image(self) -> Image.Image: width = int(self.wallpaper.width * self.ratio) height = int(self.wallpaper.height * self.ratio) image = self.wallpaper.resize((width, height), Image.ANTIALIAS) return image def draw_wallpaper( self, image: PhotoImage, x: float = None, y: float = None ) -> None: if x is None and y is None: x1, y1, x2, y2 = self.bbox(self.rect) x = (x1 + x2) / 2 y = (y1 + y2) / 2 self.wallpaper_id = self.create_image((x, y), image=image, tags=tags.WALLPAPER) self.wallpaper_drawn = image def wallpaper_upper_left(self) -> None: self.delete(self.wallpaper_id) # create new scaled image, cropped if needed width, height = self.width_and_height() image = self.get_wallpaper_image() cropx = image.width cropy = image.height if image.width > width: cropx = image.width if image.height > height: cropy = image.height cropped = image.crop((0, 0, cropx, cropy)) image = PhotoImage(cropped) # draw on canvas x1, y1, _, _ = self.bbox(self.rect) x = (cropx / 2) + x1 y = (cropy / 2) + y1 self.draw_wallpaper(image, x, y) def wallpaper_center(self) -> None: """ place the image at the center of canvas """ self.delete(self.wallpaper_id) # dimension of the cropped image width, height = self.width_and_height() image = self.get_wallpaper_image() cropx = 0 if image.width > width: cropx = (image.width - width) / 2 cropy = 0 if image.height > height: cropy = (image.height - height) / 2 x1 = 0 + cropx y1 = 0 + cropy x2 = image.width - cropx y2 = image.height - cropy cropped = image.crop((x1, y1, x2, y2)) image = PhotoImage(cropped) self.draw_wallpaper(image) def wallpaper_scaled(self) -> None: """ scale image based on canvas dimension """ self.delete(self.wallpaper_id) canvas_w, canvas_h = self.width_and_height() image = self.wallpaper.resize((int(canvas_w), int(canvas_h)), Image.ANTIALIAS) image = PhotoImage(image) self.draw_wallpaper(image) def resize_to_wallpaper(self) -> None: self.delete(self.wallpaper_id) image = PhotoImage(self.wallpaper) self.redraw_canvas((image.width(), image.height())) self.draw_wallpaper(image) def redraw_canvas(self, dimensions: Tuple[int, int] = None) -> None: logger.debug("redrawing canvas to dimensions: %s", dimensions) # reset scale and move back to original position logger.debug("resetting scaling: %s %s", self.ratio, self.offset) factor = 1 / self.ratio self.scale(tk.ALL, self.offset[0], self.offset[1], factor, factor) self.move(tk.ALL, -self.offset[0], -self.offset[1]) # reset ratio and offset self.ratio = 1.0 self.offset = (0, 0) # redraw canvas rectangle self.draw_canvas(dimensions) # redraw gridlines to new canvas size self.delete(tags.GRIDLINE) self.draw_grid() self.app.manager.show_grid.click_handler() def redraw_wallpaper(self) -> None: if self.adjust_to_dim.get(): logger.debug("drawing wallpaper to canvas dimensions") self.resize_to_wallpaper() else: option = ScaleOption(self.scale_option.get()) logger.debug("drawing canvas using scaling option: %s", option) if option == ScaleOption.UPPER_LEFT: self.wallpaper_upper_left() elif option == ScaleOption.CENTERED: self.wallpaper_center() elif option == ScaleOption.SCALED: self.wallpaper_scaled() elif option == ScaleOption.TILED: logger.warning("tiled background not implemented yet") self.organize() def organize(self) -> None: for tag in tags.ORGANIZE_TAGS: self.tag_raise(tag) def set_wallpaper(self, filename: Optional[str]) -> None: logger.info("setting canvas(%s) background: %s", self.id, filename) if filename: img = Image.open(filename) self.wallpaper = img self.wallpaper_file = filename self.redraw_wallpaper() else: if self.wallpaper_id is not None: self.delete(self.wallpaper_id) self.wallpaper = None self.wallpaper_file = None def is_selection_mode(self) -> bool: return self.manager.mode == GraphMode.SELECT def create_edge(self, src: CanvasNode, dst: CanvasNode) -> CanvasEdge: """ create an edge between source node and destination node """ edge = CanvasEdge(self.app, src) edge.complete(dst) return edge def copy_selected(self, _event: tk.Event = None) -> None: if self.core.is_runtime(): logger.debug("copy is disabled during runtime state") return if self.selection: logger.debug("to copy nodes: %s", self.selection) self.to_copy.clear() for node_id in self.selection.keys(): canvas_node = self.nodes[node_id] self.to_copy.append(canvas_node) def cut_selected(self, _event: tk.Event = None) -> None: if self.core.is_runtime(): logger.debug("cut is disabled during runtime state") return self.copy_selected() self.delete_selected() def delete_selected(self, _event: tk.Event = None) -> None: """ delete selected nodes and any data that relates to it """ logger.debug("press delete key") if self.core.is_runtime(): logger.debug("node deletion is disabled during runtime state") return self.delete_selected_objects() self.app.default_info() def paste_selected(self, _event: tk.Event = None) -> None: if self.core.is_runtime(): logger.debug("paste is disabled during runtime state") return # maps original node canvas id to copy node canvas id copy_map = {} # the edges that will be copy over to_copy_edges = set() to_copy_ids = {x.id for x in self.to_copy} for canvas_node in self.to_copy: core_node = canvas_node.core_node actual_x = core_node.position.x + 50 actual_y = core_node.position.y + 50 scaled_x, scaled_y = self.get_scaled_coords(actual_x, actual_y) copy = self.core.create_node( actual_x, actual_y, core_node.type, core_node.model ) if not copy: continue node = CanvasNode( self.app, self, scaled_x, scaled_y, copy, canvas_node.image ) # copy configurations and services node.core_node.services = core_node.services.copy() node.core_node.config_services = core_node.config_services.copy() node.core_node.emane_model_configs = deepcopy(core_node.emane_model_configs) node.core_node.wlan_config = deepcopy(core_node.wlan_config) node.core_node.mobility_config = deepcopy(core_node.mobility_config) node.core_node.service_configs = deepcopy(core_node.service_configs) node.core_node.service_file_configs = deepcopy( core_node.service_file_configs ) node.core_node.config_service_configs = deepcopy( core_node.config_service_configs ) copy_map[canvas_node.id] = node.id self.nodes[node.id] = node self.core.set_canvas_node(copy, node) for edge in canvas_node.edges: if edge.src not in to_copy_ids or edge.dst not in to_copy_ids: if canvas_node.id == edge.src: dst_node = self.nodes[edge.dst] copy_edge = self.create_edge(node, dst_node) elif canvas_node.id == edge.dst: src_node = self.nodes[edge.src] copy_edge = self.create_edge(src_node, node) else: continue copy_link = copy_edge.link iface1_id = copy_link.iface1.id if copy_link.iface1 else None iface2_id = copy_link.iface2.id if copy_link.iface2 else None options = edge.link.options if options: copy_edge.link.options = deepcopy(options) if options and options.unidirectional: asym_iface1 = None if iface1_id is not None: asym_iface1 = Interface(id=iface1_id) asym_iface2 = None if iface2_id is not None: asym_iface2 = Interface(id=iface2_id) copy_edge.asymmetric_link = Link( node1_id=copy_link.node2_id, node2_id=copy_link.node1_id, iface1=asym_iface2, iface2=asym_iface1, options=deepcopy(edge.asymmetric_link.options), ) copy_edge.redraw() else: to_copy_edges.add(edge) # copy link and link config for edge in to_copy_edges: src_node_id = copy_map[edge.src] dst_node_id = copy_map[edge.dst] src_node_copy = self.nodes[src_node_id] dst_node_copy = self.nodes[dst_node_id] copy_edge = self.create_edge(src_node_copy, dst_node_copy) copy_link = copy_edge.link iface1_id = copy_link.iface1.id if copy_link.iface1 else None iface2_id = copy_link.iface2.id if copy_link.iface2 else None options = edge.link.options if options: copy_link.options = deepcopy(options) if options and options.unidirectional: asym_iface1 = None if iface1_id is not None: asym_iface1 = Interface(id=iface1_id) asym_iface2 = None if iface2_id is not None: asym_iface2 = Interface(id=iface2_id) copy_edge.asymmetric_link = Link( node1_id=copy_link.node2_id, node2_id=copy_link.node1_id, iface1=asym_iface2, iface2=asym_iface1, options=deepcopy(edge.asymmetric_link.options), ) copy_edge.redraw() self.itemconfig( copy_edge.id, width=self.itemcget(edge.id, "width"), fill=self.itemcget(edge.id, "fill"), ) self.tag_raise(tags.NODE) def scale_graph(self) -> None: for node_id, canvas_node in self.nodes.items(): image = nutils.get_icon(canvas_node.core_node, self.app) self.itemconfig(node_id, image=image) canvas_node.image = image canvas_node.scale_text() canvas_node.scale_antennas() for edge_id in self.find_withtag(tags.EDGE): self.itemconfig(edge_id, width=int(EDGE_WIDTH * self.app.app_scale)) def get_metadata(self) -> Dict[str, Any]: wallpaper_path = None if self.wallpaper_file: wallpaper = Path(self.wallpaper_file) if appconfig.BACKGROUNDS_PATH == wallpaper.parent: wallpaper_path = wallpaper.name else: wallpaper_path = str(wallpaper) return dict( id=self.id, wallpaper=wallpaper_path, wallpaper_style=self.scale_option.get(), fit_image=self.adjust_to_dim.get(), dimensions=self.current_dimensions, ) def parse_metadata(self, config: Dict[str, Any]) -> None: fit_image = config.get("fit_image", False) self.adjust_to_dim.set(fit_image) wallpaper_style = config.get("wallpaper_style", 1) self.scale_option.set(wallpaper_style) dimensions = config.get("dimensions") if dimensions: self.redraw_canvas(dimensions) wallpaper = config.get("wallpaper") if wallpaper: wallpaper = Path(wallpaper) if not wallpaper.is_file(): wallpaper = appconfig.BACKGROUNDS_PATH.joinpath(wallpaper) logger.info("canvas(%s), wallpaper: %s", self.id, wallpaper) if wallpaper.is_file(): self.set_wallpaper(str(wallpaper)) else: self.app.show_error( "Background Error", f"background file not found: {wallpaper}" )