diff --git a/src/navigate/controller/controller.py b/src/navigate/controller/controller.py index 0dc05281e..fbe512e73 100644 --- a/src/navigate/controller/controller.py +++ b/src/navigate/controller/controller.py @@ -906,10 +906,7 @@ def refresh(width: int, height: int) -> None: None """ self.view.scroll_frame.resize(width, height) - self.view.right_frame.config( - width=width - self.view.left_frame.winfo_width() - 3, - height=height - self.view.left_frame.winfo_height(), - ) + self.view.update_idletasks() if not self.resize_ready_flag: return diff --git a/src/navigate/controller/sub_controllers/camera_view.py b/src/navigate/controller/sub_controllers/camera_view.py index d160f32bb..92fb11f29 100644 --- a/src/navigate/controller/sub_controllers/camera_view.py +++ b/src/navigate/controller/sub_controllers/camera_view.py @@ -30,6 +30,7 @@ # POSSIBILITY OF SUCH DAMAGE. # Standard Library Imports +from __future__ import annotations import platform import tkinter as tk from tkinter import messagebox @@ -245,6 +246,9 @@ def __init__(self, view, parent_controller=None) -> None: #: event: The resize event ID. self.resize_event_id = None + #: event: The bound widget resize handler ID. + self.resize_binding_id = None + #: list: The selected channels being acquired. self.selected_channels = None @@ -1309,10 +1313,11 @@ def move_stage(self) -> None: title="Warning", message="Can't move to there! Invalid stage position!" ) - def update_canvas_size(self) -> None: + def update_canvas_size( + self, width: int | None = None, height: int | None = None + ) -> None: """Update the canvas size.""" - r_canvas_width = int(self.view.canvas["width"]) - r_canvas_height = int(self.view.canvas["height"]) + r_canvas_width, r_canvas_height = self._get_canvas_widget_size(width, height) img_ratio = self.original_image_width / self.original_image_height canvas_ratio = r_canvas_width / r_canvas_height @@ -1327,6 +1332,8 @@ def update_canvas_size(self) -> None: self.canvas_height_scale = float( self.original_image_height / self.canvas_height ) + self.view.canvas_width = self.canvas_width + self.view.canvas_height = self.canvas_height def _prepare_zoom_window(self) -> tuple[slice, slice]: """Update zoom state and return crop slices for Y and X.""" @@ -1475,6 +1482,25 @@ def get_absolute_position(self) -> tuple: y = self.parent_controller.view.winfo_pointery() return x, y + def _get_canvas_widget_size( + self, width: int | None = None, height: int | None = None + ) -> tuple[int, int]: + """Return the actual drawable canvas size with stable fallbacks.""" + resolved_width = int(width) if width is not None else 0 + resolved_height = int(height) if height is not None else 0 + + if resolved_width <= 1: + resolved_width = int(self.canvas.winfo_width()) + if resolved_height <= 1: + resolved_height = int(self.canvas.winfo_height()) + + if resolved_width <= 1: + resolved_width = int(self.canvas.cget("width")) + if resolved_height <= 1: + resolved_height = int(self.canvas.cget("height")) + + return max(1, resolved_width), max(1, resolved_height) + def _ensure_canvas_image(self, w: int, h: int, mode: str) -> None: """Create/recreate the backing PhotoImage when size or mode changes. @@ -1504,13 +1530,13 @@ def _ensure_canvas_image(self, w: int, h: int, mode: str) -> None: self._photo = ImageTk.PhotoImage(base) self._photo_mode = mode - if getattr(self, "_img_item", None) is None: - self._img_item = self.canvas.create_image( - 0, 0, image=self._photo, anchor="nw" - ) - else: - # Reuse the same canvas item, just rebind the image - self.canvas.itemconfig(self._img_item, image=self._photo) + if getattr(self, "_img_item", None) is None: + self._img_item = self.canvas.create_image( + 0, 0, image=self._photo, anchor="nw" + ) + else: + # Reuse the same canvas item, just rebind the image + self.canvas.itemconfig(self._img_item, image=self._photo) def populate_image(self, image: np.ndarray) -> None: """Update the Tk canvas using a persistent PhotoImage + paste. @@ -1614,61 +1640,45 @@ def resize(self, event: tk.Event) -> None: """ if not self.parent_controller.resize_ready_flag: return - if event.width < 512 or event.height < 512: + event_widget = getattr(event, "widget", None) + resolved_widget = getattr(event_widget, "widget", event_widget) + if resolved_widget not in (self.view, self.canvas): return - if event.widget != self.view: + + width = int(getattr(event, "width", 0)) + height = int(getattr(event, "height", 0)) + if width <= 1 or height <= 1: return - if self.view.is_docked: - left_width = self.parent_controller.view.left_frame.winfo_width() - top_height = self.parent_controller.view.top_frame.winfo_height() - w_width = self.parent_controller.view.winfo_width() - w_height = self.parent_controller.view.winfo_height() - width = max(w_width - left_width - 16, 560 + self.view.lut.winfo_width()) - height = max(w_height - top_height - 50, 670) - else: - width = event.width - height = event.height - 24 if self.resize_event_id: self.view.after_cancel(self.resize_event_id) - self.resize_event_id = self.view.after(300, lambda: self.refresh(width, height)) + self.resize_event_id = self.view.after( + 100, lambda w=width, h=height: self.refresh(w, h) + ) - def refresh(self, width: int, height: int) -> None: + def refresh(self, width: int | None = None, height: int | None = None) -> None: """Refresh the window. Parameters ---------- - width : int - Width of the window. - height : int - Height of the window. + width : int or None + Width of the canvas viewport. + height : int or None + Height of the canvas viewport. """ + width, height = self._get_canvas_widget_size(width, height) if ( self.width and self.height - and abs(width - self.width) < 10 - and abs(height - self.height) < 10 + and abs(width - self.width) < 2 + and abs(height - self.height) < 2 ): return - self.canvas_width = width - self.view.lut.winfo_width() - 24 - widget_height = 0 - for widget in self.view.cam_image.winfo_children(): - if widget != self.view.canvas: - if self.view.is_docked or widget.winfo_ismapped(): - widget_height += widget.winfo_height() + 5 - if widget.winfo_height() < 30: - widget_height += 30 - - self.canvas_height = ( - height - widget_height - (50 if self.view.is_docked else -5) - ) - self.view.canvas.config(width=self.canvas_width, height=self.canvas_height) - self.view.update_idletasks() self.width, self.height = width, height # if resize the window during acquisition, the image showing should be updated - self.update_canvas_size() + self.update_canvas_size(width, height) self.reset_display(False) def update_min_max_counts(self, display: bool = False): @@ -1762,7 +1772,7 @@ def __init__(self, view, parent_controller=None) -> None: # Slider Binding self.view.slider.bind("", self.slider_update) - self.resize_event_id = self.view.bind("", self.resize) + self.resize_binding_id = self.view.canvas.bind("", self.resize) #: str: The display state. self.display_state = "Live" @@ -2397,7 +2407,7 @@ def __init__(self, view, parent_controller=None) -> None: #: dict: The render widgets. self.render_widgets = self.view.render.get_widgets() - self.resize_event_id = self.view.bind("", self.resize) + self.resize_binding_id = self.view.canvas.bind("", self.resize) #: bool: The display enabled flag. self.display_enabled = tk.BooleanVar() @@ -2872,6 +2882,7 @@ def try_to_display_image(self, image: np.ndarray) -> None: def _clear_mip(self) -> None: """Clear the mip but keep canvas interactive.""" self.canvas.delete("all") + self._img_item = None self.tk_image = None self.canvas.create_text( self.canvas_width // 2, @@ -3030,8 +3041,7 @@ def down_sample_image( """ sx, sy = self.canvas_width, self.canvas_height if self.render_widgets["perspective"].get() == "Multi": - sx = int(self.view.canvas["width"]) - sy = int(self.view.canvas["height"]) + sx, sy = self._get_canvas_widget_size() self.canvas_width = sx self.canvas_height = sy down_sampled_image = cv2.resize(image, (sx, sy)) diff --git a/src/navigate/controller/sub_controllers/waveform_tab.py b/src/navigate/controller/sub_controllers/waveform_tab.py index 14fc30d50..c14b7c70b 100644 --- a/src/navigate/controller/sub_controllers/waveform_tab.py +++ b/src/navigate/controller/sub_controllers/waveform_tab.py @@ -40,6 +40,7 @@ # Local Imports from navigate.controller.sub_controllers.gui import GUIController from navigate.tools.waveform_template_funcs import get_waveform_template_parameters +from navigate.view.custom_widgets.common import themed_grid # Logger Setup p = __name__.split(".")[1] @@ -145,8 +146,11 @@ def initialize_plots(self): """Initialize the plots in the waveform tab.""" self.view.plot_etl = self.view.fig.add_subplot(211) self.view.plot_galvo = self.view.fig.add_subplot(212) - self.view.canvas.get_tk_widget().grid( - row=5, column=0, columnspan=3, sticky=NSEW, padx=(5, 5), pady=(5, 5) + themed_grid( + self.view.canvas.get_tk_widget(), + row=0, + column=0, + sticky=NSEW, ) def plot_waveforms(self, event): diff --git a/src/navigate/view/configurator_application_window.py b/src/navigate/view/configurator_application_window.py index dbfc57fc0..7d6dd4cf7 100644 --- a/src/navigate/view/configurator_application_window.py +++ b/src/navigate/view/configurator_application_window.py @@ -40,6 +40,7 @@ # Local Imports from navigate.view.custom_widgets.DockableNotebook import DockableNotebook from navigate.view.custom_widgets.CollapsibleFrame import CollapsibleFrame +from navigate.view.theme import get_theme_padding_px, get_theme_space_px # Logger Setup p = __name__.split(".")[1] @@ -101,9 +102,20 @@ def __init__(self, root, *args, **kwargs): self.microscope_frame = ttk.Frame(self.root) self.grid(column=0, row=0, sticky=tk.NSEW) - self.top_frame.grid(row=0, column=0, sticky=tk.NSEW, padx=3, pady=3) + self.top_frame.grid( + row=0, + column=0, + sticky=tk.NSEW, + padx=get_theme_space_px(3), + pady=get_theme_space_px(3), + ) self.microscope_frame.grid( - row=1, column=0, columnspan=5, sticky=tk.NSEW, padx=3, pady=3 + row=1, + column=0, + columnspan=5, + sticky=tk.NSEW, + padx=get_theme_space_px(3), + pady=get_theme_space_px(3), ) #: ttk.Frame: The top frame of the application @@ -144,24 +156,54 @@ def __init__(self, main_frame, root, *args, **kwargs): tk.Grid.rowconfigure(self, "all", weight=1) self.new_button = ttk.Button(root, text="New Configuration") - self.new_button.grid(row=0, column=0, sticky=tk.NE, padx=3, pady=(10, 1)) + self.new_button.grid( + row=0, + column=0, + sticky=tk.NE, + padx=get_theme_space_px(3), + pady=get_theme_padding_px((10, 1)), + ) self.new_button.config(width=15) self.load_button = ttk.Button(root, text="Load Configuration") - self.load_button.grid(row=0, column=1, sticky=tk.NE, padx=3, pady=(10, 1)) + self.load_button.grid( + row=0, + column=1, + sticky=tk.NE, + padx=get_theme_space_px(3), + pady=get_theme_padding_px((10, 1)), + ) self.load_button.config(width=15) self.add_button = ttk.Button(root, text="Add A Microscope") - self.add_button.grid(row=0, column=2, sticky=tk.NE, padx=3, pady=(10, 1)) + self.add_button.grid( + row=0, + column=2, + sticky=tk.NE, + padx=get_theme_space_px(3), + pady=get_theme_padding_px((10, 1)), + ) self.add_button.config(width=15) self.save_button = ttk.Button(root, text="Save") - self.save_button.grid(row=0, column=3, sticky=tk.NE, padx=3, pady=(10, 1)) + self.save_button.grid( + row=0, + column=3, + sticky=tk.NE, + padx=get_theme_space_px(3), + pady=get_theme_padding_px((10, 1)), + ) self.save_button.config(width=15) #: ttk.Button: The button to cancel the application. self.cancel_button = ttk.Button(root, text="Cancel") - self.cancel_button.grid(row=0, column=4, sticky=tk.NE, padx=3, pady=(10, 1)) + self.cancel_button.grid( + row=0, + column=4, + sticky=tk.NE, + padx=get_theme_space_px(3), + pady=get_theme_padding_px((10, 1)), + ) self.cancel_button.config(width=15) @@ -319,13 +361,19 @@ def __init__( self.top_frame = ttk.Frame(content_frame) - self.top_frame.grid(row=0, column=0, sticky=tk.NSEW, padx=10) + self.top_frame.grid( + row=0, column=0, sticky=tk.NSEW, padx=get_theme_space_px(10) + ) self.hardware_frame = ttk.Frame(content_frame) - self.hardware_frame.grid(row=1, column=0, sticky=tk.NSEW, padx=10) + self.hardware_frame.grid( + row=1, column=0, sticky=tk.NSEW, padx=get_theme_space_px(10) + ) self.bottom_frame = ttk.Frame(content_frame) - self.bottom_frame.grid(row=2, column=0, sticky=tk.NSEW, padx=10) + self.bottom_frame.grid( + row=2, column=0, sticky=tk.NSEW, padx=get_theme_space_px(10) + ) self.frame_row = 0 self.row_offset = self.frame_row + 1 @@ -369,9 +417,19 @@ def create_hardware_widgets(self, hardware_widgets, frame, direction="vertical") continue if v[1] == "Label": label = ttk.Label(content_frame, text=v[0]) - label.grid(row=i, column=0, sticky=tk.NW, padx=3) + label.grid( + row=i, + column=0, + sticky=tk.NW, + padx=get_theme_space_px(3), + ) seperator = ttk.Separator(content_frame) - seperator.grid(row=i + 1, columnspan=2, sticky=tk.NSEW, padx=3) + seperator.grid( + row=i + 1, + columnspan=2, + sticky=tk.NSEW, + padx=get_theme_space_px(3), + ) i += 2 continue elif v[1] != "Button": @@ -379,9 +437,21 @@ def create_hardware_widgets(self, hardware_widgets, frame, direction="vertical") label_text = v[0] + " :" if v[0][-1] != ":" else v[0] label = ttk.Label(content_frame, text=label_text) if direction == "vertical": - label.grid(row=i, column=0, sticky=tk.NW, padx=(3, 10), pady=3) + label.grid( + row=i, + column=0, + sticky=tk.NW, + padx=get_theme_padding_px((3, 10)), + pady=get_theme_space_px(3), + ) else: - label.grid(row=0, column=i, sticky=tk.NW, padx=(5, 3), pady=3) + label.grid( + row=0, + column=i, + sticky=tk.NW, + padx=get_theme_padding_px((5, 3)), + pady=get_theme_space_px(3), + ) i += 1 if v[1] == "Checkbutton": widget = widget_types[v[1]]( @@ -392,7 +462,7 @@ def create_hardware_widgets(self, hardware_widgets, frame, direction="vertical") content_frame, textvariable=self.variables[k], width=30 ) if v[1] == "Combobox": - if type(v[3]) == list: + if isinstance(v[3], list): v[3] = dict([(t, t) for t in v[3]]) self.values_dict[k] = v[3] temp = list(v[3].keys()) @@ -404,7 +474,7 @@ def create_hardware_widgets(self, hardware_widgets, frame, direction="vertical") else: widget.set(temp[-1]) elif v[1] == "Spinbox": - if type(v[3]) != dict: + if not isinstance(v[3], dict): v[3] = {} widget.config(from_=v[3].get("from", 0)) widget.config(to=v[3].get("to", 100000)) @@ -423,17 +493,41 @@ def create_hardware_widgets(self, hardware_widgets, frame, direction="vertical") ), ) if direction == "vertical": - widget.grid(row=i, column=1, sticky=tk.NSEW, padx=5, pady=3) + widget.grid( + row=i, + column=1, + sticky=tk.NSEW, + padx=get_theme_space_px(5), + pady=get_theme_space_px(3), + ) else: - widget.grid(row=0, column=i, sticky=tk.NW, padx=(10, 3), pady=(3, 0)) + widget.grid( + row=0, + column=i, + sticky=tk.NW, + padx=get_theme_padding_px((10, 3)), + pady=get_theme_padding_px((3, 0)), + ) # display info label if len(v) >= 5 and v[4]: label = ttk.Label(content_frame, text=v[4]) if direction == "vertical": - label.grid(row=i, column=2, sticky=tk.NW, padx=(10, 10), pady=3) + label.grid( + row=i, + column=2, + sticky=tk.NW, + padx=get_theme_padding_px((10, 10)), + pady=get_theme_space_px(3), + ) else: - label.grid(row=1, column=i, sticky=tk.NW, padx=(10, 3), pady=0) + label.grid( + row=1, + column=i, + sticky=tk.NW, + padx=get_theme_padding_px((10, 3)), + pady=get_theme_space_px(0), + ) i += 1 def build_widgets(self, widgets, *args, parent=None, widgets_value=None, **kwargs): @@ -474,7 +568,12 @@ def build_widgets(self, widgets, *args, parent=None, widgets_value=None, **kwarg frame.label.bind("", self.create_toggle_function(frame)) else: frame = ttk.Frame(parent) - frame.grid(row=self.frame_row, column=0, sticky=tk.NSEW, padx=20) + frame.grid( + row=self.frame_row, + column=0, + sticky=tk.NSEW, + padx=get_theme_space_px(20), + ) self.frame_row += 1 ref = None diff --git a/src/navigate/view/custom_widgets/CollapsibleFrame.py b/src/navigate/view/custom_widgets/CollapsibleFrame.py index 8ec38c2d4..eeac62d92 100644 --- a/src/navigate/view/custom_widgets/CollapsibleFrame.py +++ b/src/navigate/view/custom_widgets/CollapsibleFrame.py @@ -33,7 +33,7 @@ # Third-party imports # Local imports -from navigate.view.theme import get_theme_color +from navigate.view.theme import get_theme_color, get_theme_space_px class CollapsibleFrame(tk.Frame): @@ -72,7 +72,7 @@ def __init__( bg=get_theme_color("surface_bg", "lightgrey"), fg=get_theme_color("text", "black"), relief="raised", - padx=5, + padx=get_theme_space_px(5), ) self.label.grid(row=0, column=0, sticky=tk.NSEW) diff --git a/src/navigate/view/custom_widgets/LabelInputWidgetFactory.py b/src/navigate/view/custom_widgets/LabelInputWidgetFactory.py index b45db4ff8..4a9aeb5fd 100644 --- a/src/navigate/view/custom_widgets/LabelInputWidgetFactory.py +++ b/src/navigate/view/custom_widgets/LabelInputWidgetFactory.py @@ -39,6 +39,7 @@ # Third Party Imports # Local Imports +from navigate.view.custom_widgets.common import configure_grid, themed_grid from navigate.view.custom_widgets.validation import ValidatedCombobox, ValidatedSpinbox from navigate.view.custom_widgets.hover import ( HoverButton, @@ -46,12 +47,21 @@ HoverRadioButton, HoverCheckButton, ) +from navigate.view.theme import get_theme_spacing # Logger Setup p = __name__.split(".")[1] logger = logging.getLogger(p) +def _resolve_pad_value(value): + """Resolve a padding value or spacing token to pixels.""" + + if isinstance(value, str): + return get_theme_spacing(value) + return int(value) + + class LabelInput(ttk.Frame): """Widget class that contains label and input together. @@ -165,26 +175,22 @@ def __init__( """Specify label position""" if label_pos == "top": if self.label is not None: - self.label.grid(row=0, column=0, sticky=tk.EW) + themed_grid(self.label, row=0, column=0, sticky=tk.EW) widget_row = 1 - self.rowconfigure(index=0, weight=1) - self.rowconfigure(index=1, weight=1) + configure_grid(self, columns={0: 1}, rows={0: 0, 1: 1}) else: widget_row = 0 - self.rowconfigure(index=0, weight=1) - self.widget.grid(row=widget_row, column=0, sticky=(tk.W + tk.E)) - self.columnconfigure(0, weight=1) + configure_grid(self, columns={0: 1}, rows={0: 1}) + themed_grid(self.widget, row=widget_row, column=0, sticky=tk.EW) else: if self.label is not None: - self.label.grid(row=0, column=0, sticky=tk.EW) + themed_grid(self.label, row=0, column=0, sticky=tk.EW) widget_column = 1 - self.columnconfigure(index=0, weight=1) - self.columnconfigure(index=1, weight=1) + configure_grid(self, columns={0: 0, 1: 1}, rows={0: 1}) else: widget_column = 0 - self.columnconfigure(index=0, weight=1) - self.widget.grid(row=0, column=widget_column, sticky=(tk.W + tk.E)) - self.rowconfigure(0, weight=1) + configure_grid(self, columns={0: 1}, rows={0: 1}) + themed_grid(self.widget, row=0, column=widget_column, sticky=tk.EW) def get(self, default=None): """Returns the value of the input widget @@ -330,7 +336,10 @@ def pad_input(self, left, up, right, down): -------- >>> widget.pad_input(10, 10, 10, 10) """ - self.widget.grid(padx=(left, right), pady=(up, down)) + self.widget.grid_configure( + padx=(_resolve_pad_value(left), _resolve_pad_value(right)), + pady=(_resolve_pad_value(up), _resolve_pad_value(down)), + ) class WidgetInputAdapter: diff --git a/src/navigate/view/custom_widgets/common.py b/src/navigate/view/custom_widgets/common.py index 3b69790bf..bca67601d 100644 --- a/src/navigate/view/custom_widgets/common.py +++ b/src/navigate/view/custom_widgets/common.py @@ -29,12 +29,21 @@ # POSSIBILITY OF SUCH DAMAGE. # Standard Library Imports -from typing import Dict, Any +from __future__ import annotations +from typing import Dict, Any, Optional, Tuple, Union +import tkinter as tk from tkinter import ttk +# Local Imports +from navigate.view.theme import get_theme_padding, get_theme_spacing + # Third Party Imports -# Local Imports + +GridAxisOptions = Dict[str, int] +GridAxisSpec = Optional[Union[int, GridAxisOptions]] +GridConfig = Optional[Union[int, Tuple[GridAxisSpec, ...], Dict[int, GridAxisSpec]]] +SpacingArg = Optional[Union[int, str, Tuple[Union[int, str], ...]]] def uniform_grid(cls: Any) -> None: @@ -53,6 +62,85 @@ def uniform_grid(cls: Any) -> None: cls.grid_rowconfigure(row, weight=1) +def _resolve_spacing(value: SpacingArg) -> int | tuple[int, ...] | None: + """Resolve a spacing token or literal to a Tk-compatible padding value.""" + if value is None: + return None + if isinstance(value, str): + if value.startswith("padding_"): + return get_theme_padding(value) + return get_theme_spacing(value) + if isinstance(value, tuple): + resolved: list[int] = [] + for item in value: + if isinstance(item, str): + resolved.append(get_theme_spacing(item)) + else: + resolved.append(int(item)) + return tuple(resolved) + return int(value) + + +def themed_grid( + widget: Any, + *, + sticky: str | None = tk.NSEW, + padx: SpacingArg = None, + pady: SpacingArg = None, + **kwargs: Any, +) -> None: + """Grid a widget using theme token names for spacing.""" + grid_kwargs = dict(kwargs) + if sticky is not None: + grid_kwargs["sticky"] = sticky + + resolved_padx = _resolve_spacing(padx) + if resolved_padx is not None: + grid_kwargs["padx"] = resolved_padx + + resolved_pady = _resolve_spacing(pady) + if resolved_pady is not None: + grid_kwargs["pady"] = resolved_pady + + widget.grid(**grid_kwargs) + + +def _iter_grid_specs(specs: GridConfig) -> list[tuple[int, dict[str, int]]]: + """Normalize grid configuration input into Tk configure calls.""" + if specs is None: + return [] + if isinstance(specs, int): + return [(index, {"weight": 1}) for index in range(specs)] + + if isinstance(specs, dict): + items = specs.items() + else: + items = enumerate(specs) + + normalized: list[tuple[int, dict[str, int]]] = [] + for index, spec in items: + if spec is None: + continue + if isinstance(spec, int): + normalized.append((index, {"weight": spec})) + else: + normalized.append((index, dict(spec))) + return normalized + + +def configure_grid( + widget: Any, + *, + columns: GridConfig = None, + rows: GridConfig = None, +) -> None: + """Configure grid weights and minsizes with a compact declarative syntax.""" + for index, options in _iter_grid_specs(columns): + widget.grid_columnconfigure(index, **options) + for index, options in _iter_grid_specs(rows): + widget.grid_rowconfigure(index, **options) + + class CommonMethods: """This class is a collection of common methods for handling variables, widgets, and buttons. diff --git a/src/navigate/view/custom_widgets/hover.py b/src/navigate/view/custom_widgets/hover.py index 702fe8500..9a334d45a 100644 --- a/src/navigate/view/custom_widgets/hover.py +++ b/src/navigate/view/custom_widgets/hover.py @@ -38,6 +38,7 @@ # Third Party Imports # Local Imports +from navigate.view.theme import get_theme_space_px from navigate.view.theme import get_theme_color, get_theme_font # Logger Setup @@ -230,7 +231,7 @@ def showtip(self, text): borderwidth=1, font=font, ) - label.pack(ipadx=1) + label.pack(ipadx=get_theme_space_px(1)) def seterror(self, text): """Setter for the error message diff --git a/src/navigate/view/main_application_window.py b/src/navigate/view/main_application_window.py index 061a1b4c1..f363dac0f 100644 --- a/src/navigate/view/main_application_window.py +++ b/src/navigate/view/main_application_window.py @@ -44,7 +44,7 @@ from navigate.view.main_window_content.acquire_notebook import AcquireBar from navigate.view.main_window_content.menus import Menubar from navigate.view.custom_widgets.scrollbars import ScrolledFrame -from navigate.view.custom_widgets.common import uniform_grid +from navigate.view.custom_widgets.common import configure_grid, themed_grid, uniform_grid # Logger Setup p = __name__.split(".")[1] @@ -105,7 +105,7 @@ def __init__(self, root: tk.Tk, *args: Iterable, **kwargs: Dict[str, Any]) -> No #: ScrolledFrame: The scrollable version of the main frame for the application self.scroll_frame = ScrolledFrame(root) - self.scroll_frame.grid(row=0, column=0, sticky=tk.NSEW) + themed_grid(self.scroll_frame, row=0, column=0, sticky=tk.NSEW) ttk.Frame.__init__(self, self.scroll_frame.interior, *args, **kwargs) @@ -143,12 +143,32 @@ def __init__(self, root: tk.Tk, *args: Iterable, **kwargs: Dict[str, Any]) -> No self.right_frame = ttk.Frame(self) # Grid out foundational frames - self.grid(column=0, row=0, sticky=tk.NSEW) - self.top_frame.grid( - row=0, column=0, columnspan=2, sticky=tk.NSEW, padx=3, pady=3 + themed_grid(self, column=0, row=0, sticky=tk.NSEW) + themed_grid( + self.top_frame, + row=0, + column=0, + columnspan=2, + sticky=tk.NSEW, + padx="layout_window_gap", + pady=("layout_window_gap", "layout_panel_gap"), + ) + themed_grid( + self.left_frame, + row=1, + column=0, + sticky=tk.NSEW, + padx=("layout_window_gap", "layout_section_gap"), + pady=("layout_panel_gap", "layout_window_gap"), + ) + themed_grid( + self.right_frame, + row=1, + column=1, + sticky=tk.NSEW, + padx=("layout_section_gap", "layout_window_gap"), + pady=("layout_panel_gap", "layout_window_gap"), ) - self.left_frame.grid(row=1, column=0, rowspan=2, sticky=tk.NSEW, padx=3, pady=3) - self.right_frame.grid(row=1, column=1, sticky=tk.NSEW, padx=3, pady=3) #: SettingsNotebook: The settings notebook for the application self.settings = SettingsNotebook(self.left_frame, self.root) @@ -160,8 +180,5 @@ def __init__(self, root: tk.Tk, *args: Iterable, **kwargs: Dict[str, Any]) -> No self.acquire_bar = AcquireBar(self.top_frame, self.root) uniform_grid(self.scroll_frame.interior) - self.grid_rowconfigure(0, weight=0) - self.grid_columnconfigure(0, weight=0) - self.grid_rowconfigure(1, weight=1) - self.grid_columnconfigure(1, weight=1) + configure_grid(self, columns={0: 0, 1: 1}, rows={0: 0, 1: 1}) uniform_grid(self.right_frame) diff --git a/src/navigate/view/main_window_content/acquire_notebook.py b/src/navigate/view/main_window_content/acquire_notebook.py index 5ce28f7fd..83aa588b2 100644 --- a/src/navigate/view/main_window_content/acquire_notebook.py +++ b/src/navigate/view/main_window_content/acquire_notebook.py @@ -38,6 +38,7 @@ # Third Party Imports # Local Imports +from navigate.view.custom_widgets.common import configure_grid, themed_grid # Logger Setup p = __name__.split(".")[1] @@ -76,11 +77,14 @@ def __init__( ttk.Frame.__init__(self, top_frame, *args, **kwargs) # Formatting - tk.Grid.columnconfigure(self, "all", weight=1) - tk.Grid.rowconfigure(self, "all", weight=1) + configure_grid( + self, + columns={0: 0, 1: 0, 2: 1, 3: 0, 4: 0, 5: 0}, + rows={0: 1}, + ) # Putting bar into frame - self.grid(row=0, column=0) + themed_grid(self, row=0, column=0, sticky=tk.NSEW) # Acquire Button #: ttk.Button: Button to start acquisition @@ -115,9 +119,10 @@ def __init__( self, text=f"{0:02}" f":{0:02}" f":{0:02}" ) - self.CurAcq.grid(row=0, column=0) - self.OvrAcq.grid(row=1, column=0) - self.total_acquisition_label.grid(row=0, column=3, sticky=tk.NSEW) + themed_grid(self.CurAcq, row=0, column=0, sticky=tk.EW) + themed_grid(self.OvrAcq, row=1, column=0, sticky=tk.EW) + themed_grid(self.total_acquisition_label, row=0, column=3, sticky=tk.NSEW) + configure_grid(self.progBar_frame, columns={0: 1}, rows={0: 1, 1: 1}) #: ttk.Button: Button to exit the application self.exit_btn = ttk.Button(self, text="Exit") @@ -125,10 +130,43 @@ def __init__( #: ttk.Button: Button to stop the stage self.stop_stage = ttk.Button(self, text="Stop Stage") - self.acquire_btn.grid(row=0, column=0, sticky=tk.NSEW, pady=(2, 2), padx=(2, 2)) - self.pull_down.grid(row=0, column=1, sticky=tk.NSEW, pady=(2, 2), padx=(2, 2)) - self.progBar_frame.grid( - row=0, column=2, sticky=tk.NSEW, pady=(2, 2), padx=(2, 2) + themed_grid( + self.acquire_btn, + row=0, + column=0, + sticky=tk.NSEW, + padx="space_1", + pady="space_1", + ) + themed_grid( + self.pull_down, + row=0, + column=1, + sticky=tk.NSEW, + padx="space_1", + pady="space_1", + ) + themed_grid( + self.progBar_frame, + row=0, + column=2, + sticky=tk.NSEW, + padx=("space_1", "layout_control_gap"), + pady="space_1", + ) + themed_grid( + self.stop_stage, + row=0, + column=4, + sticky=tk.NSEW, + padx="space_1", + pady="space_1", + ) + themed_grid( + self.exit_btn, + row=0, + column=5, + sticky=tk.NSEW, + padx="space_1", + pady="space_1", ) - self.stop_stage.grid(row=0, column=4, sticky=tk.NSEW, pady=(2, 2), padx=(2, 2)) - self.exit_btn.grid(row=0, column=5, sticky=tk.NSEW, pady=(2, 2), padx=(2, 2)) diff --git a/src/navigate/view/main_window_content/camera_tab.py b/src/navigate/view/main_window_content/camera_tab.py index f6f61410f..b42899005 100644 --- a/src/navigate/view/main_window_content/camera_tab.py +++ b/src/navigate/view/main_window_content/camera_tab.py @@ -41,7 +41,7 @@ import navigate from navigate.view.custom_widgets.LabelInputWidgetFactory import LabelInput from navigate.view.custom_widgets.validation import ValidatedSpinbox, ValidatedEntry -from navigate.view.custom_widgets.common import uniform_grid +from navigate.view.custom_widgets.common import configure_grid, themed_grid # Logger Setup p = __name__.split(".")[1] @@ -81,22 +81,41 @@ def __init__( #: tk.Frame: The camera mode frame self.camera_mode = CameraMode(self) - self.camera_mode.grid(row=0, column=0, sticky=tk.NSEW, padx=10, pady=10) + themed_grid( + self.camera_mode, + row=0, + column=0, + sticky=tk.NSEW, + padx=("layout_panel_gap", "layout_section_gap"), + pady=("layout_panel_gap", "layout_section_gap"), + ) # Framerate Label Frame #: tk.Frame: The framerate label frame self.framerate_info = FramerateInfo(self) - self.framerate_info.grid(row=0, column=1, sticky=tk.NSEW, padx=10, pady=10) + themed_grid( + self.framerate_info, + row=0, + column=1, + sticky=tk.NSEW, + padx=("layout_section_gap", "layout_panel_gap"), + pady=("layout_panel_gap", "layout_section_gap"), + ) # Region of Interest Settings #: tk.Frame: The region of interest settings frame self.camera_roi = ROI(self) - self.camera_roi.grid( - row=1, column=0, columnspan=2, sticky=tk.NSEW, padx=10, pady=10 + themed_grid( + self.camera_roi, + row=1, + column=0, + columnspan=2, + sticky=tk.NSEW, + padx="layout_panel_gap", + pady=("layout_section_gap", "layout_panel_gap"), ) - # Uniform Grid - uniform_grid(self) + configure_grid(self, columns={0: 1, 1: 1}, rows={0: 1, 1: 2}) class CameraMode(ttk.Labelframe): @@ -139,13 +158,19 @@ def __init__(self, settings_tab: "CameraSettingsTab", *args, **kwargs) -> None: #: list: List of all the names for the widgets. self.names = ["Sensor", "Readout", "Pixels"] - tk.Grid.columnconfigure(self, "all", weight=1) - tk.Grid.rowconfigure(self, "all", weight=1) + configure_grid(self, columns={0: 0, 1: 1}, rows=len(self.labels)) # Dropdown loop for i in range(len(self.labels)): label = ttk.Label(self, text=self.labels[i]) - label.grid(row=i, column=0, pady=5, padx=5, sticky=tk.NW) + themed_grid( + label, + row=i, + column=0, + sticky=tk.NW, + padx="layout_control_gap", + pady="space_1", + ) if i < len(self.labels) - 1: self.inputs[self.names[i]] = LabelInput( @@ -161,8 +186,13 @@ def __init__(self, settings_tab: "CameraSettingsTab", *args, **kwargs) -> None: input_var=tk.StringVar(), input_args={"from_": 0, "to": 10000, "increment": 1, "width": 5}, ) - self.inputs[self.names[i]].grid( - row=i, column=1, pady=5, padx=5, sticky=tk.NW + themed_grid( + self.inputs[self.names[i]], + row=i, + column=1, + sticky=tk.NW, + padx="layout_control_gap", + pady="space_1", ) def get_variables(self) -> dict: @@ -245,8 +275,7 @@ def __init__( "max_framerate", ] - tk.Grid.columnconfigure(self, "all", weight=1) - tk.Grid.rowconfigure(self, "all", weight=1) + configure_grid(self, columns={0: 0, 1: 1}, rows=len(self.labels)) #: list: List of all the read only values for the widgets. self.read_only = [True, True, True] @@ -254,7 +283,14 @@ def __init__( # Dropdown loop for i in range(len(self.labels)): label = ttk.Label(self, text=self.labels[i]) - label.grid(row=i, column=0, pady=5, padx=5, sticky=tk.NW) + themed_grid( + label, + row=i, + column=0, + sticky=tk.NW, + padx="layout_control_gap", + pady="space_1", + ) if self.read_only[i]: self.inputs[self.names[i]] = LabelInput( @@ -271,8 +307,13 @@ def __init__( input_var=tk.DoubleVar(), input_args={"from_": 1, "to": 1000, "increment": 1.0, "width": 6}, ) - self.inputs[self.names[i]].grid( - row=i, column=1, pady=5, padx=5, sticky=tk.NW + themed_grid( + self.inputs[self.names[i]], + row=i, + column=1, + sticky=tk.NW, + padx="layout_control_gap", + pady="space_1", ) def get_variables(self) -> dict: @@ -337,41 +378,65 @@ def __init__( text_label = "Region of Interest Settings" ttk.Labelframe.__init__(self, settings_tab, text=text_label, *args, **kwargs) - # Formatting - tk.Grid.columnconfigure(self, "all", weight=1) - tk.Grid.rowconfigure(self, "all", weight=1) + configure_grid(self, columns={0: 1, 1: 1}, rows={0: 1, 1: 1}) # Parent Label Frames for widgets #: ttk.LabelFrame: The parent frame for any the camera size. self.roi_frame = ttk.LabelFrame(self, text="Number of Pixels") - self.roi_frame.grid(row=0, column=0, sticky=tk.NSEW, padx=10, pady=10) + themed_grid( + self.roi_frame, + row=0, + column=0, + sticky=tk.NSEW, + padx=("layout_panel_gap", "layout_section_gap"), + pady=("layout_panel_gap", "layout_section_gap"), + ) # Button Frame #: ttk.LabelFrame: The parent frame for default FOV options. self.btn_frame = ttk.LabelFrame(self, text="Default FOVs") - self.btn_frame.grid(row=0, column=1, sticky=tk.NSEW, padx=(40, 10), pady=10) + themed_grid( + self.btn_frame, + row=0, + column=1, + sticky=tk.NSEW, + padx=("layout_section_gap", "layout_panel_gap"), + pady=("layout_panel_gap", "layout_section_gap"), + ) # FOV #: ttk.LabelFrame: The parent frame for the FOV size. self.fov_frame = ttk.LabelFrame(self, text="FOV Dimensions (microns)") - self.fov_frame.grid(row=1, column=0, sticky=tk.NSEW, padx=10, pady=10) + themed_grid( + self.fov_frame, + row=1, + column=0, + sticky=tk.NSEW, + padx=("layout_panel_gap", "layout_section_gap"), + pady=("layout_section_gap", "layout_panel_gap"), + ) # ROI boundary #: ttk.LabelFrame: The parent frame for the boundary of the FOV. self.roi_boundary_frame = ttk.LabelFrame(self, text="ROI Boundary") - self.roi_boundary_frame.grid( - row=1, column=1, sticky=tk.NSEW, padx=(40, 10), pady=10 + themed_grid( + self.roi_boundary_frame, + row=1, + column=1, + sticky=tk.NSEW, + padx=("layout_section_gap", "layout_panel_gap"), + pady=("layout_section_gap", "layout_panel_gap"), ) # Formatting - tk.Grid.columnconfigure(self.roi_frame, "all", weight=1) - tk.Grid.rowconfigure(self.roi_frame, "all", weight=1) - tk.Grid.columnconfigure(self.btn_frame, "all", weight=1) - tk.Grid.rowconfigure(self.btn_frame, "all", weight=1) - tk.Grid.columnconfigure(self.fov_frame, "all", weight=1) - tk.Grid.rowconfigure(self.fov_frame, "all", weight=1) - tk.Grid.columnconfigure(self.roi_boundary_frame, "all", weight=1) - tk.Grid.rowconfigure(self.roi_boundary_frame, "all", weight=1) + configure_grid(self.roi_frame, columns={0: 1}, rows={0: 1, 1: 1, 2: 1, 3: 1}) + configure_grid(self.btn_frame, columns={0: 1}, rows=4) + configure_grid(self.fov_frame, columns={0: 1}, rows={0: 1, 1: 1}) + configure_grid( + self.roi_boundary_frame, + columns={0: 0, 1: 1, 2: 1}, + rows={0: 0, 1: 1, 2: 1}, + ) #: dict: Dictionary of all the widgets in the frame. self.inputs = {} @@ -401,7 +466,14 @@ def __init__( self.buttons[btn_names[i]] = ttk.Button( self.btn_frame, text=btn_labels[i], width=9 ) - self.buttons[btn_names[i]].grid(row=i, column=0, pady=5, padx=35) + themed_grid( + self.buttons[btn_names[i]], + row=i, + column=0, + sticky=tk.NSEW, + padx="layout_control_gap", + pady="space_1", + ) for i in range(2): # Num Pix frame @@ -412,7 +484,13 @@ def __init__( input_var=tk.IntVar(), input_args={"from_": 0, "increment": 2.0, "width": 5}, ) - self.inputs[roi_labels[i]].grid(row=i, column=0, pady=5, padx=5) + themed_grid( + self.inputs[roi_labels[i]], + row=i, + column=0, + padx="layout_control_gap", + pady="space_1", + ) # FOV Frame self.inputs[fov_names[i]] = LabelInput( @@ -422,7 +500,13 @@ def __init__( input_var=tk.IntVar(), input_args={"width": 7, "required": True}, ) - self.inputs[fov_names[i]].grid(row=i, column=0, pady=1, padx=5) + themed_grid( + self.inputs[fov_names[i]], + row=i, + column=0, + padx="layout_control_gap", + pady="space_1", + ) # ROI boundary self.inputs["is_centered"] = LabelInput( @@ -431,13 +515,32 @@ def __init__( input_class=ttk.Checkbutton, input_var=tk.BooleanVar(), ) - self.inputs["is_centered"].grid( - row=0, columnspan=3, padx=(10, 0), pady=(10, 5), sticky=tk.NW + themed_grid( + self.inputs["is_centered"], + row=0, + columnspan=3, + sticky=tk.NW, + padx=("layout_control_gap", 0), + pady=("layout_control_gap", "space_1"), ) top_label = ttk.Label(self.roi_boundary_frame, text="Top-Left:") bottom_label = ttk.Label(self.roi_boundary_frame, text="Bottom-Right:") - top_label.grid(row=1, column=0, padx=10, pady=1, sticky=tk.NW) - bottom_label.grid(row=2, column=0, padx=10, pady=1, sticky=tk.NW) + themed_grid( + top_label, + row=1, + column=0, + sticky=tk.NW, + padx="layout_control_gap", + pady="space_1", + ) + themed_grid( + bottom_label, + row=2, + column=0, + sticky=tk.NW, + padx="layout_control_gap", + pady="space_1", + ) for i in range(len(roi_boundary_names)): self.inputs[roi_boundary_names[i]] = LabelInput( parent=self.roi_boundary_frame, @@ -447,8 +550,13 @@ def __init__( input_var=tk.IntVar(), input_args={"from_": 0, "to": 2048, "increment": 1.0, "width": 6}, ) - self.inputs[roi_boundary_names[i]].grid( - row=i // 2 + 1, column=i % 2 + 1, pady=1, padx=5, sticky=tk.NW + themed_grid( + self.inputs[roi_boundary_names[i]], + row=i // 2 + 1, + column=i % 2 + 1, + sticky=tk.NW, + padx="layout_control_gap", + pady="space_1", ) self.inputs[roi_boundary_names[i]].label.grid(padx=(0, 10), sticky=tk.NW) @@ -460,7 +568,13 @@ def __init__( input_var=tk.StringVar(), input_args={"width": 5}, ) - self.inputs[self.binning].grid(row=3, column=0, pady=5, padx=5) + themed_grid( + self.inputs[self.binning], + row=3, + column=0, + padx="layout_control_gap", + pady="space_1", + ) # Number of Pixels self.inputs["Width"].grid(pady=(10, 5)) diff --git a/src/navigate/view/main_window_content/channels_tab.py b/src/navigate/view/main_window_content/channels_tab.py index c2bd42f4e..b783cc290 100644 --- a/src/navigate/view/main_window_content/channels_tab.py +++ b/src/navigate/view/main_window_content/channels_tab.py @@ -43,7 +43,12 @@ from navigate.view.custom_widgets.hover import HoverButton from navigate.view.custom_widgets.validation import ValidatedSpinbox, ValidatedCombobox from navigate.view.custom_widgets.LabelInputWidgetFactory import LabelInput -from navigate.view.custom_widgets.common import uniform_grid +from navigate.view.custom_widgets.common import configure_grid, themed_grid, uniform_grid +from navigate.view.theme import ( + get_theme_padding_px, + get_theme_spacing, + get_theme_space_px, +) import navigate # Logger Setup @@ -83,32 +88,68 @@ def __init__( #: ChannelCreator: The frame that holds the channel settings self.channel_widgets_frame = ChannelCreator(self) - self.channel_widgets_frame.grid( - row=0, column=0, columnspan=3, sticky=tk.NSEW, padx=10, pady=10 + themed_grid( + self.channel_widgets_frame, + row=0, + column=0, + columnspan=3, + sticky=tk.NSEW, + padx="layout_panel_gap", + pady=("layout_panel_gap", "layout_section_gap"), ) #: StackAcquisitionFrame: The frame that holds the stack acquisition settings self.stack_acq_frame = StackAcquisitionFrame(self) - self.stack_acq_frame.grid( - row=1, column=0, columnspan=3, sticky=tk.NSEW, padx=10, pady=10 + themed_grid( + self.stack_acq_frame, + row=1, + column=0, + columnspan=3, + sticky=tk.NSEW, + padx="layout_panel_gap", + pady="layout_section_gap", ) #: StackTimePointFrame: The frame that holds the time settings self.stack_timepoint_frame = StackTimePointFrame(self) - self.stack_timepoint_frame.grid( - row=3, column=0, columnspan=3, sticky=tk.NSEW, padx=10, pady=10 + themed_grid( + self.stack_timepoint_frame, + row=2, + column=0, + columnspan=3, + sticky=tk.NSEW, + padx="layout_panel_gap", + pady="layout_section_gap", ) #: MultiPointFrame: The frame that holds the multipoint settings self.multipoint_frame = MultiPointFrame(self) - self.multipoint_frame.grid( - row=4, column=0, columnspan=1, sticky=tk.NSEW, padx=10, pady=10 + themed_grid( + self.multipoint_frame, + row=3, + column=0, + columnspan=1, + sticky=tk.NSEW, + padx=("layout_panel_gap", "layout_section_gap"), + pady=("layout_section_gap", "layout_panel_gap"), ) #: QuickLaunchFrame: The frame that holds the quick launch buttons self.quick_launch = QuickLaunchFrame(self) - self.quick_launch.grid( - row=4, column=1, columnspan=2, sticky=tk.NSEW, padx=10, pady=10 + themed_grid( + self.quick_launch, + row=3, + column=1, + columnspan=2, + sticky=tk.NSEW, + padx=("layout_section_gap", "layout_panel_gap"), + pady=("layout_section_gap", "layout_panel_gap"), + ) + + configure_grid( + self, + columns={0: 1, 1: 1, 2: 1}, + rows={0: 3, 1: 2, 2: 1, 3: 1}, ) @@ -140,10 +181,10 @@ def __init__( ttk.Labelframe.__init__(self, channels_tab, text=self.title, *args, **kwargs) #: int: The default padding for widgets in the x direction - self.pad_x = 1 + self.pad_x = get_theme_spacing("space_1") #: int: The default padding for widgets in the y direction - self.pad_y = 1 + self.pad_y = get_theme_spacing("space_1") #: list: List of the variables for the channel check buttons self.channel_variables = [] @@ -203,6 +244,7 @@ def __init__( #: list: List of the frames for the columns self.frame_columns = [] + configure_grid(self, columns={0: 1}) def populate_frame( self, channels: int, filter_wheels: int, filter_wheel_names: list @@ -227,10 +269,11 @@ def populate_frame( self.create_labels(filter_wheel_names, filter_wheels) # Configure the columns for consistent spacing - for i in range(len(self.label_text)): - self.columnconfigure(i, weight=1) - for i in range(channels): - self.rowconfigure(i, weight=1, uniform="1") + configure_grid( + self, + columns={i: 1 for i in range(len(self.label_text))}, + rows={i + 1: {"weight": 1, "uniform": "1"} for i in range(channels)}, + ) # Creates the widgets for each channel - populates the rows. for num in range(0, channels): @@ -410,8 +453,14 @@ def __init__(self, settings_tab: ChannelsTab, *args: list, **kwargs: dict) -> No self.stack_frame = ttk.Frame(self) self.additional_stack_frame = ttk.Frame(self) - self.stack_frame.grid(row=0, column=0, sticky=tk.NSEW) - self.additional_stack_frame.grid(row=1, column=0, sticky=tk.NSEW) + themed_grid( + self.stack_frame, + row=0, + column=0, + sticky=tk.NSEW, + pady=(0, "layout_section_gap"), + ) + themed_grid(self.additional_stack_frame, row=1, column=0, sticky=tk.NSEW) self.rowconfigure(0, weight=1) self.rowconfigure(1, weight=1) @@ -433,7 +482,11 @@ def __init__(self, settings_tab: ChannelsTab, *args: list, **kwargs: dict) -> No input_args={"width": 6}, ) self.inputs[start_names[i]].grid( - row=i + 1, column=0, sticky="N", pady=2, padx=(6, 0) + row=i + 1, + column=0, + sticky="N", + pady=get_theme_space_px(2), + padx=get_theme_padding_px((6, 0)), ) self.inputs[start_names[i]].label.grid(sticky="N") @@ -441,7 +494,13 @@ def __init__(self, settings_tab: ChannelsTab, *args: list, **kwargs: dict) -> No self.buttons["set_start"] = HoverButton( self.stack_frame, text="Set Start Pos/Foc" ) - self.buttons["set_start"].grid(row=3, column=0, sticky="N", pady=2, padx=(6, 0)) + self.buttons["set_start"].grid( + row=3, + column=0, + sticky="N", + pady=get_theme_space_px(2), + padx=get_theme_padding_px((6, 0)), + ) # End Pos Frame (Vertically Oriented) end_names = ["end_position", "end_focus"] @@ -459,13 +518,23 @@ def __init__(self, settings_tab: ChannelsTab, *args: list, **kwargs: dict) -> No input_args={"width": 6}, ) self.inputs[end_names[i]].grid( - row=i + 1, column=1, sticky="N", pady=2, padx=(6, 0) + row=i + 1, + column=1, + sticky="N", + pady=get_theme_space_px(2), + padx=get_theme_padding_px((6, 0)), ) self.inputs[end_names[i]].label.grid(sticky="N") # End Button self.buttons["set_end"] = HoverButton(self.stack_frame, text="Set End Pos/Foc") - self.buttons["set_end"].grid(row=3, column=1, sticky="N", pady=2, padx=(6, 0)) + self.buttons["set_end"].grid( + row=3, + column=1, + sticky="N", + pady=get_theme_space_px(2), + padx=get_theme_padding_px((6, 0)), + ) #: ttk.Label: The label for the step size step_size_label = ttk.Label(self.stack_frame, text="Step Size") @@ -476,7 +545,12 @@ def __init__(self, settings_tab: ChannelsTab, *args: list, **kwargs: dict) -> No input_var=tk.DoubleVar(), input_args={"width": 6}, ) - self.inputs["step_size"].grid(row=1, column=2, sticky="N", padx=6) + self.inputs["step_size"].grid( + row=1, + column=2, + sticky="N", + padx=get_theme_space_px(6), + ) # Slice Frame (Vertically oriented) #: ttk.Label: The label to add empty space to the slice frame @@ -491,7 +565,11 @@ def __init__(self, settings_tab: ChannelsTab, *args: list, **kwargs: dict) -> No ) self.inputs["number_z_steps"].widget.configure(state="disabled") self.inputs["number_z_steps"].grid( - row=1, column=3, sticky="NSEW", pady=2, padx=(6, 0) + row=1, + column=3, + sticky="NSEW", + pady=get_theme_space_px(2), + padx=get_theme_padding_px((6, 0)), ) # devices @@ -504,7 +582,12 @@ def __init__(self, settings_tab: ChannelsTab, *args: list, **kwargs: dict) -> No ) self.inputs["z_device"].state(["disabled", "readonly"]) self.inputs["z_device"].grid( - row=4, column=0, columnspan=2, sticky="NSEW", padx=6, pady=5 + row=4, + column=0, + columnspan=2, + sticky="NSEW", + padx=get_theme_space_px(6), + pady=get_theme_space_px(5), ) self.inputs["f_device"] = LabelInput( @@ -516,7 +599,12 @@ def __init__(self, settings_tab: ChannelsTab, *args: list, **kwargs: dict) -> No ) self.inputs["f_device"].state(["disabled", "readonly"]) self.inputs["f_device"].grid( - row=5, column=0, columnspan=2, sticky="NSEW", padx=6, pady=5 + row=5, + column=0, + columnspan=2, + sticky="NSEW", + padx=get_theme_space_px(6), + pady=get_theme_space_px(5), ) # Laser Cycling Settings @@ -529,7 +617,12 @@ def __init__(self, settings_tab: ChannelsTab, *args: list, **kwargs: dict) -> No ) self.inputs["cycling"].state(["readonly"]) self.inputs["cycling"].grid( - row=6, column=0, columnspan=2, sticky="NSEW", padx=6, pady=5 + row=6, + column=0, + columnspan=2, + sticky="NSEW", + padx=get_theme_space_px(6), + pady=get_theme_space_px(5), ) self.inputs["speed"] = LabelInput( @@ -541,7 +634,12 @@ def __init__(self, settings_tab: ChannelsTab, *args: list, **kwargs: dict) -> No ) self.inputs["speed"].state(["disabled", "readonly"]) self.inputs["speed"].grid( - row=7, column=0, columnspan=2, sticky="NSEW", padx=5, pady=5 + row=7, + column=0, + columnspan=2, + sticky="NSEW", + padx=get_theme_space_px(5), + pady=get_theme_space_px(5), ) self.cubic_frame = ttk.Frame(self.stack_frame) @@ -551,14 +649,15 @@ def __init__(self, settings_tab: ChannelsTab, *args: list, **kwargs: dict) -> No column=2, columnspan=2, sticky=tk.NE, - padx=(5, 15), - pady=(5, 0), + padx=get_theme_padding_px((5, 15)), + pady=get_theme_padding_px((5, 0)), ) image_directory = Path(__file__).resolve().parent self.image = tk.PhotoImage( - file=image_directory.joinpath("images", "cubic_bottom_to_top.png") + master=self, + file=image_directory.joinpath("images", "cubic_bottom_to_top.png"), ) # Use ttk.Label @@ -569,8 +668,8 @@ def __init__(self, settings_tab: ChannelsTab, *args: list, **kwargs: dict) -> No column=0, columnspan=2, sticky=tk.NSEW, - padx=(5, 0), - pady=(5, 0), + padx=get_theme_padding_px((5, 0)), + pady=get_theme_padding_px((5, 0)), ) self.inputs["top"] = LabelInput( @@ -580,7 +679,13 @@ def __init__(self, settings_tab: ChannelsTab, *args: list, **kwargs: dict) -> No input_var=tk.DoubleVar(), input_args={"width": 6}, ) - self.inputs["top"].grid(row=0, column=2, sticky=tk.EW, padx=0, pady=(15, 0)) + self.inputs["top"].grid( + row=0, + column=2, + sticky=tk.EW, + padx=get_theme_space_px(0), + pady=get_theme_padding_px((15, 0)), + ) self.inputs["top"].widget.configure(state="disabled") self.inputs["bottom"] = LabelInput( @@ -590,7 +695,13 @@ def __init__(self, settings_tab: ChannelsTab, *args: list, **kwargs: dict) -> No input_var=tk.DoubleVar(), input_args={"width": 6}, ) - self.inputs["bottom"].grid(row=1, column=2, sticky=tk.EW, padx=0, pady=(10, 0)) + self.inputs["bottom"].grid( + row=1, + column=2, + sticky=tk.EW, + padx=get_theme_space_px(0), + pady=get_theme_padding_px((10, 0)), + ) self.inputs["bottom"].widget.configure(state="disabled") self.inputs["z_offset"] = LabelInput( @@ -602,10 +713,16 @@ def __init__(self, settings_tab: ChannelsTab, *args: list, **kwargs: dict) -> No ) self.inputs["z_offset"].widget.configure(state="disabled") self.inputs["z_offset"].grid( - row=0, column=0, columnspan=2, sticky="NSEW", padx=6, pady=5 + row=0, + column=0, + columnspan=2, + sticky="NSEW", + padx=get_theme_space_px(6), + pady=get_theme_space_px(5), ) uniform_grid(self) + uniform_grid(self.stack_frame) # Initialize DescriptionHovers self.inputs["step_size"].widget.hover.setdescription("The Z-stack step size.") @@ -676,11 +793,23 @@ def create_additional_stack_widgets(self, axes: list, devices: dict) -> None: # Create the additional stack widgets here separator = ttk.Separator(self.additional_stack_frame, orient=tk.HORIZONTAL) - separator.grid(row=0, column=0, columnspan=10, sticky=tk.NSEW, pady=(5, 0)) + separator.grid( + row=0, + column=0, + columnspan=10, + sticky=tk.NSEW, + pady=get_theme_padding_px((5, 0)), + ) # Stacking on axes label = ttk.Label(self.additional_stack_frame, text="Stacking on axes:") - label.grid(row=1, column=0, sticky=tk.NSEW, padx=(5, 30), pady=(5, 0)) + label.grid( + row=1, + column=0, + sticky=tk.NSEW, + padx=get_theme_padding_px((5, 30)), + pady=get_theme_padding_px((5, 0)), + ) for axis in axes: self.additional_stack_setting_variables[f"stack_{axis}"] = tk.BooleanVar() @@ -694,13 +823,18 @@ def create_additional_stack_widgets(self, axes: list, devices: dict) -> None: row=1, column=axes.index(axis) + 1, sticky=tk.NW, - padx=(5, 10), - pady=(5, 0), + padx=get_theme_padding_px((5, 10)), + pady=get_theme_padding_px((5, 0)), ) self.additional_stack_setting_frame = ttk.Frame(self.additional_stack_frame) self.additional_stack_setting_frame.grid( - row=2, column=0, columnspan=10, sticky=tk.NSEW, padx=(5, 30), pady=(5, 0) + row=2, + column=0, + columnspan=10, + sticky=tk.NSEW, + padx=get_theme_padding_px((5, 30)), + pady=get_theme_padding_px((5, 0)), ) self.additional_stack_setting_labels = {} @@ -708,17 +842,35 @@ def create_additional_stack_widgets(self, axes: list, devices: dict) -> None: ["Axis", "Device", "Offset (" + "\N{GREEK SMALL LETTER MU}" + "m)"] ): # , "Step", "Slice Num"]): label = ttk.Label(self.additional_stack_setting_frame, text=label_text) - label.grid(row=0, column=i, sticky=tk.NSEW, padx=10, pady=2) + label.grid( + row=0, + column=i, + sticky=tk.NSEW, + padx=get_theme_space_px(10), + pady=get_theme_space_px(2), + ) for i, axis in enumerate(axes): label = ttk.Label(self.additional_stack_setting_frame, text=axis.upper()) - label.grid(row=i + 1, column=0, sticky=tk.NSEW, padx=10, pady=2) + label.grid( + row=i + 1, + column=0, + sticky=tk.NSEW, + padx=get_theme_space_px(10), + pady=get_theme_space_px(2), + ) label.grid_remove() self.additional_stack_setting_labels[axis] = label # Create the device label label = ttk.Label( self.additional_stack_setting_frame, text=self.devices_dict[axis] ) - label.grid(row=i + 1, column=1, sticky=tk.NSEW, padx=10, pady=2) + label.grid( + row=i + 1, + column=1, + sticky=tk.NSEW, + padx=get_theme_space_px(10), + pady=get_theme_space_px(2), + ) label.grid_remove() self.additional_stack_setting_labels[f"{axis}_device"] = label # Create the offset spinbox @@ -731,7 +883,11 @@ def create_additional_stack_widgets(self, axes: list, devices: dict) -> None: textvariable=self.additional_stack_setting_variables[index_name], ) self.inputs[index_name].grid( - row=i + 1, column=2, sticky=tk.NSEW, padx=10, pady=2 + row=i + 1, + column=2, + sticky=tk.NSEW, + padx=get_theme_space_px(10), + pady=get_theme_space_px(2), ) self.inputs[index_name].grid_remove() @@ -848,7 +1004,13 @@ def __init__(self, settings_tab: ChannelsTab, *args: list, **kwargs: dict) -> No #: ttk.Label: The label for the save data checkbox self.laser_label = ttk.Label(self, text="Save Data") - self.laser_label.grid(row=0, column=0, sticky=tk.NSEW, padx=(4, 5), pady=(4, 0)) + self.laser_label.grid( + row=0, + column=0, + sticky=tk.NSEW, + padx=get_theme_padding_px((4, 5)), + pady=get_theme_padding_px((4, 0)), + ) #: tk.BooleanVar: The variable for the save data checkbox self.save_data = tk.BooleanVar() @@ -856,13 +1018,22 @@ def __init__(self, settings_tab: ChannelsTab, *args: list, **kwargs: dict) -> No #: ttk.Checkbutton: The save data checkbox self.save_check = ttk.Checkbutton(self, text="", variable=self.save_data) - self.save_check.grid(row=0, column=1, sticky=tk.NSEW, pady=(4, 0)) + self.save_check.grid( + row=0, + column=1, + sticky=tk.NSEW, + pady=get_theme_padding_px((4, 0)), + ) self.inputs["save_check"] = self.save_check #: ttk.Label: The label for the timepoints spinbox self.filterwheel_label = ttk.Label(self, text="Timepoints") self.filterwheel_label.grid( - row=1, column=0, sticky=tk.NSEW, padx=(4, 5), pady=2 + row=1, + column=0, + sticky=tk.NSEW, + padx=get_theme_padding_px((4, 5)), + pady=get_theme_space_px(2), ) #: tk.StringVar: The variable for the timepoints spinbox @@ -872,12 +1043,23 @@ def __init__(self, settings_tab: ChannelsTab, *args: list, **kwargs: dict) -> No self.exp_time_spinbox = ValidatedSpinbox( self, textvariable=self.exp_time_spinval, width=3 ) - self.exp_time_spinbox.grid(row=1, column=1, sticky=tk.NSEW, pady=2) + self.exp_time_spinbox.grid( + row=1, + column=1, + sticky=tk.NSEW, + pady=get_theme_space_px(2), + ) self.inputs["time_spin"] = self.exp_time_spinbox #: ttk.Label: The label for the stack acquisition time spinbox self.exp_time_label = ttk.Label(self, text="Stack Acq. Time") - self.exp_time_label.grid(row=2, column=0, sticky=tk.NSEW, padx=(4, 5), pady=2) + self.exp_time_label.grid( + row=2, + column=0, + sticky=tk.NSEW, + padx=get_theme_padding_px((4, 5)), + pady=get_theme_space_px(2), + ) #: tk.StringVar: The variable for the stack acquisition time spinbox self.stack_acq_spinval = tk.StringVar() @@ -886,12 +1068,23 @@ def __init__(self, settings_tab: ChannelsTab, *args: list, **kwargs: dict) -> No self.stack_acq_spinbox = ttk.Spinbox( self, textvariable=self.stack_acq_spinval, width=6 ) - self.stack_acq_spinbox.grid(row=2, column=1, sticky=tk.NSEW, pady=2) + self.stack_acq_spinbox.grid( + row=2, + column=1, + sticky=tk.NSEW, + pady=get_theme_space_px(2), + ) self.stack_acq_spinbox.state(["disabled"]) #: ttk.Label: The label for the stack pause spinbox self.exp_time_label = ttk.Label(self, text="Stack Pause (s)") - self.exp_time_label.grid(row=0, column=2, sticky=tk.NSEW, padx=(4, 5), pady=2) + self.exp_time_label.grid( + row=0, + column=2, + sticky=tk.NSEW, + padx=get_theme_padding_px((4, 5)), + pady=get_theme_space_px(2), + ) #: tk.StringVar: The variable for the stack pause spinbox self.stack_pause_spinval = tk.StringVar() @@ -900,12 +1093,23 @@ def __init__(self, settings_tab: ChannelsTab, *args: list, **kwargs: dict) -> No self.stack_pause_spinbox = ValidatedSpinbox( self, textvariable=self.stack_pause_spinval, width=6 ) - self.stack_pause_spinbox.grid(row=0, column=3, sticky=tk.NSEW, pady=2) + self.stack_pause_spinbox.grid( + row=0, + column=3, + sticky=tk.NSEW, + pady=get_theme_space_px(2), + ) self.inputs["stack_pause"] = self.stack_pause_spinbox #: ttk.Label: The label for the time point interval spinbox self.exp_time_label = ttk.Label(self, text="Time Interval (hh:mm:ss)") - self.exp_time_label.grid(row=1, column=2, sticky=tk.NSEW, padx=(4, 5), pady=2) + self.exp_time_label.grid( + row=1, + column=2, + sticky=tk.NSEW, + padx=get_theme_padding_px((4, 5)), + pady=get_theme_space_px(2), + ) #: tk.StringVar: The variable for the time point interval spinbox self.timepoint_interval_spinval = tk.StringVar() @@ -916,13 +1120,22 @@ def __init__(self, settings_tab: ChannelsTab, *args: list, **kwargs: dict) -> No self.timepoint_interval_spinbox = ttk.Spinbox( self, textvariable=self.timepoint_interval_spinval, width=6 ) - self.timepoint_interval_spinbox.grid(row=1, column=3, sticky=tk.NSEW, pady=2) + self.timepoint_interval_spinbox.grid( + row=1, + column=3, + sticky=tk.NSEW, + pady=get_theme_space_px(2), + ) self.timepoint_interval_spinbox.state(["disabled"]) # Starts it disabled #: ttk.Label: The label for the total time spinbox self.exp_time_label = ttk.Label(self, text="Experiment Duration (hh:mm:ss)") self.exp_time_label.grid( - row=2, column=2, sticky=tk.NSEW, padx=(4, 5), pady=(2, 6) + row=2, + column=2, + sticky=tk.NSEW, + padx=get_theme_padding_px((4, 5)), + pady=get_theme_padding_px((2, 6)), ) #: tk.StringVar: The variable for the total time spinbox @@ -934,8 +1147,14 @@ def __init__(self, settings_tab: ChannelsTab, *args: list, **kwargs: dict) -> No self.total_time_spinval = ttk.Spinbox( self, textvariable=self.total_time_spinval, width=6 ) - self.total_time_spinval.grid(row=2, column=3, sticky=tk.NSEW, pady=(2, 6)) + self.total_time_spinval.grid( + row=2, + column=3, + sticky=tk.NSEW, + pady=get_theme_padding_px((2, 6)), + ) self.total_time_spinval.state(["disabled"]) + configure_grid(self, columns={0: 0, 1: 1, 2: 0, 3: 1}, rows={0: 1, 1: 1, 2: 1}) def get_variables(self) -> dict: """Returns a dictionary of all the variables that are tied to each widget name. @@ -985,20 +1204,36 @@ def __init__(self, settings_tab: ChannelsTab, *args: list, **kwargs: dict) -> No #: ttk.Label: The label for the save data checkbox self.laser_label = ttk.Label(self, text="Enable") - self.laser_label.grid(row=0, column=0, sticky=tk.NSEW, padx=(4, 4), pady=(4, 4)) + self.laser_label.grid( + row=0, + column=0, + sticky=tk.NSEW, + padx=get_theme_padding_px((4, 4)), + pady=get_theme_padding_px((4, 4)), + ) #: tk.BooleanVar: The variable for the save data checkbox self.on_off = tk.BooleanVar() #: ttk.Checkbutton: The save data checkbox self.save_check = ttk.Checkbutton(self, text="", variable=self.on_off) - self.save_check.grid(row=0, column=1, sticky=tk.NSEW, pady=(4, 4)) + self.save_check.grid( + row=0, + column=1, + sticky=tk.NSEW, + pady=get_theme_padding_px((4, 4)), + ) #: dict: Dictionary of the buttons in the frame self.buttons = {"tiling": ttk.Button(self, text="Launch Tiling Wizard")} self.buttons["tiling"].grid( - row=0, column=2, sticky=tk.NSEW, padx=(10, 0), pady=(4, 4) + row=0, + column=2, + sticky=tk.NSEW, + padx=get_theme_padding_px((10, 0)), + pady=get_theme_padding_px((4, 4)), ) + configure_grid(self, columns={0: 0, 1: 0, 2: 1}, rows={0: 1}) class QuickLaunchFrame(ttk.Labelframe): @@ -1028,9 +1263,18 @@ def __init__(self, settings_tab: ChannelsTab, *args: list, **kwargs: dict) -> No "waveform_parameters": ttk.Button(self, text="Waveform Parameters") } self.buttons["waveform_parameters"].grid( - row=0, column=2, sticky=tk.NSEW, padx=(4, 4), pady=(4, 4) + row=0, + column=2, + sticky=tk.NSEW, + padx=get_theme_padding_px((4, 4)), + pady=get_theme_padding_px((4, 4)), ) self.buttons["autofocus_button"] = ttk.Button(self, text="Autofocus Settings") self.buttons["autofocus_button"].grid( - row=1, column=2, sticky=tk.NSEW, padx=(4, 4), pady=(4, 4) + row=1, + column=2, + sticky=tk.NSEW, + padx=get_theme_padding_px((4, 4)), + pady=get_theme_padding_px((4, 4)), ) + configure_grid(self, columns={0: 1, 1: 1, 2: 1}, rows={0: 1, 1: 1}) diff --git a/src/navigate/view/main_window_content/display_notebook.py b/src/navigate/view/main_window_content/display_notebook.py index bb255e654..f1d058ea7 100644 --- a/src/navigate/view/main_window_content/display_notebook.py +++ b/src/navigate/view/main_window_content/display_notebook.py @@ -47,20 +47,23 @@ WidgetInputAdapter, ) from navigate.view.custom_widgets.validation import ValidatedSpinbox -from navigate.view.custom_widgets.common import CommonMethods, uniform_grid -from navigate.view.theme import get_theme_font, get_theme_spacing +from navigate.view.custom_widgets.common import ( + CommonMethods, + configure_grid, + themed_grid, +) +from navigate.view.theme import ( + get_theme_color, + get_theme_font, + get_theme_padding, + get_theme_spacing, +) # Logger Setup p = __name__.split(".")[1] logger = logging.getLogger(p) -def _space(px: int) -> int: - """Resolve spacing through the active GUI theme token map.""" - normalized_px = max(0, int(px)) - return get_theme_spacing(f"space_{normalized_px}", normalized_px) - - class CameraNotebook(DockableNotebook): """This class is the notebook that holds the camera view and waveform settings tabs.""" @@ -83,7 +86,7 @@ def __init__( DockableNotebook.__init__(self, frame, *args, **kwargs) # Putting notebook 2 into top right frame - self.grid(row=0, column=0, sticky=tk.NSEW) + themed_grid(self, row=0, column=0, sticky=tk.NSEW) #: CameraTab: The camera tab. self.camera_tab = CameraTab(self) @@ -101,7 +104,7 @@ def __init__( self.add(self.mip_tab, text="MIP", sticky=tk.NSEW) self.add(self.waveform_tab, text="Waveforms", sticky=tk.NSEW) - uniform_grid(self) + configure_grid(self, columns={0: 1}, rows={0: 1}) class MIPTab(tk.Frame): @@ -122,6 +125,7 @@ def __init__( Arbitrary keyword arguments. """ # Init Frame + kwargs.setdefault("bg", get_theme_color("panel_bg")) tk.Frame.__init__(self, cam_wave, *args, **kwargs) #: int: The index of the tab. @@ -130,27 +134,43 @@ def __init__( #: Bool: The docked flag. self.is_docked = True + canvas_min_size = get_theme_spacing("layout_canvas_min_size") + sidebar_min_width = get_theme_spacing("layout_sidebar_min_width") + #: ttk.Frame: The frame that will hold the camera image. - self.cam_image = ttk.Frame(self) - self.cam_image.grid(row=0, column=0, rowspan=3, sticky=tk.NSEW) + self.cam_image = ttk.Frame( + self, padding=get_theme_padding("padding_canvas_surface") + ) + themed_grid( + self.cam_image, + row=0, + column=0, + rowspan=3, + sticky=tk.NSEW, + padx=("layout_panel_gap", "layout_section_gap"), + pady="layout_panel_gap", + ) #: bool: The docked flag. self.is_docked = True #: int: The width of the canvas. - self.canvas_width = 512 + self.canvas_width = canvas_min_size #: int: The height of the canvas. - self.canvas_height = 512 + self.canvas_height = canvas_min_size #: tk.Canvas: The canvas that will hold the camera image. self.canvas = tk.Canvas( - self.cam_image, width=self.canvas_width, height=self.canvas_height - ) - outer_pad = _space(5) - self.canvas.grid( - row=0, column=0, sticky=tk.NSEW, padx=outer_pad, pady=outer_pad + self.cam_image, + width=1, + height=1, + borderwidth=0, + highlightthickness=0, + relief=tk.FLAT, + background=get_theme_color("surface_bg"), ) + themed_grid(self.canvas, row=0, column=0, sticky=tk.NSEW) #: matplotlib.figure.Figure: The figure that will hold the camera image. self.matplotlib_figure = Figure(figsize=(6.0, 6.0), tight_layout=True) @@ -160,21 +180,46 @@ def __init__( #: DisplayModeFrame: The frame that controls single-channel vs overlay display. self.display_mode = DisplayModeFrame(self) - self.display_mode.grid( - row=0, column=1, sticky=tk.NSEW, padx=outer_pad, pady=outer_pad + themed_grid( + self.display_mode, + row=0, + column=1, + sticky=tk.NSEW, + padx=("layout_section_gap", "layout_panel_gap"), + pady=("layout_panel_gap", "layout_section_gap"), ) #: IntensityFrame: The frame that will hold the scale settings/palette color. self.lut = IntensityFrame(self) - self.lut.grid(row=1, column=1, sticky=tk.NSEW, padx=outer_pad, pady=outer_pad) + themed_grid( + self.lut, + row=1, + column=1, + sticky=tk.NSEW, + padx=("layout_section_gap", "layout_panel_gap"), + pady=(0, "layout_section_gap"), + ) #: RenderFrame: The frame that will hold the live display functionality. self.render = MipRenderFrame(self) - self.render.grid( - row=2, column=1, sticky=tk.NSEW, padx=outer_pad, pady=outer_pad + themed_grid( + self.render, + row=2, + column=1, + sticky=tk.NSEW, + padx=("layout_section_gap", "layout_panel_gap"), + pady=(0, "layout_panel_gap"), ) - uniform_grid(self) + configure_grid( + self, + columns={ + 0: {"weight": 1, "minsize": canvas_min_size}, + 1: {"weight": 0, "minsize": sidebar_min_width}, + }, + rows={0: 0, 1: 0, 2: 1}, + ) + configure_grid(self.cam_image, columns={0: 1}, rows={0: 1}) class CameraTab(tk.Frame): @@ -195,37 +240,60 @@ def __init__( Arbitrary keyword arguments. """ # Init Frame + kwargs.setdefault("bg", get_theme_color("panel_bg")) tk.Frame.__init__(self, cam_wave, *args, **kwargs) #: int: The index of the tab. self.index = 0 - #: ttk.Frame: The frame that will hold the camera image. - self.cam_image = ttk.Frame(self) - self.cam_image.grid(row=0, column=0, sticky=tk.NSEW) - self.display_setting = ttk.Frame(self) - self.display_setting.grid(row=0, column=1, sticky=tk.NSEW) + canvas_min_size = get_theme_spacing("layout_canvas_min_size") + histogram_min_height = get_theme_spacing("layout_histogram_min_height") + sidebar_min_width = get_theme_spacing("layout_sidebar_min_width") - self.grid_rowconfigure(0, weight=1) - self.grid_columnconfigure(0, weight=1) + #: ttk.Frame: The frame that will hold the camera image. + self.cam_image = ttk.Frame( + self, padding=get_theme_padding("padding_canvas_surface") + ) + themed_grid( + self.cam_image, + row=0, + column=0, + sticky=tk.NSEW, + padx=("layout_panel_gap", "layout_section_gap"), + pady="layout_panel_gap", + ) + self.display_setting = ttk.Frame( + self, padding=get_theme_padding("padding_panel_card") + ) + themed_grid( + self.display_setting, + row=0, + column=1, + sticky=tk.NSEW, + padx=("layout_section_gap", "layout_panel_gap"), + pady="layout_panel_gap", + ) #: bool: The docked flag. self.is_docked = True #: int: The width of the canvas. - self.canvas_width = 512 + self.canvas_width = canvas_min_size #: int: The height of the canvas. - self.canvas_height = 512 + self.canvas_height = canvas_min_size #: tk.Canvas: The canvas that will hold the camera image. self.canvas = tk.Canvas( - self.cam_image, width=self.canvas_width, height=self.canvas_height - ) - outer_pad = _space(5) - self.canvas.grid( - row=0, column=0, sticky=tk.NSEW, padx=outer_pad, pady=outer_pad + self.cam_image, + width=1, + height=1, + borderwidth=0, + highlightthickness=0, + relief=tk.FLAT, + background=get_theme_color("surface_bg"), ) + themed_grid(self.canvas, row=0, column=0, sticky=tk.NSEW) #: matplotlib.figure.Figure: The figure that will hold the camera image. self.matplotlib_figure = Figure(figsize=[6, 6], tight_layout=True) @@ -244,42 +312,81 @@ def __init__( label="Slice", ) self.slider.configure(state="disabled", font=get_theme_font("caption")) - self.slider.grid( - row=1, column=0, sticky=tk.NSEW, padx=outer_pad, pady=outer_pad + themed_grid( + self.slider, + row=1, + column=0, + sticky=tk.EW, + pady=("layout_section_gap", 0), ) self.slider.grid_remove() #: HistogramFrame: The frame that will hold the histogram. self.histogram = HistogramFrame(self) - self.histogram.grid( - row=2, + themed_grid( + self.histogram, + row=1, column=0, columnspan=2, sticky=tk.NSEW, - padx=outer_pad, - pady=outer_pad, + padx="layout_panel_gap", + pady=(0, "layout_panel_gap"), ) - #: IntensityFrame: The frame that will hold the scale settings/palette color. + #: DisplayModeFrame: The frame that controls single-channel vs overlay display. self.display_mode = DisplayModeFrame(self.display_setting) - self.display_mode.grid( - row=0, column=1, sticky=tk.NSEW, padx=outer_pad, pady=outer_pad - ) + themed_grid(self.display_mode, row=0, column=0, sticky=tk.NSEW) #: IntensityFrame: The frame that will hold the scale settings/palette color. self.lut = IntensityFrame(self.display_setting) - self.lut.grid(row=1, column=1, sticky=tk.NSEW, padx=outer_pad, pady=outer_pad) + themed_grid( + self.lut, + row=1, + column=0, + sticky=tk.NSEW, + pady=("layout_section_gap", 0), + ) #: MetricsFrame: The frame that will hold the camera selection and counts. self.image_metrics = MetricsFrame(self.display_setting) - self.image_metrics.grid( - row=2, column=1, sticky=tk.NSEW, padx=outer_pad, pady=outer_pad + themed_grid( + self.image_metrics, + row=2, + column=0, + sticky=tk.NSEW, + pady=("layout_section_gap", 0), ) #: RenderFrame: The frame that will hold the live display functionality. self.live_frame = RenderFrame(self.display_setting) - self.live_frame.grid( - row=3, column=1, sticky=tk.NSEW, padx=outer_pad, pady=outer_pad + themed_grid( + self.live_frame, + row=3, + column=0, + sticky=tk.NSEW, + pady=("layout_section_gap", 0), + ) + + configure_grid( + self, + columns={ + 0: {"weight": 1, "minsize": canvas_min_size}, + 1: {"weight": 0, "minsize": sidebar_min_width}, + }, + rows={ + 0: {"weight": 4, "minsize": canvas_min_size}, + 1: {"weight": 1, "minsize": histogram_min_height}, + }, + ) + configure_grid( + self.cam_image, + columns={0: 1}, + rows={0: {"weight": 1, "minsize": canvas_min_size}, 1: 0}, + ) + configure_grid( + self.display_setting, + columns={0: 1}, + rows={0: 0, 1: 0, 2: 0, 3: 0}, ) @@ -302,22 +409,15 @@ def __init__( """ text_label = "Intensity Histogram" + kwargs.setdefault("padding", get_theme_padding("padding_panel_card")) ttk.Labelframe.__init__(self, camera_tab, text=text_label, *args, **kwargs) - self.grid_rowconfigure(0, weight=1) - self.grid_columnconfigure(0, weight=1) + configure_grid(self, columns={0: 1}, rows={0: 1}) #: ttk.Frame: The frame for the histogram. self.frame = ttk.Frame(self) - self.frame.grid( - row=0, - column=0, - sticky=tk.NSEW, - padx=_space(0), - pady=_space(0), - ) - self.frame.grid_rowconfigure(0, weight=1) - self.frame.grid_columnconfigure(0, weight=1) + themed_grid(self.frame, row=0, column=0, sticky=tk.NSEW) + configure_grid(self.frame, columns={0: 1}, rows={0: 1}) #: matplotlib.figure.Figure: The figure for the histogram. self.figure = Figure(figsize=(1, 1)) @@ -350,6 +450,7 @@ def __init__( self, camera_tab: CameraTab, *args: Iterable, **kwargs: Dict[str, Any] ) -> None: text_label = "Display Mode" + kwargs.setdefault("padding", get_theme_padding("padding_panel_card")) ttk.Labelframe.__init__(self, camera_tab, text=text_label, *args, **kwargs) self.inputs = { @@ -364,12 +465,8 @@ def __init__( self.inputs["mode"].widget["values"] = ("Single", "Overlay") self.inputs["mode"].set("Single") self.inputs["mode"].widget.state(["!disabled", "readonly"]) - compact_pad = _space(3) - self.inputs["mode"].grid( - row=0, column=0, sticky=tk.NSEW, padx=compact_pad, pady=compact_pad - ) - - uniform_grid(self) + themed_grid(self.inputs["mode"], row=0, column=0, sticky=tk.NSEW) + configure_grid(self, columns={0: 1}, rows={0: 0}) class RenderFrame(ttk.Labelframe): @@ -391,6 +488,7 @@ def __init__( """ # Init Frame text_label = "Image Display" + kwargs.setdefault("padding", get_theme_padding("padding_panel_card")) ttk.Labelframe.__init__(self, camera_tab, text=text_label, *args, **kwargs) #: tk.StringVar: The variable that holds the live display functionality. @@ -400,17 +498,23 @@ def __init__( self.live = ttk.Combobox(self, textvariable=self.live_var, width=6) self.live["values"] = ("Live", "Slice") self.live.set("Live") - self.live.grid(row=0, column=0, sticky=tk.W) + themed_grid(self.live, row=0, column=0, sticky=tk.EW) self.live.state(["!disabled", "readonly"]) self.channel_var = tk.StringVar() self.channel = ttk.Combobox(self, textvariable=self.channel_var, width=6) self.channel["values"] = "CH1" self.channel.set("CH1") - self.channel.grid(row=1, column=0, sticky=tk.W) + themed_grid( + self.channel, + row=1, + column=0, + sticky=tk.EW, + pady=("layout_control_gap", 0), + ) self.channel.state(["disabled", "readonly"]) - uniform_grid(self) + configure_grid(self, columns={0: 1}, rows={0: 0, 1: 0}) class MipRenderFrame(ttk.Labelframe, CommonMethods): @@ -432,6 +536,7 @@ def __init__( """ # Init Frame text_label = "Image Display" + kwargs.setdefault("padding", get_theme_padding("padding_panel_card")) ttk.Labelframe.__init__(self, camera_tab, text=text_label, *args, **kwargs) # Label Strings @@ -457,16 +562,15 @@ def __init__( } self.inputs["perspective"].widget.state(["!disabled", "readonly"]) self.inputs["channel"].widget.state(["!disabled", "readonly"]) - compact_pad = _space(3) - self.inputs["perspective"].grid( - row=0, column=0, sticky=tk.EW, padx=compact_pad, pady=compact_pad - ) - self.inputs["channel"].grid( - row=1, column=0, sticky=tk.EW, padx=compact_pad, pady=compact_pad + themed_grid(self.inputs["perspective"], row=0, column=0, sticky=tk.EW) + themed_grid( + self.inputs["channel"], + row=1, + column=0, + sticky=tk.EW, + pady=("layout_control_gap", 0), ) - self.columnconfigure(0, weight=1) - - uniform_grid(self) + configure_grid(self, columns={0: 1}, rows={0: 0, 1: 0}) class WaveformTab(tk.Frame): @@ -488,6 +592,7 @@ def __init__( """ # Init Frame + kwargs.setdefault("bg", get_theme_color("panel_bg")) tk.Frame.__init__(self, camera_tab, *args, **kwargs) #: int: The index of the tab. @@ -497,8 +602,17 @@ def __init__( self.is_docked = True #: ttk.Frame: The frame that will hold the waveform plots. - self.waveform_plots = ttk.Frame(self) - self.waveform_plots.grid(row=0, column=0, sticky=tk.NSEW) + self.waveform_plots = ttk.Frame( + self, padding=get_theme_padding("padding_canvas_surface") + ) + themed_grid( + self.waveform_plots, + row=0, + column=0, + sticky=tk.NSEW, + padx="layout_panel_gap", + pady="layout_panel_gap", + ) #: matplotlib.figure.Figure: The figure that will hold the waveform plots. self.fig = Figure(figsize=(6, 6), dpi=100) @@ -509,12 +623,24 @@ def __init__( #: WaveformSettingsFrame: The frame that will hold the waveform settings. self.waveform_settings = WaveformSettingsFrame(self) - outer_pad = _space(5) - self.waveform_settings.grid( - row=1, column=0, sticky=tk.NSEW, padx=outer_pad, pady=outer_pad + themed_grid( + self.waveform_settings, + row=1, + column=0, + sticky=tk.EW, + padx="layout_panel_gap", + pady=(0, "layout_panel_gap"), ) - uniform_grid(self) + configure_grid( + self, + columns={0: 1}, + rows={ + 0: {"weight": 1, "minsize": get_theme_spacing("layout_canvas_min_size")}, + 1: 0, + }, + ) + configure_grid(self.waveform_plots, columns={0: 1}, rows={0: 1}) class WaveformSettingsFrame(ttk.Labelframe, CommonMethods): @@ -536,6 +662,7 @@ def __init__( """ # Init Frame text_label = "Settings" + kwargs.setdefault("padding", get_theme_padding("padding_panel_card")) ttk.Labelframe.__init__(self, waveform_tab, text=text_label, *args, **kwargs) #: dict: The dictionary that holds the widgets. @@ -549,10 +676,7 @@ def __init__( ) } - compact_pad = _space(3) - self.inputs["sample_rate"].grid( - row=0, column=0, sticky=tk.NSEW, padx=compact_pad, pady=compact_pad - ) + themed_grid(self.inputs["sample_rate"], row=0, column=0, sticky=tk.NSEW) self.inputs["waveform_template"] = LabelInput( parent=self, @@ -561,11 +685,15 @@ def __init__( input_var=tk.StringVar(), input_args={"width": 20}, ) - self.inputs["waveform_template"].grid( - row=0, column=1, sticky=tk.NSEW, padx=compact_pad, pady=compact_pad + themed_grid( + self.inputs["waveform_template"], + row=0, + column=1, + sticky=tk.NSEW, + padx=("layout_section_gap", 0), ) - uniform_grid(self) + configure_grid(self, columns={0: 0, 1: 1}, rows={0: 0}) class MetricsFrame(ttk.Labelframe, CommonMethods): @@ -586,6 +714,7 @@ def __init__( Arbitrary keyword arguments. """ text_label = "Image Metrics" + kwargs.setdefault("padding", get_theme_padding("padding_panel_card")) ttk.Labelframe.__init__(self, camera_tab, text=text_label, *args, **kwargs) #: dict: The dictionary that holds the widgets. @@ -598,8 +727,6 @@ def __init__( self.names = ["Frames", "Image", "Channel"] # Loop for widgets - outer_pad = _space(5) - compact_pad = _space(3) for i in range(len(self.labels)): if i == 0: self.inputs[self.names[i]] = LabelInput( @@ -610,12 +737,13 @@ def __init__( input_args={"from_": 1, "to": 32, "increment": 1, "width": 5}, label_pos="top", ) - self.inputs[self.names[i]].grid( + themed_grid( + self.inputs[self.names[i]], row=i, column=0, sticky=tk.NSEW, - padx=outer_pad, - pady=compact_pad, + padx="layout_control_gap", + pady=("layout_control_gap", 0), ) if i > 0: self.inputs[self.names[i]] = LabelInput( @@ -626,16 +754,17 @@ def __init__( input_args={"width": 5, "state": "disabled"}, label_pos="top", ) - self.inputs[self.names[i]].grid( + themed_grid( + self.inputs[self.names[i]], row=i, column=0, sticky=tk.NSEW, - padx=outer_pad, - pady=compact_pad, + padx="layout_control_gap", + pady=("layout_control_gap", 0), ) self.inputs[self.names[i]].configure(width=5) - uniform_grid(self) + configure_grid(self, columns={0: 1}) class IntensityFrame(ttk.Labelframe, CommonMethods): @@ -657,6 +786,7 @@ def __init__( """ # Init Frame text_label = "LUT" + kwargs.setdefault("padding", get_theme_padding("padding_panel_card")) ttk.Labelframe.__init__(self, camera_tab, text=text_label, *args, **kwargs) #: dict: The dictionary that holds the single-channel widgets. @@ -683,13 +813,12 @@ def __init__( self._active_multichannel_gamma = tk.DoubleVar(value=1.0) self.transpose = tk.BooleanVar() self.trans = "Flip XY" - dense_pad = _space(2) - compact_pad = _space(3) + dense_pad = get_theme_spacing("space_2") self.single_channel_frame = ttk.Frame(self) - self.single_channel_frame.grid(row=0, column=0, sticky=tk.NSEW) + themed_grid(self.single_channel_frame, row=0, column=0, sticky=tk.NSEW) self.multichannel_frame = ttk.Frame(self) - self.multichannel_frame.grid(row=0, column=0, sticky=tk.NSEW) + themed_grid(self.multichannel_frame, row=0, column=0, sticky=tk.NSEW) self.multichannel_frame.grid_remove() ttk.Label(self.multichannel_frame, text="Channel").grid( @@ -879,11 +1008,14 @@ def __init__( input_var=self.color, input_args={"value": self.color_values[i]}, ) - self.inputs[self.color_labels[i]].grid( - row=row, column=0, sticky=tk.W, pady=compact_pad + themed_grid( + self.inputs[self.color_labels[i]], + row=row, + column=0, + sticky=tk.W, + pady=("layout_control_gap", 0), ) row += 1 - #: tk.BooleanVar: The variable that holds the autoscale flag. self.autoscale = tk.BooleanVar() @@ -901,7 +1033,13 @@ def __init__( input_class=ttk.Checkbutton, input_var=self.autoscale, ) - self.inputs[self.auto].grid(row=row, column=0, sticky=tk.W, pady=compact_pad) + themed_grid( + self.inputs[self.auto], + row=row, + column=0, + sticky=tk.W, + pady=("layout_control_gap", 0), + ) row += 1 # Max and Min Counts @@ -913,19 +1051,19 @@ def __init__( input_var=tk.IntVar(), input_args={"from_": 1, "to": 2**16 - 1, "increment": 1, "width": 5}, ) - self.inputs[self.minmax_names[i]].grid( + themed_grid( + self.inputs[self.minmax_names[i]], row=row, column=0, - sticky=tk.W, - padx=compact_pad, - pady=compact_pad, + sticky=tk.EW, + padx="layout_control_gap", + pady=("layout_control_gap", 0), ) row += 1 - uniform_grid(self.single_channel_frame) - self.multichannel_frame.grid_columnconfigure(0, weight=0) - self.multichannel_frame.grid_columnconfigure(1, weight=1) - uniform_grid(self) + configure_grid(self.single_channel_frame, columns={0: 1}) + configure_grid(self.multichannel_frame, columns={0: 0, 1: 1}) + configure_grid(self, columns={0: 1}, rows={0: 1}) # Default to the compact LUT editor from startup, before acquisition begins. self.set_multichannel_controls_visible(True) diff --git a/src/navigate/view/main_window_content/multiposition_tab.py b/src/navigate/view/main_window_content/multiposition_tab.py index 569c85f9b..11b9152a1 100644 --- a/src/navigate/view/main_window_content/multiposition_tab.py +++ b/src/navigate/view/main_window_content/multiposition_tab.py @@ -42,7 +42,7 @@ from pandastable.headers import IndexHeader # Local Imports -from navigate.view.custom_widgets.common import uniform_grid +from navigate.view.custom_widgets.common import configure_grid, themed_grid from navigate.view.theme import get_theme_color, get_theme_font # Logger Setup @@ -87,18 +87,28 @@ def __init__(self, setntbk, *args, **kwargs): #: MultiPointFrame: The frame that contains the widgets for the multipoint # experiment settings. self.tiling_buttons = MultiPointFrame(self) - self.tiling_buttons.grid( - row=0, column=0, columnspan=3, sticky=tk.NSEW, padx=10, pady=10 + themed_grid( + self.tiling_buttons, + row=0, + column=0, + sticky=tk.EW, + padx="layout_panel_gap", + pady=("layout_panel_gap", "layout_section_gap"), ) #: MultiPointList: The frame that contains the widgets for the multipoint # experiment settings. self.multipoint_list = MultiPointList(self) - self.multipoint_list.grid( - row=6, column=0, columnspan=3, sticky=tk.NSEW, padx=10, pady=10 + themed_grid( + self.multipoint_list, + row=1, + column=0, + sticky=tk.NSEW, + padx="layout_panel_gap", + pady=("layout_section_gap", "layout_panel_gap"), ) - uniform_grid(self) + configure_grid(self, columns={0: 1}, rows={0: 0, 1: 1}) class MultiPointFrame(ttk.Labelframe): @@ -141,12 +151,17 @@ def __init__(self, settings_tab, *args, **kwargs): else: row, column = 0, 1 - button.grid( - row=row, column=column, sticky=tk.NSEW, padx=(4, 4), pady=(4, 4) + themed_grid( + button, + row=row, + column=column, + sticky=tk.NSEW, + padx="space_2", + pady="space_2", ) counter += 1 - uniform_grid(self) + configure_grid(self, columns={0: 1, 1: 1}, rows={0: 1, 1: 1}) def get_variables(self): """Returns a dictionary of all the variables that are tied to each widget name. @@ -204,6 +219,7 @@ def __init__(self, settings_tab, *args, **kwargs): self.pt = MultiPositionTable(self, showtoolbar=False, showstatusbar=True) self.pt.show() self.pt.model.df = df + configure_grid(self, columns={0: 0, 1: 1}, rows={0: 0, 1: 1, 2: 0}) def get_table(self): """Returns a reference to multipoint table dataframe. diff --git a/src/navigate/view/main_window_content/settings_notebook.py b/src/navigate/view/main_window_content/settings_notebook.py index 5da447601..340351002 100644 --- a/src/navigate/view/main_window_content/settings_notebook.py +++ b/src/navigate/view/main_window_content/settings_notebook.py @@ -39,6 +39,7 @@ # Local Imports from navigate.view.custom_widgets.DockableNotebook import DockableNotebook +from navigate.view.custom_widgets.common import configure_grid, themed_grid # Import Sub-Frames from navigate.view.main_window_content.camera_tab import CameraSettingsTab @@ -82,7 +83,7 @@ def __init__( super().__init__(frame_left, root, *args, **kwargs) # Putting notebook 1 into left frame - self.grid(row=0, column=0) + themed_grid(self, row=0, column=0, sticky=tk.NSEW) #: ChannelsTab: Channels tab self.channels_tab = ChannelsTab(self) @@ -110,3 +111,5 @@ def __init__( self.add(self.camera_settings_tab, text="Camera Settings", sticky=tk.NSEW) self.add(self.stage_control_tab, text="Stage Control", sticky=tk.NSEW) self.add(self.multiposition_tab, text="Multiposition", sticky=tk.NSEW) + + configure_grid(self, columns={0: 1}, rows={0: 1}) diff --git a/src/navigate/view/main_window_content/stage_tab.py b/src/navigate/view/main_window_content/stage_tab.py index d030fab1e..b20a0d467 100644 --- a/src/navigate/view/main_window_content/stage_tab.py +++ b/src/navigate/view/main_window_content/stage_tab.py @@ -44,8 +44,8 @@ from navigate.view.custom_widgets.LabelInputWidgetFactory import LabelInput from navigate.view.custom_widgets.validation import ValidatedSpinbox from navigate.view.custom_widgets.validation import ValidatedEntry -from navigate.view.custom_widgets.common import uniform_grid -from navigate.view.theme import get_theme_color +from navigate.view.custom_widgets.common import configure_grid, themed_grid, uniform_grid +from navigate.view.theme import get_theme_color, get_theme_space_px import navigate # Logger Setup @@ -159,39 +159,93 @@ def __init__( #: PositionFrame: Position frame. self.position_frame = PositionFrame(self) - self.position_frame.grid( - row=0, column=0, rowspan=1, sticky=tk.NSEW, padx=3, pady=3 + themed_grid( + self.position_frame, + row=0, + column=0, + sticky=tk.NSEW, + padx=("layout_panel_gap", "layout_section_gap"), + pady=("layout_panel_gap", "layout_section_gap"), ) #: StackShortcuts: Stack shortcuts. self.stack_shortcuts = StackShortcuts(self) - self.stack_shortcuts.grid(row=1, column=0, rowspan=1, sticky=tk.NSEW) + themed_grid( + self.stack_shortcuts, + row=1, + column=0, + sticky=tk.NSEW, + padx=("layout_panel_gap", "layout_section_gap"), + pady=("layout_section_gap", "layout_panel_gap"), + ) #: XYFrame: XY frame. self.xy_frame = XYFrame(self) - self.xy_frame.grid(row=0, column=1, rowspan=2, sticky=tk.NSEW, padx=3, pady=3) + themed_grid( + self.xy_frame, + row=0, + column=1, + rowspan=2, + sticky=tk.NSEW, + padx="layout_section_gap", + pady="layout_panel_gap", + ) #: OtherAxisFrame: Z frame. self.z_frame = OtherAxisFrame(stage_control_tab=self, name="Z") - self.z_frame.grid(row=0, column=2, rowspan=2, sticky=tk.NSEW, padx=3, pady=3) + themed_grid( + self.z_frame, + row=0, + column=2, + rowspan=2, + sticky=tk.NSEW, + padx=("layout_section_gap", "layout_panel_gap"), + pady="layout_panel_gap", + ) #: OtherAxisFrame: Theta frame. self.theta_frame = OtherAxisFrame(stage_control_tab=self, name="Theta") - self.theta_frame.grid( - row=2, column=2, rowspan=2, sticky=tk.NSEW, padx=3, pady=3 + themed_grid( + self.theta_frame, + row=2, + column=2, + rowspan=2, + sticky=tk.NSEW, + padx=("layout_section_gap", "layout_panel_gap"), + pady=("layout_section_gap", "layout_panel_gap"), ) # OtherAxisFrame: Focus frame. self.f_frame = OtherAxisFrame(stage_control_tab=self, name="Focus") - self.f_frame.grid(row=2, column=0, rowspan=2, sticky=tk.NSEW, padx=3, pady=3) + themed_grid( + self.f_frame, + row=2, + column=0, + rowspan=2, + sticky=tk.NSEW, + padx=("layout_panel_gap", "layout_section_gap"), + pady=("layout_section_gap", "layout_panel_gap"), + ) #: StopFrame: Stop frame. self.stop_frame = StopFrame( stage_control_tab=self, name="Stage Movement Interrupt" ) - self.stop_frame.grid(row=2, column=1, rowspan=2, sticky=tk.NSEW, padx=3, pady=3) + themed_grid( + self.stop_frame, + row=2, + column=1, + rowspan=2, + sticky=tk.NSEW, + padx="layout_section_gap", + pady=("layout_section_gap", "layout_panel_gap"), + ) - uniform_grid(self) + configure_grid( + self, + columns={0: 1, 1: 1, 2: 1}, + rows={0: 1, 1: 1, 2: 1, 3: 1}, + ) self.default_axes = ["x", "y", "z", "theta", "f"] @@ -224,6 +278,7 @@ def load_images(self) -> None: self, f"{name}", tk.PhotoImage( + master=self, file=image_directory.joinpath("images", f"{name}.png") ).subsample(2, 2), ) @@ -318,19 +373,22 @@ def add_additional_stage(self, stage_name: str) -> None: """ self.default_axes.append(stage_name) additional_stage = OtherAxisFrame(self, stage_name.upper()) - additional_stage.grid( - row=(len(self.default_axes) % 2) * 2, - column=len(self.default_axes) // 2 + 1, + row = (len(self.default_axes) % 2) * 2 + column = len(self.default_axes) // 2 + 1 + themed_grid( + additional_stage, + row=row, + column=column, sticky=tk.NSEW, rowspan=2, - padx=3, - pady=3, + padx="layout_panel_gap", + pady="layout_panel_gap", ) setattr(self, f"{stage_name}_frame", additional_stage) self.position_frame.add_position_entry(stage_name, stage_name.upper()) - uniform_grid(self) + configure_grid(self, columns={column: 1}, rows={row: 1, row + 1: 1}) class OtherAxisFrame(ttk.Labelframe): @@ -416,26 +474,66 @@ def __init__( space_2 = ttk.Label(self, borderwidth=0) # Griding out buttons - self.large_up_btn.grid( - row=(row := 0), column=1, rowspan=1, columnspan=1, padx=2, pady=2 + themed_grid( + self.large_up_btn, + row=(row := 0), + column=1, + rowspan=1, + columnspan=1, + padx="space_1", + pady="space_1", ) - self.up_btn.grid( - row=(row := row + 1), column=1, rowspan=1, columnspan=1, pady=(2, 0) + themed_grid( + self.up_btn, + row=(row := row + 1), + column=1, + rowspan=1, + columnspan=1, + pady=("space_1", 0), ) - space_1.grid( - row=(row := row + 1), column=1, rowspan=1, columnspan=1, padx=2, pady=0 + themed_grid( + space_1, + row=(row := row + 1), + column=1, + rowspan=1, + columnspan=1, + padx="space_1", + pady=get_theme_space_px(0), ) - self.increment_box.grid( - row=(row := row + 1), column=1, rowspan=1, columnspan=1, padx=2, pady=0 + themed_grid( + self.increment_box, + row=(row := row + 1), + column=1, + rowspan=1, + columnspan=1, + padx="space_1", + pady=get_theme_space_px(0), ) - space_2.grid( - row=(row := row + 1), column=1, rowspan=1, columnspan=1, padx=2, pady=0 + themed_grid( + space_2, + row=(row := row + 1), + column=1, + rowspan=1, + columnspan=1, + padx="space_1", + pady=get_theme_space_px(0), ) - self.down_btn.grid( - row=(row := row + 1), column=1, rowspan=1, columnspan=1, pady=(0, 2) + themed_grid( + self.down_btn, + row=(row := row + 1), + column=1, + rowspan=1, + columnspan=1, + pady=(0, "space_1"), ) - self.large_down_btn.grid( - row=(row + 1), column=1, rowspan=1, columnspan=1, padx=2, pady=2 + themed_grid( + self.large_down_btn, + row=(row + 1), + column=1, + rowspan=1, + columnspan=1, + padx="space_1", + pady="space_1", ) uniform_grid(self) @@ -787,42 +885,97 @@ def __init__( } # Up - self.large_up_y_btn.grid( - row=0, column=4, rowspan=2, columnspan=2, padx=2, pady=2 + themed_grid( + self.large_up_y_btn, + row=0, + column=4, + rowspan=2, + columnspan=2, + padx="space_1", + pady="space_1", ) - self.up_y_btn.grid(row=2, column=4, rowspan=2, columnspan=2, padx=2, pady=2) + themed_grid( + self.up_y_btn, + row=2, + column=4, + rowspan=2, + columnspan=2, + padx="space_1", + pady="space_1", + ) # Increment box. - self.increment_box.grid( - row=4, column=4, rowspan=3, columnspan=2, padx=2, pady=2 + themed_grid( + self.increment_box, + row=4, + column=4, + rowspan=3, + columnspan=2, + padx="space_1", + pady="space_1", ) # Down - self.down_y_btn.grid(row=7, column=4, rowspan=2, columnspan=2, padx=2, pady=2) + themed_grid( + self.down_y_btn, + row=7, + column=4, + rowspan=2, + columnspan=2, + padx="space_1", + pady="space_1", + ) - self.large_down_y_btn.grid( - row=9, column=4, rowspan=2, columnspan=2, padx=2, pady=2 + themed_grid( + self.large_down_y_btn, + row=9, + column=4, + rowspan=2, + columnspan=2, + padx="space_1", + pady="space_1", ) # Left - self.large_down_x_btn.grid( - row=5, column=0, rowspan=2, columnspan=2, padx=2, pady=2 + themed_grid( + self.large_down_x_btn, + row=5, + column=0, + rowspan=2, + columnspan=2, + padx="space_1", + pady="space_1", ) - self.down_x_btn.grid( + themed_grid( + self.down_x_btn, row=5, column=2, rowspan=2, columnspan=2, - padx=2, - pady=2, + padx="space_1", + pady="space_1", ) # Right - self.up_x_btn.grid(row=5, column=6, rowspan=2, columnspan=2, padx=2, pady=2) - self.large_up_x_btn.grid( - row=5, column=8, rowspan=2, columnspan=2, padx=2, pady=2 + themed_grid( + self.up_x_btn, + row=5, + column=6, + rowspan=2, + columnspan=2, + padx="space_1", + pady="space_1", + ) + themed_grid( + self.large_up_x_btn, + row=5, + column=8, + rowspan=2, + columnspan=2, + padx="space_1", + pady="space_1", ) uniform_grid(self) @@ -970,9 +1123,9 @@ def __init__( ) # Griding out buttons - self.stop_btn.grid(row=0, column=0, rowspan=2, pady=2) - self.home_btn.grid(row=1, column=0, rowspan=2, pady=2) - self.joystick_btn.grid(row=2, column=0, rowspan=2, pady=2) + themed_grid(self.stop_btn, row=0, column=0, rowspan=2, pady="space_1") + themed_grid(self.home_btn, row=1, column=0, rowspan=2, pady="space_1") + themed_grid(self.joystick_btn, row=2, column=0, rowspan=2, pady="space_1") uniform_grid(self) diff --git a/src/navigate/view/theme.py b/src/navigate/view/theme.py index aa719ca0c..5f150194b 100644 --- a/src/navigate/view/theme.py +++ b/src/navigate/view/theme.py @@ -100,7 +100,16 @@ "space_7": 16, "space_8": 20, "space_9": 24, + "layout_window_gap": 12, + "layout_panel_gap": 10, + "layout_section_gap": 8, + "layout_control_gap": 6, + "layout_sidebar_min_width": 224, + "layout_canvas_min_size": 512, + "layout_histogram_min_height": 180, "padding_button": (8, 4), + "padding_panel_card": (12, 10), + "padding_canvas_surface": (10, 10, 10, 10), "padding_stage_stop_button": (8, 16), "padding_stage_home_button": (8, 6), "padding_notebook_tab": (10, 4), @@ -661,9 +670,9 @@ def _apply_rounded_notebook_tabs( notebook_bg : str Notebook background color for corner blending. panel_bg : str - Panel background color for selected and disabled tabs. + Panel background color for unselected and disabled tabs. surface_bg : str - Surface background color for unselected tabs. + Surface background color for selected tabs. border : str Border color for tab outlines. accent_hover : str @@ -690,7 +699,7 @@ def _apply_rounded_notebook_tabs( normal = _rounded_photo( root, "rounded_tab_normal", - surface_bg, + panel_bg, border, width=tab_w, height=tab_h, @@ -701,7 +710,7 @@ def _apply_rounded_notebook_tabs( selected = _rounded_photo( root, "rounded_tab_selected", - panel_bg, + surface_bg, border, width=tab_w, height=tab_h, @@ -1102,6 +1111,13 @@ def apply_theme(root: tk.Tk, gui_settings: Any = None) -> tuple[str, dict[str, s stage_stop_button_padding = get_theme_padding("padding_stage_stop_button") stage_home_button_padding = get_theme_padding("padding_stage_home_button") notebook_tab_padding = get_theme_padding("padding_notebook_tab") + if len(notebook_tab_padding) == 2: + selected_notebook_tab_padding = ( + notebook_tab_padding[0] + 1, + notebook_tab_padding[1] + 2, + ) + else: + selected_notebook_tab_padding = notebook_tab_padding root.configure(bg=window_bg) @@ -1363,7 +1379,7 @@ def apply_theme(root: tk.Tk, gui_settings: Any = None) -> tuple[str, dict[str, s _safe_style_configure( style, "TNotebook.Tab", - background=surface_bg, + background=panel_bg, foreground=text, padding=notebook_tab_padding, font=font_body_bold, @@ -1371,8 +1387,10 @@ def apply_theme(root: tk.Tk, gui_settings: Any = None) -> tuple[str, dict[str, s _safe_style_map( style, "TNotebook.Tab", - background=[("selected", panel_bg), ("active", accent_hover)], + background=[("selected", surface_bg), ("active", accent_hover)], foreground=[("disabled", muted_text), ("selected", text)], + padding=[("selected", selected_notebook_tab_padding)], + expand=[("selected", (1, 1, 1, 0))], ) _apply_rounded_notebook_tabs( root, diff --git a/test/controller/sub_controllers/test_mip_view_projection.py b/test/controller/sub_controllers/test_mip_view_projection.py index e34374aad..cc96a7a78 100644 --- a/test/controller/sub_controllers/test_mip_view_projection.py +++ b/test/controller/sub_controllers/test_mip_view_projection.py @@ -50,6 +50,39 @@ def get(self): return self.value +class _CanvasSizeStub: + def __init__(self, width, height, *, requested_width=1, requested_height=1): + self._width = width + self._height = height + self._requested_width = requested_width + self._requested_height = requested_height + + def winfo_width(self): + return self._width + + def winfo_height(self): + return self._height + + def cget(self, key): + if key == "width": + return self._requested_width + if key == "height": + return self._requested_height + raise KeyError(key) + + +class _PhotoStub: + def __init__(self, width, height): + self._width = width + self._height = height + + def width(self): + return self._width + + def height(self): + return self._height + + def test_try_to_display_image_updates_orthogonal_projections(monkeypatch): controller = MIPViewController.__new__(MIPViewController) controller.image_mode = "z-stack" @@ -290,6 +323,51 @@ def test_get_mip_image_multi_perspective_composition(): np.testing.assert_array_equal(image[3:5, 0:2], np.array([[50, 60], [70, 80]])) +def test_mip_down_sample_image_multi_uses_canvas_widget_size(): + controller = MIPViewController.__new__(MIPViewController) + controller.render_widgets = {"perspective": _Getter("Multi")} + controller.canvas = _CanvasSizeStub(640, 480, requested_width=1, requested_height=1) + controller.canvas_width = 1 + controller.canvas_height = 1 + + image = np.arange(25, dtype=np.uint16).reshape(5, 5) + down_sampled = controller.down_sample_image(image) + + assert down_sampled.shape == (480, 640) + assert controller.canvas_width == 640 + assert controller.canvas_height == 480 + + +def test_clear_mip_resets_cached_canvas_image_item(): + controller = MIPViewController.__new__(MIPViewController) + controller.canvas = MagicMock() + controller.canvas_width = 320 + controller.canvas_height = 240 + controller._img_item = 42 + controller.tk_image = object() + + controller._clear_mip() + + controller.canvas.delete.assert_called_once_with("all") + controller.canvas.create_text.assert_called_once() + assert controller._img_item is None + assert controller.tk_image is None + + +def test_ensure_canvas_image_recreates_missing_canvas_item_without_new_photo(): + controller = BaseViewController.__new__(BaseViewController) + controller.canvas = MagicMock() + controller._photo = _PhotoStub(10, 20) + controller._photo_mode = "RGB" + controller._img_item = None + + controller._ensure_canvas_image(10, 20, "RGB") + + controller.canvas.create_image.assert_called_once_with( + 0, 0, image=controller._photo, anchor="nw" + ) + + def test_overlay_channel_defaults_follow_imagej_order(): controller = MIPViewController.__new__(MIPViewController) controller.selected_channels = ["CH1", "CH2", "CH3", "CH4"] diff --git a/test/view/custom_widgets/test_LabelInputWidgetFactory.py b/test/view/custom_widgets/test_LabelInputWidgetFactory.py index 942866738..605fd4b59 100644 --- a/test/view/custom_widgets/test_LabelInputWidgetFactory.py +++ b/test/view/custom_widgets/test_LabelInputWidgetFactory.py @@ -7,18 +7,50 @@ def get(self): raise TypeError -def test_label_input_get(): +def _grid_padding_pair(value): + text = str(value).strip() + if text.startswith("(") and text.endswith(")"): + parts = [int(part.strip()) for part in text[1:-1].split(",") if part.strip()] + else: + parts = [int(part) for part in text.split()] + if len(parts) == 1: + return (parts[0], parts[0]) + return tuple(parts) + + +def test_label_input_get(tk_root): from navigate.view.custom_widgets.LabelInputWidgetFactory import LabelInput - root = tk.Tk() - label_input = LabelInput(root) - root.update() + frame = ttk.Frame(tk_root) + frame.grid(row=0, column=0) + label_input = LabelInput(frame) + tk_root.update() assert label_input.get() == "" - label_input = LabelInput(root, input_var=NastyVar()) - root.update() + label_input.destroy() + label_input = LabelInput(frame, input_var=NastyVar()) + tk_root.update() assert label_input.get() == "" assert label_input.get(1) == 1 - root.destroy() + label_input.destroy() + frame.destroy() + + +def test_label_input_pad_input_resolves_theme_spacing_tokens(tk_root): + from navigate.view.custom_widgets.LabelInputWidgetFactory import LabelInput + + frame = ttk.Frame(tk_root) + frame.grid(row=0, column=0) + label_input = LabelInput(frame, label="Exposure") + tk_root.update_idletasks() + + label_input.pad_input("space_2", "space_1", "space_3", "space_4") + tk_root.update_idletasks() + + grid_info = label_input.widget.grid_info() + assert _grid_padding_pair(grid_info["padx"]) == (4, 6) + assert _grid_padding_pair(grid_info["pady"]) == (2, 8) + label_input.destroy() + frame.destroy() def test_label_input_checkbutton_honors_left_label_position(tk_root): diff --git a/test/view/main_window_content/test_main_ui_layout.py b/test/view/main_window_content/test_main_ui_layout.py new file mode 100644 index 000000000..340f0b181 --- /dev/null +++ b/test/view/main_window_content/test_main_ui_layout.py @@ -0,0 +1,93 @@ +import tkinter as tk +from tkinter import ttk + +from navigate.view.main_window_content.acquire_notebook import AcquireBar +from navigate.view.main_window_content.settings_notebook import SettingsNotebook +from navigate.view.theme import get_theme_padding_px, get_theme_space_px + + +def _grid_padding_pair(value): + text = str(value).strip() + if text.startswith("(") and text.endswith(")"): + parts = [int(part.strip()) for part in text[1:-1].split(",") if part.strip()] + else: + parts = [int(part) for part in text.split()] + if len(parts) == 1: + return (parts[0], parts[0]) + return tuple(parts) + + +def test_acquire_bar_uses_weighted_progress_layout(tk_root): + host = ttk.Frame(tk_root) + host.grid(row=0, column=0, sticky=tk.NSEW) + + acquire_bar = AcquireBar(host, tk_root) + tk_root.update_idletasks() + + assert acquire_bar.grid_info()["sticky"] == "nesw" + assert acquire_bar.grid_columnconfigure(2)["weight"] == 1 + assert acquire_bar.progBar_frame.grid_columnconfigure(0)["weight"] == 1 + assert acquire_bar.progBar_frame.grid_rowconfigure(1)["weight"] == 1 + + host.destroy() + + +def test_settings_notebook_tabs_use_weighted_main_layout(tk_root): + host = ttk.Frame(tk_root) + host.grid(row=0, column=0, sticky=tk.NSEW) + + settings_notebook = SettingsNotebook(host, tk_root) + tk_root.update_idletasks() + + assert settings_notebook.grid_columnconfigure(0)["weight"] == 1 + assert settings_notebook.grid_rowconfigure(0)["weight"] == 1 + + assert settings_notebook.channels_tab.grid_rowconfigure(0)["weight"] == 3 + assert settings_notebook.channels_tab.grid_rowconfigure(3)["weight"] == 1 + assert settings_notebook.camera_settings_tab.grid_columnconfigure(1)["weight"] == 1 + assert settings_notebook.camera_settings_tab.grid_rowconfigure(1)["weight"] == 2 + assert settings_notebook.stage_control_tab.grid_columnconfigure(2)["weight"] == 1 + assert settings_notebook.stage_control_tab.grid_rowconfigure(3)["weight"] == 1 + assert settings_notebook.multiposition_tab.grid_columnconfigure(0)["weight"] == 1 + assert settings_notebook.multiposition_tab.grid_rowconfigure(1)["weight"] == 1 + + host.destroy() + + +def test_channels_tab_forms_use_themed_spacing(tk_root): + host = ttk.Frame(tk_root) + host.grid(row=0, column=0, sticky=tk.NSEW) + + settings_notebook = SettingsNotebook(host, tk_root) + tk_root.update_idletasks() + + stack_frame = settings_notebook.channels_tab.stack_acq_frame + timepoint_frame = settings_notebook.channels_tab.stack_timepoint_frame + multipoint_frame = settings_notebook.channels_tab.multipoint_frame + quick_launch = settings_notebook.channels_tab.quick_launch + + assert _grid_padding_pair(stack_frame.inputs["start_position"].grid_info()["padx"]) == ( + get_theme_padding_px((6, 0)) + ) + assert _grid_padding_pair(stack_frame.inputs["start_position"].grid_info()["pady"]) == ( + get_theme_space_px(2), + get_theme_space_px(2), + ) + assert _grid_padding_pair(stack_frame.inputs["z_offset"].grid_info()["pady"]) == ( + get_theme_space_px(5), + get_theme_space_px(5), + ) + assert _grid_padding_pair(timepoint_frame.laser_label.grid_info()["padx"]) == ( + get_theme_padding_px((4, 5)) + ) + assert _grid_padding_pair(timepoint_frame.total_time_spinval.grid_info()["pady"]) == ( + get_theme_padding_px((2, 6)) + ) + assert _grid_padding_pair(multipoint_frame.buttons["tiling"].grid_info()["padx"]) == ( + get_theme_padding_px((10, 0)) + ) + assert _grid_padding_pair( + quick_launch.buttons["waveform_parameters"].grid_info()["padx"] + ) == get_theme_padding_px((4, 4)) + + host.destroy() diff --git a/test/view/test_configurator_application_window.py b/test/view/test_configurator_application_window.py index 0acb851a2..17580f278 100644 --- a/test/view/test_configurator_application_window.py +++ b/test/view/test_configurator_application_window.py @@ -32,7 +32,20 @@ from tkinter import ttk +from navigate.view.custom_widgets.CollapsibleFrame import CollapsibleFrame from navigate.view.configurator_application_window import ConfigurationAssistantWindow +from navigate.view.theme import get_theme_padding_px, get_theme_space_px + + +def _grid_padding_pair(value): + text = str(value).strip() + if text.startswith("(") and text.endswith(")"): + parts = [int(part.strip()) for part in text[1:-1].split(",") if part.strip()] + else: + parts = [int(part) for part in text.split()] + if len(parts) == 1: + return (parts[0], parts[0]) + return tuple(parts) def test_configurator_top_window_uses_ttk_buttons(tk_root): @@ -53,3 +66,37 @@ def test_configurator_top_window_uses_ttk_buttons(tk_root): top.save_button.destroy() top.cancel_button.destroy() view.destroy() + + +def test_configurator_window_uses_themed_spacing(tk_root): + view = ConfigurationAssistantWindow(tk_root) + tk_root.update_idletasks() + + assert _grid_padding_pair(view.top_frame.grid_info()["padx"]) == ( + get_theme_space_px(3), + get_theme_space_px(3), + ) + assert _grid_padding_pair(view.microscope_frame.grid_info()["pady"]) == ( + get_theme_space_px(3), + get_theme_space_px(3), + ) + assert _grid_padding_pair(view.top_window.new_button.grid_info()["pady"]) == ( + get_theme_padding_px((10, 1)) + ) + + view.top_window.new_button.destroy() + view.top_window.load_button.destroy() + view.top_window.add_button.destroy() + view.top_window.save_button.destroy() + view.top_window.cancel_button.destroy() + view.destroy() + + +def test_collapsible_frame_header_uses_themed_spacing(tk_root): + frame = CollapsibleFrame(tk_root, title="Hardware") + frame.grid(row=0, column=0, sticky="nsew") + tk_root.update_idletasks() + + assert int(frame.label.cget("padx")) == get_theme_space_px(5) + + frame.destroy()