Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions src/navigate/controller/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
90 changes: 50 additions & 40 deletions src/navigate/controller/sub_controllers/camera_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -224,6 +225,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

Expand Down Expand Up @@ -793,10 +797,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

Expand All @@ -811,6 +816,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 digital_zoom(self) -> np.ndarray:
"""Apply digital zoom.
Expand Down Expand Up @@ -959,6 +966,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.

Expand Down Expand Up @@ -1099,61 +1125,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):
Expand Down Expand Up @@ -1247,7 +1257,7 @@ def __init__(self, view, parent_controller=None) -> None:
# Slider Binding
self.view.slider.bind("<Motion>", self.slider_update)

self.resize_event_id = self.view.bind("<Configure>", self.resize)
self.resize_binding_id = self.view.canvas.bind("<Configure>", self.resize)

#: str: The display state.
self.display_state = "Live"
Expand Down Expand Up @@ -1645,7 +1655,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("<Configure>", self.resize)
self.resize_binding_id = self.view.canvas.bind("<Configure>", self.resize)

#: bool: The display enabled flag.
self.display_enabled = tk.BooleanVar()
Expand Down
8 changes: 6 additions & 2 deletions src/navigate/controller/sub_controllers/waveform_tab.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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):
Expand Down
29 changes: 19 additions & 10 deletions src/navigate/view/custom_widgets/LabelInputWidgetFactory.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,19 +38,29 @@
# 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,
HoverTkButton,
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.

Expand Down Expand Up @@ -138,7 +148,7 @@ def __init__(
else:
#: ttk.Label: The label of the input widget
self.label = ttk.Label(self, text=label, **label_args)
self.label.grid(row=0, column=0, sticky=tk.EW)
themed_grid(self.label, row=0, column=0, sticky=tk.EW)
input_args["textvariable"] = input_var

"""Call the passed widget type constructor with the passed args"""
Expand All @@ -147,15 +157,11 @@ def __init__(

"""Specify label position"""
if label_pos == "top":
self.widget.grid(row=1, column=0, sticky=(tk.W + tk.E))
self.columnconfigure(0, weight=1)
self.rowconfigure(index=0, weight=1)
self.rowconfigure(index=1, weight=1)
themed_grid(self.widget, row=1, column=0, sticky=tk.EW)
configure_grid(self, columns={0: 1}, rows={0: 0, 1: 1})
else:
self.widget.grid(row=0, column=1, sticky=(tk.W + tk.E))
self.rowconfigure(0, weight=1)
self.columnconfigure(index=0, weight=1)
self.columnconfigure(index=1, weight=1)
themed_grid(self.widget, row=0, column=1, sticky=tk.EW)
configure_grid(self, columns={0: 0, 1: 1}, rows={0: 1})

def get(self, default=None):
"""Returns the value of the input widget
Expand Down Expand Up @@ -301,4 +307,7 @@ 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)),
)
92 changes: 90 additions & 2 deletions src/navigate/view/custom_widgets/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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.
Expand Down
Loading
Loading