From 2d121f2bebacd5e7b32206f1ff0cb95e3adae7b3 Mon Sep 17 00:00:00 2001 From: Nodar Gogoberidze Date: Mon, 12 May 2025 10:30:23 -0400 Subject: [PATCH 01/23] Create fixed size scroll area --- src/ndv/views/_wx/_array_view.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/ndv/views/_wx/_array_view.py b/src/ndv/views/_wx/_array_view.py index 1b8e82ae..2092c0be 100644 --- a/src/ndv/views/_wx/_array_view.py +++ b/src/ndv/views/_wx/_array_view.py @@ -445,8 +445,13 @@ def __init__(self, canvas_widget: wx.Window, parent: wx.Window = None): self.add_roi_btn = wx.ToggleButton(self, label="ROI", size=(40, -1)) _add_icon(self.add_roi_btn, "mdi:vector-rectangle") + # create a scrolled panel for LUTs + self.luts_scroll = wx.ScrolledWindow(self, style=wx.VSCROLL) + self.luts_scroll.SetScrollRate(0, 10) + # LUT layout (simple vertical grouping for LUT widgets) self.luts = wx.BoxSizer(wx.VERTICAL) + self.luts_scroll.SetSizer(self.luts) self._btns = wx.BoxSizer(wx.HORIZONTAL) self._btns.AddStretchSpacer() @@ -465,13 +470,17 @@ def __init__(self, canvas_widget: wx.Window, parent: wx.Window = None): inner.Add(self._canvas, 1, wx.EXPAND | wx.ALL) inner.Add(self._hover_info_label, 0, wx.EXPAND | wx.BOTTOM) inner.Add(self.dims_sliders, 0, wx.EXPAND | wx.BOTTOM) - inner.Add(self.luts, 0, wx.EXPAND) + inner.Add(self.luts_scroll, 0, wx.EXPAND | wx.BOTTOM, 5) inner.Add(self._btns, 0, wx.EXPAND) outer = wx.BoxSizer(wx.VERTICAL) outer.Add(inner, 1, wx.EXPAND | wx.ALL, 10) self.SetSizer(outer) self.SetInitialSize(wx.Size(600, 800)) + + # set a fixed height for the scrollable area for luts + inner.SetItemMinSize(self.luts_scroll, -1, 200) # fixed height of 200 px + self.Layout() @@ -538,8 +547,11 @@ def set_visible_axes(self, axes: Sequence[AxisKey]) -> None: def frontend_widget(self) -> wx.Window: return self._wxwidget + def _lut_area(self) -> wx.Window: + return self._wxwidget.luts_scroll + def add_lut_view(self, channel: ChannelKey) -> WxLutView: - wdg = self.frontend_widget() + wdg = self._lut_area() view = WxRGBView(wdg, channel) if channel == "RGB" else WxLutView(wdg, channel) self._wxwidget.luts.Add(view._wxwidget, 0, wx.EXPAND | wx.BOTTOM, 5) self._luts[channel] = view @@ -548,7 +560,8 @@ def add_lut_view(self, channel: ChannelKey) -> WxLutView: view.histogramRequested.connect(self.histogramRequested) # Update the layout to reflect above changes - self._wxwidget.Layout() + wdg.Layout() + wdg.FitInside() return view # TODO: Fix type @@ -559,10 +572,12 @@ def add_histogram(self, channel: ChannelKey, canvas: HistogramCanvas) -> None: self._wxwidget.Layout() def remove_lut_view(self, lut: LutView) -> None: + scrollwdg = self._lut_area() wxwdg = cast("_WxLUTWidget", lut.frontend_widget()) self._wxwidget.luts.Detach(wxwdg) wxwdg.Destroy() - self._wxwidget.Layout() + scrollwdg.Layout() + scrollwdg.FitInside() def create_sliders(self, coords: Mapping[Hashable, Sequence]) -> None: self._wxwidget.dims_sliders.create_sliders(coords) From 2876cfdda207f9e53d660e4c59328822c30a301d Mon Sep 17 00:00:00 2001 From: Nodar Gogoberidze Date: Mon, 12 May 2025 13:16:05 -0400 Subject: [PATCH 02/23] Add channel selector --- src/ndv/views/_wx/_array_view.py | 186 ++++++++++++++++++++++++++++++- 1 file changed, 184 insertions(+), 2 deletions(-) diff --git a/src/ndv/views/_wx/_array_view.py b/src/ndv/views/_wx/_array_view.py index 2092c0be..5d9496a0 100644 --- a/src/ndv/views/_wx/_array_view.py +++ b/src/ndv/views/_wx/_array_view.py @@ -67,6 +67,142 @@ def _add_icon(btn: wx.AnyButton, icon: str) -> None: btn.SetLabel("") btn.SetBitmapLabel(bitmap) +class _LutChannelSelector(wx.Panel): + def __init__(self, parent: wx.Window, channels: None | list[ChannelKey]=None): + super().__init__(parent) + + self.channels = channels or [] + # all channels visible, by default + self.visible_channels = set(self.channels) + + # Dropdown button with currenct selection + self.dropdown_btn = wx.Button( + self, label="Channel Display Options", style=wx.BU_EXACTFIT) + self.dropdown_btn.Bind(wx.EVT_BUTTON, self._on_dropdown_clicked) + + # Display indicator for how many channels are currently visible + self.selection_info = wx.StaticText(self, label="") + self._update_selection_info() + + # Create a popup window for the checklist + self.popup = wx.Dialog(self, style=wx.BORDER_SIMPLE | wx.STAY_ON_TOP | wx.FRAME_NO_TASKBAR | wx.FRAME_FLOAT_ON_PARENT) + self.popup.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW)) + # evt handler for clicking outside of popup dialog + self.popup.Bind(wx.EVT_ACTIVATE, self._on_popup_deactivate) + + # Create a checklist for the popup + self.checklist = wx.CheckListBox(self.popup, choices=[]) + self.checklist.Bind(wx.EVT_CHECKLISTBOX, self._on_selection_changed) + + # Select All / None buttons + btn_sizer = wx.BoxSizer(wx.HORIZONTAL) + self.select_all_btn = wx.Button(self.popup, label="Select All", size=(80, -1)) + self.select_none_btn = wx.Button(self.popup, label="Select None", size=(80, -1)) + self.select_all_btn.Bind(wx.EVT_BUTTON, self._on_select_all) + self.select_none_btn.Bind(wx.EVT_BUTTON, self._on_select_none) + btn_sizer.Add(self.select_all_btn, 0, wx.ALL, 5) + btn_sizer.Add(self.select_none_btn, 0, wx.ALL, 5) + + # Layout for popup + popup_sizer = wx.BoxSizer(wx.VERTICAL) + popup_sizer.Add(self.checklist, 1, wx.EXPAND | wx.ALL, 5) + popup_sizer.Add(btn_sizer, 0, wx.ALIGN_CENTER | wx.BOTTOM, 5) + self.popup.SetSizer(popup_sizer) + + # Layout for the main widget + sizer = wx.BoxSizer(wx.HORIZONTAL) + sizer.Add(self.dropdown_btn, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5) + sizer.Add(self.selection_info, 0, wx.ALIGN_CENTER_VERTICAL) + self.SetSizer(sizer) + + # Signals + self.on_selection_changed_callback = None + + def set_channels(self, channels: list[ChannelKey]): + """Update the channel list.""" + self.channels = list(channels) + self.visible_channels = set(self.channels) + self._update_checklist() + self._update_selection_info() + + def _update_checklist(self): + """Update the checklist with current channels.""" + self.checklist.Clear() + for channel in self.channels: + self.checklist.Append(str(channel)) + idx = self.checklist.FindString(str(channel)) + if channel in self.visible_channels: + self.checklist.Check(idx) + + def _update_selection_info(self): + """Update the selection info text.""" + if len(self.visible_channels) == len(self.channels): + self.selection_info.SetLabel( + f"All channels visible ({len(self.channels)})") + else: + self.selection_info.SetLabel( + f"{len(self.visible_channels)} of {len(self.channels)} channels visible") + self.Layout() + + def _on_dropdown_clicked(self, evt): + """Show the dropdown checklist.""" + # Position the popup below the button + btn_size = self.dropdown_btn.GetSize() + btn_pos = self.dropdown_btn.GetPosition() + popup_pos = self.ClientToScreen( + wx.Point(btn_pos.x, btn_pos.y + btn_size.height)) + + # Update the checklist + self._update_checklist() + + # Size the popup according to content + self.popup.SetSize(wx.Size(250, min(400, 50 + len(self.channels) * 22))) + self.checklist.SetSize(self.popup.GetClientSize()) + self.popup.Layout() + + # Show the popup + self.popup.SetPosition(popup_pos) + self.popup.Show(show=True) + + def _on_popup_deactivate(self, evt): + """Close the popup when it loses focus (user clicks outside).""" + if not evt.GetActive(): + self.popup.Show(show=False) + + def _on_selection_changed(self, evt): + """Handle selection changes in the checklist.""" + idx = evt.GetSelection() + channel = self.channels[idx] + + if self.checklist.IsChecked(idx): + self.visible_channels.add(channel) + else: + self.visible_channels.discard(channel) + + self._update_selection_info() + + # Notify parent + if self.on_selection_changed_callback: + self.on_selection_changed_callback(self.visible_channels) + + def _on_select_all(self, evt): + """Select all channels.""" + for i in range(self.checklist.GetCount()): + self.checklist.Check(i, True) + self.visible_channels = set(self.channels) + self._update_selection_info() + if self.on_selection_changed_callback: + self.on_selection_changed_callback(self.visible_channels) + + def _on_select_none(self, evt): + """Deselect all channels.""" + for i in range(self.checklist.GetCount()): + self.checklist.Check(i, False) + self.visible_channels = set() + self._update_selection_info() + if self.on_selection_changed_callback: + self.on_selection_changed_callback(self.visible_channels) + # mostly copied from _qt.qt_view._QLUTWidget class _WxLUTWidget(wx.Panel): @@ -445,7 +581,15 @@ def __init__(self, canvas_widget: wx.Window, parent: wx.Window = None): self.add_roi_btn = wx.ToggleButton(self, label="ROI", size=(40, -1)) _add_icon(self.add_roi_btn, "mdi:vector-rectangle") - # create a scrolled panel for LUTs + # Add LUT channel selector + self.lut_selector = _LutChannelSelector(self) + + # Toolbar for LUT management + self.lut_toolbar = wx.BoxSizer(wx.HORIZONTAL) + self.lut_toolbar.Add(self.lut_selector, 0, wx.EXPAND | wx.ALL, 5) + self.lut_toolbar.AddStretchSpacer() + + # Create a scrolled panel for LUTs self.luts_scroll = wx.ScrolledWindow(self, style=wx.VSCROLL) self.luts_scroll.SetScrollRate(0, 10) @@ -470,6 +614,7 @@ def __init__(self, canvas_widget: wx.Window, parent: wx.Window = None): inner.Add(self._canvas, 1, wx.EXPAND | wx.ALL) inner.Add(self._hover_info_label, 0, wx.EXPAND | wx.BOTTOM) inner.Add(self.dims_sliders, 0, wx.EXPAND | wx.BOTTOM) + inner.Add(self.lut_toolbar, 0, wx.EXPAND | wx.BOTTOM, 5) inner.Add(self.luts_scroll, 0, wx.EXPAND | wx.BOTTOM, 5) inner.Add(self._btns, 0, wx.EXPAND) @@ -479,7 +624,7 @@ def __init__(self, canvas_widget: wx.Window, parent: wx.Window = None): self.SetInitialSize(wx.Size(600, 800)) # set a fixed height for the scrollable area for luts - inner.SetItemMinSize(self.luts_scroll, -1, 200) # fixed height of 200 px + inner.SetItemMinSize(self.luts_scroll, -1, 200) self.Layout() @@ -506,6 +651,9 @@ def __init__( wdg.ndims_btn.Bind(wx.EVT_TOGGLEBUTTON, self._on_ndims_toggled) wdg.add_roi_btn.Bind(wx.EVT_TOGGLEBUTTON, self._on_add_roi_toggled) + # Connect the LUT selector callback + wdg.lut_selector.on_selection_changed_callback = self._on_lut_selection_changed + def _on_channel_mode_changed(self, event: wx.CommandEvent) -> None: mode = self._wxwidget.channel_mode_combo.GetValue() self.channelModeChanged.emit(mode) @@ -537,6 +685,18 @@ def _on_add_roi_toggled(self, event: wx.CommandEvent) -> None: InteractionMode.CREATE_ROI if create_roi else InteractionMode.PAN_ZOOM ) + def _on_lut_selection_changed(self, visible_channels: list[ChannelKey]): + """Handle changes in visible LUT selection.""" + for channel, lut_view in self._luts.items(): + # Show/Hide the LUT view based on selection + if channel in visible_channels: + lut_view._wxwidget.Show() + else: + lut_view._wxwidget.Hide() + + # Update layout + self._wxwidget.Layout() + def visible_axes(self) -> Sequence[AxisKey]: return self._visible_axes # no widget to control this yet @@ -550,6 +710,9 @@ def frontend_widget(self) -> wx.Window: def _lut_area(self) -> wx.Window: return self._wxwidget.luts_scroll + def _lut_selector(self) -> _LutChannelSelector: + return self._wxwidget.lut_selector + def add_lut_view(self, channel: ChannelKey) -> WxLutView: wdg = self._lut_area() view = WxRGBView(wdg, channel) if channel == "RGB" else WxLutView(wdg, channel) @@ -559,6 +722,10 @@ def add_lut_view(self, channel: ChannelKey) -> WxLutView: view._wxwidget.histogram_btn.Show(self._viewer_model.show_histogram_button) view.histogramRequested.connect(self.histogramRequested) + # Add the channel to the selector + channels = list(self._luts.keys()) + self._lut_selector().set_channels(channels) + # Update the layout to reflect above changes wdg.Layout() wdg.FitInside() @@ -572,10 +739,25 @@ def add_histogram(self, channel: ChannelKey, canvas: HistogramCanvas) -> None: self._wxwidget.Layout() def remove_lut_view(self, lut: LutView) -> None: + # Find the channel key for this LUT view + channel_to_remove = None + for channel, view in self._luts.items(): + if view == lut: + channel_to_remove = channel + break + scrollwdg = self._lut_area() wxwdg = cast("_WxLUTWidget", lut.frontend_widget()) self._wxwidget.luts.Detach(wxwdg) wxwdg.Destroy() + + # Remove from our dictionaries + if channel_to_remove: + del self._luts[channel_to_remove] + + # Update the channel selector + self._lut_selector().set_channels(list(self._luts.keys())) + scrollwdg.Layout() scrollwdg.FitInside() From 316643f16c644b01a82fa5f40bfaf58b15acacd4 Mon Sep 17 00:00:00 2001 From: Nodar Gogoberidze Date: Tue, 13 May 2025 09:50:07 -0400 Subject: [PATCH 03/23] Setup signaling for updating layout --- src/ndv/views/_wx/_array_view.py | 55 ++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/src/ndv/views/_wx/_array_view.py b/src/ndv/views/_wx/_array_view.py index 5d9496a0..3645603d 100644 --- a/src/ndv/views/_wx/_array_view.py +++ b/src/ndv/views/_wx/_array_view.py @@ -68,6 +68,9 @@ def _add_icon(btn: wx.AnyButton, icon: str) -> None: btn.SetBitmapLabel(bitmap) class _LutChannelSelector(wx.Panel): + # actually set[ChannelKey] | set + selectionChanged = Signal(set) + def __init__(self, parent: wx.Window, channels: None | list[ChannelKey]=None): super().__init__(parent) @@ -115,9 +118,6 @@ def __init__(self, parent: wx.Window, channels: None | list[ChannelKey]=None): sizer.Add(self.selection_info, 0, wx.ALIGN_CENTER_VERTICAL) self.SetSizer(sizer) - # Signals - self.on_selection_changed_callback = None - def set_channels(self, channels: list[ChannelKey]): """Update the channel list.""" self.channels = list(channels) @@ -165,7 +165,7 @@ def _on_dropdown_clicked(self, evt): self.popup.Show(show=True) def _on_popup_deactivate(self, evt): - """Close the popup when it loses focus (user clicks outside).""" + """Close the popup when it loses focus (user clicks outside).""" if not evt.GetActive(): self.popup.Show(show=False) @@ -181,9 +181,7 @@ def _on_selection_changed(self, evt): self._update_selection_info() - # Notify parent - if self.on_selection_changed_callback: - self.on_selection_changed_callback(self.visible_channels) + self.selectionChanged.emit(self.visible_channels) def _on_select_all(self, evt): """Select all channels.""" @@ -191,8 +189,7 @@ def _on_select_all(self, evt): self.checklist.Check(i, True) self.visible_channels = set(self.channels) self._update_selection_info() - if self.on_selection_changed_callback: - self.on_selection_changed_callback(self.visible_channels) + self.selectionChanged.emit(self.visible_channels) def _on_select_none(self, evt): """Deselect all channels.""" @@ -200,8 +197,7 @@ def _on_select_none(self, evt): self.checklist.Check(i, False) self.visible_channels = set() self._update_selection_info() - if self.on_selection_changed_callback: - self.on_selection_changed_callback(self.visible_channels) + self.selectionChanged.emit(self.visible_channels) # mostly copied from _qt.qt_view._QLUTWidget @@ -295,6 +291,7 @@ def __init__(self, parent: wx.Window) -> None: class WxLutView(LutView): # NB: In practice this will be a ChannelKey but Unions not allowed here. histogramRequested = psygnal.Signal(object) + lutsUpdated = Signal() def __init__(self, parent: wx.Window, channel: ChannelKey = None) -> None: super().__init__() @@ -443,15 +440,18 @@ def set_clim_bounds( def set_channel_visible(self, visible: bool) -> None: self._wxwidget.visible.SetValue(visible) + self.lutsUpdated.emit() def set_visible(self, visible: bool) -> None: if visible: self._wxwidget.Show() else: self._wxwidget.Hide() + self.lutsUpdated.emit() def close(self) -> None: self._wxwidget.Close() + self.lutsUpdated.emit() class WxRGBView(WxLutView): @@ -617,15 +617,24 @@ def __init__(self, canvas_widget: wx.Window, parent: wx.Window = None): inner.Add(self.lut_toolbar, 0, wx.EXPAND | wx.BOTTOM, 5) inner.Add(self.luts_scroll, 0, wx.EXPAND | wx.BOTTOM, 5) inner.Add(self._btns, 0, wx.EXPAND) + self._inner_sizer = inner outer = wx.BoxSizer(wx.VERTICAL) outer.Add(inner, 1, wx.EXPAND | wx.ALL, 10) self.SetSizer(outer) self.SetInitialSize(wx.Size(600, 800)) - # set a fixed height for the scrollable area for luts - inner.SetItemMinSize(self.luts_scroll, -1, 200) + self.update_lut_scroll_size() + self.Layout() + def update_lut_scroll_size(self): + self.luts_scroll.Layout() + total_size = self.luts.GetMinSize() + total_height = total_size.GetHeight() + new_height = max(30, min(total_height, 200)) + self._inner_sizer.SetItemMinSize(self.luts_scroll, -1, new_height) + self.luts_scroll.SetVirtualSize(total_size) + self.luts_scroll.FitInside() self.Layout() @@ -651,8 +660,7 @@ def __init__( wdg.ndims_btn.Bind(wx.EVT_TOGGLEBUTTON, self._on_ndims_toggled) wdg.add_roi_btn.Bind(wx.EVT_TOGGLEBUTTON, self._on_add_roi_toggled) - # Connect the LUT selector callback - wdg.lut_selector.on_selection_changed_callback = self._on_lut_selection_changed + wdg.lut_selector.selectionChanged.connect(self._on_lut_selection_changed) def _on_channel_mode_changed(self, event: wx.CommandEvent) -> None: mode = self._wxwidget.channel_mode_combo.GetValue() @@ -690,9 +698,9 @@ def _on_lut_selection_changed(self, visible_channels: list[ChannelKey]): for channel, lut_view in self._luts.items(): # Show/Hide the LUT view based on selection if channel in visible_channels: - lut_view._wxwidget.Show() + lut_view.set_channel_visible(True) else: - lut_view._wxwidget.Hide() + lut_view.set_channel_visible(False) # Update layout self._wxwidget.Layout() @@ -714,21 +722,21 @@ def _lut_selector(self) -> _LutChannelSelector: return self._wxwidget.lut_selector def add_lut_view(self, channel: ChannelKey) -> WxLutView: - wdg = self._lut_area() - view = WxRGBView(wdg, channel) if channel == "RGB" else WxLutView(wdg, channel) + scrollwdg = self._lut_area() + view = WxRGBView(scrollwdg, channel) if channel == "RGB" else WxLutView(scrollwdg, channel) self._wxwidget.luts.Add(view._wxwidget, 0, wx.EXPAND | wx.BOTTOM, 5) self._luts[channel] = view # TODO: Reusable synchronization with ViewerModel view._wxwidget.histogram_btn.Show(self._viewer_model.show_histogram_button) view.histogramRequested.connect(self.histogramRequested) + view.lutsUpdated.connect(self._wxwidget.update_lut_scroll_size) # Add the channel to the selector channels = list(self._luts.keys()) self._lut_selector().set_channels(channels) - # Update the layout to reflect above changes - wdg.Layout() - wdg.FitInside() + self._wxwidget.update_lut_scroll_size() + return view # TODO: Fix type @@ -758,8 +766,7 @@ def remove_lut_view(self, lut: LutView) -> None: # Update the channel selector self._lut_selector().set_channels(list(self._luts.keys())) - scrollwdg.Layout() - scrollwdg.FitInside() + self._wxwidget.update_lut_scroll_size() def create_sliders(self, coords: Mapping[Hashable, Sequence]) -> None: self._wxwidget.dims_sliders.create_sliders(coords) From 2d6980043abca8286c7d78fac8f3ae086da66fab Mon Sep 17 00:00:00 2001 From: Nodar Gogoberidze Date: Tue, 13 May 2025 17:48:22 -0400 Subject: [PATCH 04/23] Make display options smarter - only numbers in display options - only show display options if composite - TODO: only display selection options on mode change --- src/ndv/views/_wx/_array_view.py | 44 ++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/src/ndv/views/_wx/_array_view.py b/src/ndv/views/_wx/_array_view.py index 3645603d..dcfc4269 100644 --- a/src/ndv/views/_wx/_array_view.py +++ b/src/ndv/views/_wx/_array_view.py @@ -74,9 +74,9 @@ class _LutChannelSelector(wx.Panel): def __init__(self, parent: wx.Window, channels: None | list[ChannelKey]=None): super().__init__(parent) - self.channels = channels or [] + self.channels: list[ChannelKey] = channels or [] # all channels visible, by default - self.visible_channels = set(self.channels) + self.visible_channels: set[ChannelKey] = set(self.channels) # Dropdown button with currenct selection self.dropdown_btn = wx.Button( @@ -118,10 +118,23 @@ def __init__(self, parent: wx.Window, channels: None | list[ChannelKey]=None): sizer.Add(self.selection_info, 0, wx.ALIGN_CENTER_VERTICAL) self.SetSizer(sizer) - def set_channels(self, channels: list[ChannelKey]): - """Update the channel list.""" - self.channels = list(channels) - self.visible_channels = set(self.channels) + def set_visible_channels(self, visible_channels: list[ChannelKey]): + """Update the visible channels.""" + self.visible_channels = set(visible_channels) + self._update_checklist() + self._update_selection_info() + + def add_channel(self, channel: ChannelKey): + if (type(channel) is int) or (type(channel) is str and channel.isdigit()): + if channel not in self.channels: + self.channels.append(channel) + self.visible_channels.add(channel) + self._update_checklist() + self._update_selection_info() + + def remove_channel(self, channel: ChannelKey): + self.channels = [ch for ch in self.channels if not ch == channel] + self.visible_channels = {ch for ch in self.visible_channels if not ch == channel} self._update_checklist() self._update_selection_info() @@ -440,7 +453,6 @@ def set_clim_bounds( def set_channel_visible(self, visible: bool) -> None: self._wxwidget.visible.SetValue(visible) - self.lutsUpdated.emit() def set_visible(self, visible: bool) -> None: if visible: @@ -695,12 +707,13 @@ def _on_add_roi_toggled(self, event: wx.CommandEvent) -> None: def _on_lut_selection_changed(self, visible_channels: list[ChannelKey]): """Handle changes in visible LUT selection.""" + self._wxwidget.lut_selector.set_visible_channels(visible_channels) for channel, lut_view in self._luts.items(): # Show/Hide the LUT view based on selection if channel in visible_channels: - lut_view.set_channel_visible(True) + lut_view.set_visible(True) else: - lut_view.set_channel_visible(False) + lut_view.set_visible(False) # Update layout self._wxwidget.Layout() @@ -731,10 +744,7 @@ def add_lut_view(self, channel: ChannelKey) -> WxLutView: view.histogramRequested.connect(self.histogramRequested) view.lutsUpdated.connect(self._wxwidget.update_lut_scroll_size) - # Add the channel to the selector - channels = list(self._luts.keys()) - self._lut_selector().set_channels(channels) - + self._lut_selector().add_channel(channel) self._wxwidget.update_lut_scroll_size() return view @@ -754,7 +764,6 @@ def remove_lut_view(self, lut: LutView) -> None: channel_to_remove = channel break - scrollwdg = self._lut_area() wxwdg = cast("_WxLUTWidget", lut.frontend_widget()) self._wxwidget.luts.Detach(wxwdg) wxwdg.Destroy() @@ -763,8 +772,7 @@ def remove_lut_view(self, lut: LutView) -> None: if channel_to_remove: del self._luts[channel_to_remove] - # Update the channel selector - self._lut_selector().set_channels(list(self._luts.keys())) + self._lut_selector().add_channel(channel) self._wxwidget.update_lut_scroll_size() @@ -792,6 +800,10 @@ def set_hover_info(self, text: str) -> None: def set_channel_mode(self, mode: ChannelMode) -> None: self._wxwidget.channel_mode_combo.SetValue(mode) + if mode == ChannelMode.COMPOSITE: + self._wxwidget.lut_selector.Show() + else: + self._wxwidget.lut_selector.Hide() def set_visible(self, visible: bool) -> None: if visible: From 3d342273c154535101c82c6ed391fd47763c4b3a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 13 May 2025 21:53:41 +0000 Subject: [PATCH 05/23] style(pre-commit.ci): auto fixes [...] --- src/ndv/views/_wx/_array_view.py | 37 +++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/src/ndv/views/_wx/_array_view.py b/src/ndv/views/_wx/_array_view.py index dcfc4269..d585e437 100644 --- a/src/ndv/views/_wx/_array_view.py +++ b/src/ndv/views/_wx/_array_view.py @@ -67,11 +67,12 @@ def _add_icon(btn: wx.AnyButton, icon: str) -> None: btn.SetLabel("") btn.SetBitmapLabel(bitmap) + class _LutChannelSelector(wx.Panel): # actually set[ChannelKey] | set selectionChanged = Signal(set) - def __init__(self, parent: wx.Window, channels: None | list[ChannelKey]=None): + def __init__(self, parent: wx.Window, channels: None | list[ChannelKey] = None): super().__init__(parent) self.channels: list[ChannelKey] = channels or [] @@ -80,7 +81,8 @@ def __init__(self, parent: wx.Window, channels: None | list[ChannelKey]=None): # Dropdown button with currenct selection self.dropdown_btn = wx.Button( - self, label="Channel Display Options", style=wx.BU_EXACTFIT) + self, label="Channel Display Options", style=wx.BU_EXACTFIT + ) self.dropdown_btn.Bind(wx.EVT_BUTTON, self._on_dropdown_clicked) # Display indicator for how many channels are currently visible @@ -88,8 +90,16 @@ def __init__(self, parent: wx.Window, channels: None | list[ChannelKey]=None): self._update_selection_info() # Create a popup window for the checklist - self.popup = wx.Dialog(self, style=wx.BORDER_SIMPLE | wx.STAY_ON_TOP | wx.FRAME_NO_TASKBAR | wx.FRAME_FLOAT_ON_PARENT) - self.popup.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW)) + self.popup = wx.Dialog( + self, + style=wx.BORDER_SIMPLE + | wx.STAY_ON_TOP + | wx.FRAME_NO_TASKBAR + | wx.FRAME_FLOAT_ON_PARENT, + ) + self.popup.SetBackgroundColour( + wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW) + ) # evt handler for clicking outside of popup dialog self.popup.Bind(wx.EVT_ACTIVATE, self._on_popup_deactivate) @@ -134,7 +144,9 @@ def add_channel(self, channel: ChannelKey): def remove_channel(self, channel: ChannelKey): self.channels = [ch for ch in self.channels if not ch == channel] - self.visible_channels = {ch for ch in self.visible_channels if not ch == channel} + self.visible_channels = { + ch for ch in self.visible_channels if not ch == channel + } self._update_checklist() self._update_selection_info() @@ -150,11 +162,11 @@ def _update_checklist(self): def _update_selection_info(self): """Update the selection info text.""" if len(self.visible_channels) == len(self.channels): - self.selection_info.SetLabel( - f"All channels visible ({len(self.channels)})") + self.selection_info.SetLabel(f"All channels visible ({len(self.channels)})") else: self.selection_info.SetLabel( - f"{len(self.visible_channels)} of {len(self.channels)} channels visible") + f"{len(self.visible_channels)} of {len(self.channels)} channels visible" + ) self.Layout() def _on_dropdown_clicked(self, evt): @@ -163,7 +175,8 @@ def _on_dropdown_clicked(self, evt): btn_size = self.dropdown_btn.GetSize() btn_pos = self.dropdown_btn.GetPosition() popup_pos = self.ClientToScreen( - wx.Point(btn_pos.x, btn_pos.y + btn_size.height)) + wx.Point(btn_pos.x, btn_pos.y + btn_size.height) + ) # Update the checklist self._update_checklist() @@ -736,7 +749,11 @@ def _lut_selector(self) -> _LutChannelSelector: def add_lut_view(self, channel: ChannelKey) -> WxLutView: scrollwdg = self._lut_area() - view = WxRGBView(scrollwdg, channel) if channel == "RGB" else WxLutView(scrollwdg, channel) + view = ( + WxRGBView(scrollwdg, channel) + if channel == "RGB" + else WxLutView(scrollwdg, channel) + ) self._wxwidget.luts.Add(view._wxwidget, 0, wx.EXPAND | wx.BOTTOM, 5) self._luts[channel] = view # TODO: Reusable synchronization with ViewerModel From 732bb2eafbb5b41b1dbf728ee9cc985dbe2f803d Mon Sep 17 00:00:00 2001 From: Nodar Gogoberidze Date: Wed, 14 May 2025 14:23:13 -0400 Subject: [PATCH 06/23] Display previously visible items on mode change --- src/ndv/views/_wx/_array_view.py | 47 ++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/src/ndv/views/_wx/_array_view.py b/src/ndv/views/_wx/_array_view.py index d585e437..3c72fdb5 100644 --- a/src/ndv/views/_wx/_array_view.py +++ b/src/ndv/views/_wx/_array_view.py @@ -317,7 +317,8 @@ def __init__(self, parent: wx.Window) -> None: class WxLutView(LutView): # NB: In practice this will be a ChannelKey but Unions not allowed here. histogramRequested = psygnal.Signal(object) - lutsUpdated = Signal() + # channel: ChannelKey, visible: bool|None, validate: bool|None + lutUpdated = Signal(object, bool, bool) def __init__(self, parent: wx.Window, channel: ChannelKey = None) -> None: super().__init__() @@ -467,16 +468,17 @@ def set_clim_bounds( def set_channel_visible(self, visible: bool) -> None: self._wxwidget.visible.SetValue(visible) - def set_visible(self, visible: bool) -> None: - if visible: - self._wxwidget.Show() - else: - self._wxwidget.Hide() - self.lutsUpdated.emit() + def set_visible(self, visible: bool, validate: bool=True) -> None: + """Sets visibility. + + `validate` will check the display options (`_LUTChannelSelctor`) + to ensure that visibility is toggled off there. + """ + self.lutUpdated.emit(self._channel, visible, validate) def close(self) -> None: self._wxwidget.Close() - self.lutsUpdated.emit() + self.lutUpdated.emit(self._channel) class WxRGBView(WxLutView): @@ -723,10 +725,11 @@ def _on_lut_selection_changed(self, visible_channels: list[ChannelKey]): self._wxwidget.lut_selector.set_visible_channels(visible_channels) for channel, lut_view in self._luts.items(): # Show/Hide the LUT view based on selection + # don't validate since we just set the visible channels if channel in visible_channels: - lut_view.set_visible(True) + lut_view.set_visible(True, validate=False) else: - lut_view.set_visible(False) + lut_view.set_visible(False, validate=False) # Update layout self._wxwidget.Layout() @@ -759,13 +762,35 @@ def add_lut_view(self, channel: ChannelKey) -> WxLutView: # TODO: Reusable synchronization with ViewerModel view._wxwidget.histogram_btn.Show(self._viewer_model.show_histogram_button) view.histogramRequested.connect(self.histogramRequested) - view.lutsUpdated.connect(self._wxwidget.update_lut_scroll_size) + view.lutUpdated.connect(self.update_lut_view) self._lut_selector().add_channel(channel) self._wxwidget.update_lut_scroll_size() return view + def update_lut_view( + self, + channel: ChannelKey, + visible: None | bool=None, + validate: None | bool=None + ): + # NOTE: we validate on `channel_mode` change + # and only when changed to `COMPOSITE` + # so this working properly depends on + # `channel_mode_combo`'s value being changed FIRST + # before each lut_view's `set_visible` is called, + # which is currently the case + mode = self._wxwidget.channel_mode_combo.GetValue() + if validate and mode == ChannelMode.COMPOSITE.value: + visible_channels = self._wxwidget.lut_selector.visible_channels + visible = visible and channel in visible_channels + if visible: + self._luts[channel]._wxwidget.Show() + elif visible is not None: + self._luts[channel]._wxwidget.Hide() + self._wxwidget.update_lut_scroll_size() + # TODO: Fix type def add_histogram(self, channel: ChannelKey, canvas: HistogramCanvas) -> None: if lut := self._luts.get(channel, None): From 5ea44a1135fd05b46029a8b72aad2289afa79aae Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 14 May 2025 18:23:43 +0000 Subject: [PATCH 07/23] style(pre-commit.ci): auto fixes [...] --- src/ndv/views/_wx/_array_view.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ndv/views/_wx/_array_view.py b/src/ndv/views/_wx/_array_view.py index 3c72fdb5..3cd148fd 100644 --- a/src/ndv/views/_wx/_array_view.py +++ b/src/ndv/views/_wx/_array_view.py @@ -468,7 +468,7 @@ def set_clim_bounds( def set_channel_visible(self, visible: bool) -> None: self._wxwidget.visible.SetValue(visible) - def set_visible(self, visible: bool, validate: bool=True) -> None: + def set_visible(self, visible: bool, validate: bool = True) -> None: """Sets visibility. `validate` will check the display options (`_LUTChannelSelctor`) @@ -772,8 +772,8 @@ def add_lut_view(self, channel: ChannelKey) -> WxLutView: def update_lut_view( self, channel: ChannelKey, - visible: None | bool=None, - validate: None | bool=None + visible: None | bool = None, + validate: None | bool = None, ): # NOTE: we validate on `channel_mode` change # and only when changed to `COMPOSITE` From 0ac765321b42b27277881b78441d67dbe964f0c2 Mon Sep 17 00:00:00 2001 From: Nodar Gogoberidze Date: Wed, 14 May 2025 14:34:51 -0400 Subject: [PATCH 08/23] Make update_lut_view private --- src/ndv/views/_wx/_array_view.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ndv/views/_wx/_array_view.py b/src/ndv/views/_wx/_array_view.py index 3cd148fd..02baeb19 100644 --- a/src/ndv/views/_wx/_array_view.py +++ b/src/ndv/views/_wx/_array_view.py @@ -762,14 +762,14 @@ def add_lut_view(self, channel: ChannelKey) -> WxLutView: # TODO: Reusable synchronization with ViewerModel view._wxwidget.histogram_btn.Show(self._viewer_model.show_histogram_button) view.histogramRequested.connect(self.histogramRequested) - view.lutUpdated.connect(self.update_lut_view) + view.lutUpdated.connect(self._update_lut_view) self._lut_selector().add_channel(channel) self._wxwidget.update_lut_scroll_size() return view - def update_lut_view( + def _update_lut_view( self, channel: ChannelKey, visible: None | bool = None, From bcff2806d2b1b3d37ddbb323f0e1814ed0b2234f Mon Sep 17 00:00:00 2001 From: Nodar Gogoberidze Date: Wed, 14 May 2025 14:52:37 -0400 Subject: [PATCH 09/23] 'visible' -> 'displayed' to avoid ambiguity --- src/ndv/views/_wx/_array_view.py | 55 ++++++++++++++++---------------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/src/ndv/views/_wx/_array_view.py b/src/ndv/views/_wx/_array_view.py index 02baeb19..a742714e 100644 --- a/src/ndv/views/_wx/_array_view.py +++ b/src/ndv/views/_wx/_array_view.py @@ -76,8 +76,8 @@ def __init__(self, parent: wx.Window, channels: None | list[ChannelKey] = None): super().__init__(parent) self.channels: list[ChannelKey] = channels or [] - # all channels visible, by default - self.visible_channels: set[ChannelKey] = set(self.channels) + # all channels displayed, by default + self.displayed_channels: set[ChannelKey] = set(self.channels) # Dropdown button with currenct selection self.dropdown_btn = wx.Button( @@ -85,7 +85,7 @@ def __init__(self, parent: wx.Window, channels: None | list[ChannelKey] = None): ) self.dropdown_btn.Bind(wx.EVT_BUTTON, self._on_dropdown_clicked) - # Display indicator for how many channels are currently visible + # Display indicator for how many channels are currently displayed self.selection_info = wx.StaticText(self, label="") self._update_selection_info() @@ -128,9 +128,8 @@ def __init__(self, parent: wx.Window, channels: None | list[ChannelKey] = None): sizer.Add(self.selection_info, 0, wx.ALIGN_CENTER_VERTICAL) self.SetSizer(sizer) - def set_visible_channels(self, visible_channels: list[ChannelKey]): - """Update the visible channels.""" - self.visible_channels = set(visible_channels) + def set_displayed_channels(self, displayed_channels: list[ChannelKey]): + self.displayed_channels = set(displayed_channels) self._update_checklist() self._update_selection_info() @@ -138,14 +137,14 @@ def add_channel(self, channel: ChannelKey): if (type(channel) is int) or (type(channel) is str and channel.isdigit()): if channel not in self.channels: self.channels.append(channel) - self.visible_channels.add(channel) + self.displayed_channels.add(channel) self._update_checklist() self._update_selection_info() def remove_channel(self, channel: ChannelKey): self.channels = [ch for ch in self.channels if not ch == channel] - self.visible_channels = { - ch for ch in self.visible_channels if not ch == channel + self.displayed_channels = { + ch for ch in self.displayed_channels if not ch == channel } self._update_checklist() self._update_selection_info() @@ -156,16 +155,18 @@ def _update_checklist(self): for channel in self.channels: self.checklist.Append(str(channel)) idx = self.checklist.FindString(str(channel)) - if channel in self.visible_channels: + if channel in self.displayed_channels: self.checklist.Check(idx) def _update_selection_info(self): """Update the selection info text.""" - if len(self.visible_channels) == len(self.channels): - self.selection_info.SetLabel(f"All channels visible ({len(self.channels)})") + if len(self.displayed_channels) == len(self.channels): + self.selection_info.SetLabel( + f"All channels displayed ({len(self.channels)})" + ) else: self.selection_info.SetLabel( - f"{len(self.visible_channels)} of {len(self.channels)} channels visible" + f"{len(self.displayed_channels)} of {len(self.channels)} channels displayed" ) self.Layout() @@ -201,29 +202,29 @@ def _on_selection_changed(self, evt): channel = self.channels[idx] if self.checklist.IsChecked(idx): - self.visible_channels.add(channel) + self.displayed_channels.add(channel) else: - self.visible_channels.discard(channel) + self.displayed_channels.discard(channel) self._update_selection_info() - self.selectionChanged.emit(self.visible_channels) + self.selectionChanged.emit(self.displayed_channels) def _on_select_all(self, evt): """Select all channels.""" for i in range(self.checklist.GetCount()): self.checklist.Check(i, True) - self.visible_channels = set(self.channels) + self.displayed_channels = set(self.channels) self._update_selection_info() - self.selectionChanged.emit(self.visible_channels) + self.selectionChanged.emit(self.displayed_channels) def _on_select_none(self, evt): """Deselect all channels.""" for i in range(self.checklist.GetCount()): self.checklist.Check(i, False) - self.visible_channels = set() + self.displayed_channels = set() self._update_selection_info() - self.selectionChanged.emit(self.visible_channels) + self.selectionChanged.emit(self.displayed_channels) # mostly copied from _qt.qt_view._QLUTWidget @@ -720,13 +721,13 @@ def _on_add_roi_toggled(self, event: wx.CommandEvent) -> None: InteractionMode.CREATE_ROI if create_roi else InteractionMode.PAN_ZOOM ) - def _on_lut_selection_changed(self, visible_channels: list[ChannelKey]): - """Handle changes in visible LUT selection.""" - self._wxwidget.lut_selector.set_visible_channels(visible_channels) + def _on_lut_selection_changed(self, displayed_channels: list[ChannelKey]): + """Handle changes in displayed LUT selection.""" + self._wxwidget.lut_selector.set_displayed_channels(displayed_channels) for channel, lut_view in self._luts.items(): # Show/Hide the LUT view based on selection - # don't validate since we just set the visible channels - if channel in visible_channels: + # don't validate since we just set the displayed channels + if channel in displayed_channels: lut_view.set_visible(True, validate=False) else: lut_view.set_visible(False, validate=False) @@ -783,8 +784,8 @@ def _update_lut_view( # which is currently the case mode = self._wxwidget.channel_mode_combo.GetValue() if validate and mode == ChannelMode.COMPOSITE.value: - visible_channels = self._wxwidget.lut_selector.visible_channels - visible = visible and channel in visible_channels + displayed_channels = self._wxwidget.lut_selector.displayed_channels + visible = visible and channel in displayed_channels if visible: self._luts[channel]._wxwidget.Show() elif visible is not None: From 7abd03dd83543c9b77e624453508eabe5cf00e9f Mon Sep 17 00:00:00 2001 From: Nodar Gogoberidze Date: Fri, 16 May 2025 10:06:46 -0400 Subject: [PATCH 10/23] Revert visibility signaling - reverts emit when setting lut visibility - reintroduces bug where all channels displayed on channel mode change - setup for alternative approach taht doesn't rely on emitting when setting visible --- src/ndv/views/_wx/_array_view.py | 54 +++++++------------------------- 1 file changed, 12 insertions(+), 42 deletions(-) diff --git a/src/ndv/views/_wx/_array_view.py b/src/ndv/views/_wx/_array_view.py index a742714e..bd1465fe 100644 --- a/src/ndv/views/_wx/_array_view.py +++ b/src/ndv/views/_wx/_array_view.py @@ -227,7 +227,6 @@ def _on_select_none(self, evt): self.selectionChanged.emit(self.displayed_channels) -# mostly copied from _qt.qt_view._QLUTWidget class _WxLUTWidget(wx.Panel): def __init__(self, parent: wx.Window) -> None: super().__init__(parent) @@ -318,8 +317,7 @@ def __init__(self, parent: wx.Window) -> None: class WxLutView(LutView): # NB: In practice this will be a ChannelKey but Unions not allowed here. histogramRequested = psygnal.Signal(object) - # channel: ChannelKey, visible: bool|None, validate: bool|None - lutUpdated = Signal(object, bool, bool) + lutUpdated = Signal() def __init__(self, parent: wx.Window, channel: ChannelKey = None) -> None: super().__init__() @@ -469,17 +467,16 @@ def set_clim_bounds( def set_channel_visible(self, visible: bool) -> None: self._wxwidget.visible.SetValue(visible) - def set_visible(self, visible: bool, validate: bool = True) -> None: - """Sets visibility. - - `validate` will check the display options (`_LUTChannelSelctor`) - to ensure that visibility is toggled off there. - """ - self.lutUpdated.emit(self._channel, visible, validate) + def set_visible(self, visible: bool) -> None: + if visible: + self._wxwidget.Show() + else: + self._wxwidget.Hide() + self.lutUpdated.emit() def close(self) -> None: self._wxwidget.Close() - self.lutUpdated.emit(self._channel) + self.lutUpdated.emit() class WxRGBView(WxLutView): @@ -723,16 +720,12 @@ def _on_add_roi_toggled(self, event: wx.CommandEvent) -> None: def _on_lut_selection_changed(self, displayed_channels: list[ChannelKey]): """Handle changes in displayed LUT selection.""" - self._wxwidget.lut_selector.set_displayed_channels(displayed_channels) for channel, lut_view in self._luts.items(): - # Show/Hide the LUT view based on selection - # don't validate since we just set the displayed channels if channel in displayed_channels: - lut_view.set_visible(True, validate=False) + lut_view.set_visible(True) else: - lut_view.set_visible(False, validate=False) + lut_view.set_visible(False) - # Update layout self._wxwidget.Layout() def visible_axes(self) -> Sequence[AxisKey]: @@ -763,35 +756,13 @@ def add_lut_view(self, channel: ChannelKey) -> WxLutView: # TODO: Reusable synchronization with ViewerModel view._wxwidget.histogram_btn.Show(self._viewer_model.show_histogram_button) view.histogramRequested.connect(self.histogramRequested) - view.lutUpdated.connect(self._update_lut_view) + view.lutUpdated.connect(self._wxwidget.update_lut_scroll_size) self._lut_selector().add_channel(channel) self._wxwidget.update_lut_scroll_size() return view - def _update_lut_view( - self, - channel: ChannelKey, - visible: None | bool = None, - validate: None | bool = None, - ): - # NOTE: we validate on `channel_mode` change - # and only when changed to `COMPOSITE` - # so this working properly depends on - # `channel_mode_combo`'s value being changed FIRST - # before each lut_view's `set_visible` is called, - # which is currently the case - mode = self._wxwidget.channel_mode_combo.GetValue() - if validate and mode == ChannelMode.COMPOSITE.value: - displayed_channels = self._wxwidget.lut_selector.displayed_channels - visible = visible and channel in displayed_channels - if visible: - self._luts[channel]._wxwidget.Show() - elif visible is not None: - self._luts[channel]._wxwidget.Hide() - self._wxwidget.update_lut_scroll_size() - # TODO: Fix type def add_histogram(self, channel: ChannelKey, canvas: HistogramCanvas) -> None: if lut := self._luts.get(channel, None): @@ -815,8 +786,7 @@ def remove_lut_view(self, lut: LutView) -> None: if channel_to_remove: del self._luts[channel_to_remove] - self._lut_selector().add_channel(channel) - + self._lut_selector().remove_channel(channel) self._wxwidget.update_lut_scroll_size() def create_sliders(self, coords: Mapping[Hashable, Sequence]) -> None: From e5980feb042d4bb028e0e8d562f7d832c1795f27 Mon Sep 17 00:00:00 2001 From: Nodar Gogoberidze Date: Fri, 16 May 2025 13:19:45 -0400 Subject: [PATCH 11/23] Set lut view state to keep visibility and display info - allows remembering display and visibility setttings across channel mode changes --- src/ndv/views/_wx/_array_view.py | 194 ++++++++++++++++++------------- 1 file changed, 114 insertions(+), 80 deletions(-) diff --git a/src/ndv/views/_wx/_array_view.py b/src/ndv/views/_wx/_array_view.py index bd1465fe..c2127f0b 100644 --- a/src/ndv/views/_wx/_array_view.py +++ b/src/ndv/views/_wx/_array_view.py @@ -3,6 +3,7 @@ import warnings from pathlib import Path from sys import version_info +from enum import Enum from typing import TYPE_CHECKING, cast import psygnal @@ -75,106 +76,110 @@ class _LutChannelSelector(wx.Panel): def __init__(self, parent: wx.Window, channels: None | list[ChannelKey] = None): super().__init__(parent) - self.channels: list[ChannelKey] = channels or [] - # all channels displayed, by default - self.displayed_channels: set[ChannelKey] = set(self.channels) + # channels must be list not set, because we index into it + # when the corresponding checklist index is changed + self._channels: list[ChannelKey] = channels or [] + # all channels displayed in view + # if displayed, may or may not be visible, if not displayed, not visible + self._displayed_channels: set[ChannelKey] = set(self._channels) + self._luts: dict[ChannelKey, WxLutView] = {} # Dropdown button with currenct selection - self.dropdown_btn = wx.Button( + self._dropdown_btn = wx.Button( self, label="Channel Display Options", style=wx.BU_EXACTFIT ) - self.dropdown_btn.Bind(wx.EVT_BUTTON, self._on_dropdown_clicked) + self._dropdown_btn.Bind(wx.EVT_BUTTON, self._on_dropdown_clicked) # Display indicator for how many channels are currently displayed - self.selection_info = wx.StaticText(self, label="") + self._selection_info = wx.StaticText(self, label="") self._update_selection_info() # Create a popup window for the checklist - self.popup = wx.Dialog( + self._popup = wx.Dialog( self, style=wx.BORDER_SIMPLE | wx.STAY_ON_TOP | wx.FRAME_NO_TASKBAR | wx.FRAME_FLOAT_ON_PARENT, ) - self.popup.SetBackgroundColour( + self._popup.SetBackgroundColour( wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW) ) # evt handler for clicking outside of popup dialog - self.popup.Bind(wx.EVT_ACTIVATE, self._on_popup_deactivate) + self._popup.Bind(wx.EVT_ACTIVATE, self._on_popup_deactivate) # Create a checklist for the popup - self.checklist = wx.CheckListBox(self.popup, choices=[]) - self.checklist.Bind(wx.EVT_CHECKLISTBOX, self._on_selection_changed) + self._checklist = wx.CheckListBox(self._popup, choices=[]) + self._checklist.Bind(wx.EVT_CHECKLISTBOX, self._on_selection_changed) # Select All / None buttons btn_sizer = wx.BoxSizer(wx.HORIZONTAL) - self.select_all_btn = wx.Button(self.popup, label="Select All", size=(80, -1)) - self.select_none_btn = wx.Button(self.popup, label="Select None", size=(80, -1)) - self.select_all_btn.Bind(wx.EVT_BUTTON, self._on_select_all) - self.select_none_btn.Bind(wx.EVT_BUTTON, self._on_select_none) - btn_sizer.Add(self.select_all_btn, 0, wx.ALL, 5) - btn_sizer.Add(self.select_none_btn, 0, wx.ALL, 5) + self._select_all_btn = wx.Button(self._popup, label="Select All", size=(80, -1)) + self._select_none_btn = wx.Button(self._popup, label="Select None", size=(80, -1)) + self._select_all_btn.Bind(wx.EVT_BUTTON, self._on_select_all) + self._select_none_btn.Bind(wx.EVT_BUTTON, self._on_select_none) + btn_sizer.Add(self._select_all_btn, 0, wx.ALL, 5) + btn_sizer.Add(self._select_none_btn, 0, wx.ALL, 5) # Layout for popup popup_sizer = wx.BoxSizer(wx.VERTICAL) - popup_sizer.Add(self.checklist, 1, wx.EXPAND | wx.ALL, 5) + popup_sizer.Add(self._checklist, 1, wx.EXPAND | wx.ALL, 5) popup_sizer.Add(btn_sizer, 0, wx.ALIGN_CENTER | wx.BOTTOM, 5) - self.popup.SetSizer(popup_sizer) + self._popup.SetSizer(popup_sizer) # Layout for the main widget sizer = wx.BoxSizer(wx.HORIZONTAL) - sizer.Add(self.dropdown_btn, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5) - sizer.Add(self.selection_info, 0, wx.ALIGN_CENTER_VERTICAL) + sizer.Add(self._dropdown_btn, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5) + sizer.Add(self._selection_info, 0, wx.ALIGN_CENTER_VERTICAL) self.SetSizer(sizer) def set_displayed_channels(self, displayed_channels: list[ChannelKey]): - self.displayed_channels = set(displayed_channels) + self._displayed_channels = set(displayed_channels) self._update_checklist() self._update_selection_info() - def add_channel(self, channel: ChannelKey): - if (type(channel) is int) or (type(channel) is str and channel.isdigit()): - if channel not in self.channels: - self.channels.append(channel) - self.displayed_channels.add(channel) + def add_channel(self, view: WxLutView): + if (type(view.channel) is int) or (type(view.channel) is str and view.channel.isdigit()): + if view.channel not in self._channels: + self._channels.append(view.channel) + self._displayed_channels.add(view.channel) + self._luts[view.channel] = view self._update_checklist() self._update_selection_info() def remove_channel(self, channel: ChannelKey): - self.channels = [ch for ch in self.channels if not ch == channel] - self.displayed_channels = { - ch for ch in self.displayed_channels if not ch == channel - } + self._channels = [ch for ch in self._channels if not ch == channel] + self._displayed_channels.discard(channel) + del self._luts[channel] self._update_checklist() self._update_selection_info() def _update_checklist(self): """Update the checklist with current channels.""" - self.checklist.Clear() - for channel in self.channels: - self.checklist.Append(str(channel)) - idx = self.checklist.FindString(str(channel)) - if channel in self.displayed_channels: - self.checklist.Check(idx) + self._checklist.Clear() + for channel in self._channels: + self._checklist.Append(str(channel)) + idx = self._checklist.FindString(str(channel)) + if channel in self._displayed_channels: + self._checklist.Check(idx) def _update_selection_info(self): """Update the selection info text.""" - if len(self.displayed_channels) == len(self.channels): - self.selection_info.SetLabel( - f"All channels displayed ({len(self.channels)})" + if len(self._displayed_channels) == len(self._channels): + self._selection_info.SetLabel( + f"All channels displayed ({len(self._channels)})" ) else: - self.selection_info.SetLabel( - f"{len(self.displayed_channels)} of {len(self.channels)} channels displayed" + self._selection_info.SetLabel( + f"{len(self._displayed_channels)} of {len(self._channels)} channels displayed" ) self.Layout() def _on_dropdown_clicked(self, evt): """Show the dropdown checklist.""" # Position the popup below the button - btn_size = self.dropdown_btn.GetSize() - btn_pos = self.dropdown_btn.GetPosition() + btn_size = self._dropdown_btn.GetSize() + btn_pos = self._dropdown_btn.GetPosition() popup_pos = self.ClientToScreen( wx.Point(btn_pos.x, btn_pos.y + btn_size.height) ) @@ -183,48 +188,55 @@ def _on_dropdown_clicked(self, evt): self._update_checklist() # Size the popup according to content - self.popup.SetSize(wx.Size(250, min(400, 50 + len(self.channels) * 22))) - self.checklist.SetSize(self.popup.GetClientSize()) - self.popup.Layout() + self._popup.SetSize(wx.Size(250, min(400, 50 + len(self._channels) * 22))) + self._checklist.SetSize(self._popup.GetClientSize()) + self._popup.Layout() # Show the popup - self.popup.SetPosition(popup_pos) - self.popup.Show(show=True) + self._popup.SetPosition(popup_pos) + self._popup.Show(show=True) def _on_popup_deactivate(self, evt): """Close the popup when it loses focus (user clicks outside).""" if not evt.GetActive(): - self.popup.Show(show=False) + self._popup.Show(show=False) def _on_selection_changed(self, evt): """Handle selection changes in the checklist.""" idx = evt.GetSelection() - channel = self.channels[idx] + channel = self._channels[idx] - if self.checklist.IsChecked(idx): - self.displayed_channels.add(channel) + if self._checklist.IsChecked(idx): + self._displayed_channels.add(channel) + self._luts[channel].set_display(True) else: - self.displayed_channels.discard(channel) + self._displayed_channels.discard(channel) + self._luts[channel].set_display(False) self._update_selection_info() - self.selectionChanged.emit(self.displayed_channels) + self.selectionChanged.emit() def _on_select_all(self, evt): """Select all channels.""" - for i in range(self.checklist.GetCount()): - self.checklist.Check(i, True) - self.displayed_channels = set(self.channels) + for i in range(self._checklist.GetCount()): + self._checklist.Check(i, True) + self._displayed_channels.clear() + for channel in self._channels: + self._luts[channel].set_display(True) + self._displayed_channels.add(channel) self._update_selection_info() - self.selectionChanged.emit(self.displayed_channels) + self.selectionChanged.emit() def _on_select_none(self, evt): """Deselect all channels.""" - for i in range(self.checklist.GetCount()): - self.checklist.Check(i, False) - self.displayed_channels = set() + for i in range(self._checklist.GetCount()): + self._checklist.Check(i, False) + self._displayed_channels.clear() + for channel in self._channels: + self._luts[channel].set_display(False) self._update_selection_info() - self.selectionChanged.emit(self.displayed_channels) + self.selectionChanged.emit() class _WxLUTWidget(wx.Panel): @@ -314,6 +326,12 @@ def __init__(self, parent: wx.Window) -> None: self.Layout() +# VISIBLE: LUT controls are displayed and channel is included in rendered raster +# DISPLAYED: LUT controls are displayed and channel is not included in rendered raster +# HIDDEN: LUT controls are not displayed and channel is not included in rendered raster +_DisplayStatus = Enum("DisplayStatus", "VISIBLE DISPLAYED HIDDEN") + + class WxLutView(LutView): # NB: In practice this will be a ChannelKey but Unions not allowed here. histogramRequested = psygnal.Signal(object) @@ -322,8 +340,9 @@ class WxLutView(LutView): def __init__(self, parent: wx.Window, channel: ChannelKey = None) -> None: super().__init__() self._wxwidget = wdg = _WxLUTWidget(parent) - self._channel = channel + self.channel = channel self.histogram: HistogramCanvas | None = None + self._display_status: _DisplayStatus = _DisplayStatus.VISIBLE # TODO: use emit_fast wdg.visible.Bind(wx.EVT_CHECKBOX, self._on_visible_changed) wdg.cmap.Bind(wx.EVT_COMBOBOX, self._on_cmap_changed) @@ -377,7 +396,7 @@ def _on_histogram_requested(self, event: wx.CommandEvent) -> None: self._show_histogram(toggled) if self.histogram is None: - self.histogramRequested.emit(self._channel) + self.histogramRequested.emit(self.channel) def _on_log_btn_toggled(self, event: wx.CommandEvent) -> None: toggled = self._wxwidget.log_btn.GetValue() @@ -464,16 +483,41 @@ def set_clim_bounds( self._wxwidget.clims.SetMin(mi) self._wxwidget.clims.SetMax(ma) + def _display_hidden(self): + self._display_status = _DisplayStatus.HIDDEN + self._wxwidget.Hide() + + def _display_enable(self): + self._display_status = _DisplayStatus.DISPLAYED + self._wxwidget.Show() + + def _display_visible(self): + self._display_status = _DisplayStatus.VISIBLE + self._wxwidget.visible.SetValue(True) + def set_channel_visible(self, visible: bool) -> None: - self._wxwidget.visible.SetValue(visible) + if visible and self._display_status != _DisplayStatus.HIDDEN: + self._display_visible() + # elif visible do nothing + elif not visible: + self._wxwidget.visible.SetValue(False) def set_visible(self, visible: bool) -> None: - if visible: + if visible and self._display_status != _DisplayStatus.HIDDEN: self._wxwidget.Show() - else: + # elif visible do nothing + elif not visible: self._wxwidget.Hide() self.lutUpdated.emit() + def set_display(self, display: bool) -> None: + if display and self._display_status == _DisplayStatus.HIDDEN: + self._display_enable() + # elif display do nothing + elif not display: + self._display_hidden() + self.lutUpdated.emit() + def close(self) -> None: self._wxwidget.Close() self.lutUpdated.emit() @@ -685,7 +729,7 @@ def __init__( wdg.ndims_btn.Bind(wx.EVT_TOGGLEBUTTON, self._on_ndims_toggled) wdg.add_roi_btn.Bind(wx.EVT_TOGGLEBUTTON, self._on_add_roi_toggled) - wdg.lut_selector.selectionChanged.connect(self._on_lut_selection_changed) + wdg.lut_selector.selectionChanged.connect(self._wxwidget.Layout) def _on_channel_mode_changed(self, event: wx.CommandEvent) -> None: mode = self._wxwidget.channel_mode_combo.GetValue() @@ -718,16 +762,6 @@ def _on_add_roi_toggled(self, event: wx.CommandEvent) -> None: InteractionMode.CREATE_ROI if create_roi else InteractionMode.PAN_ZOOM ) - def _on_lut_selection_changed(self, displayed_channels: list[ChannelKey]): - """Handle changes in displayed LUT selection.""" - for channel, lut_view in self._luts.items(): - if channel in displayed_channels: - lut_view.set_visible(True) - else: - lut_view.set_visible(False) - - self._wxwidget.Layout() - def visible_axes(self) -> Sequence[AxisKey]: return self._visible_axes # no widget to control this yet @@ -758,7 +792,7 @@ def add_lut_view(self, channel: ChannelKey) -> WxLutView: view.histogramRequested.connect(self.histogramRequested) view.lutUpdated.connect(self._wxwidget.update_lut_scroll_size) - self._lut_selector().add_channel(channel) + self._lut_selector().add_channel(view) self._wxwidget.update_lut_scroll_size() return view From 9f5bec0bdc99ecb66a85affe813e8e7b51972fc5 Mon Sep 17 00:00:00 2001 From: Nodar Gogoberidze Date: Fri, 16 May 2025 16:21:26 -0400 Subject: [PATCH 12/23] Set visible off when not displayed --- src/ndv/views/_wx/_array_view.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/ndv/views/_wx/_array_view.py b/src/ndv/views/_wx/_array_view.py index c2127f0b..5deb1287 100644 --- a/src/ndv/views/_wx/_array_view.py +++ b/src/ndv/views/_wx/_array_view.py @@ -486,6 +486,8 @@ def set_clim_bounds( def _display_hidden(self): self._display_status = _DisplayStatus.HIDDEN self._wxwidget.Hide() + self._wxwidget.visible.SetValue(False) + self._on_visible_changed(None) def _display_enable(self): self._display_status = _DisplayStatus.DISPLAYED @@ -505,18 +507,20 @@ def set_channel_visible(self, visible: bool) -> None: def set_visible(self, visible: bool) -> None: if visible and self._display_status != _DisplayStatus.HIDDEN: self._wxwidget.Show() + self.lutUpdated.emit() # elif visible do nothing elif not visible: self._wxwidget.Hide() - self.lutUpdated.emit() + self.lutUpdated.emit() def set_display(self, display: bool) -> None: if display and self._display_status == _DisplayStatus.HIDDEN: self._display_enable() + self.lutUpdated.emit() # elif display do nothing elif not display: self._display_hidden() - self.lutUpdated.emit() + self.lutUpdated.emit() def close(self) -> None: self._wxwidget.Close() From c59ce25fb6e53cc64e2b00e69eab956a971c8788 Mon Sep 17 00:00:00 2001 From: Nodar Gogoberidze Date: Fri, 16 May 2025 17:06:14 -0400 Subject: [PATCH 13/23] Show display options only after thresh --- src/ndv/views/_wx/_array_view.py | 38 +++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/src/ndv/views/_wx/_array_view.py b/src/ndv/views/_wx/_array_view.py index 5deb1287..046e7f76 100644 --- a/src/ndv/views/_wx/_array_view.py +++ b/src/ndv/views/_wx/_array_view.py @@ -654,13 +654,23 @@ def __init__(self, canvas_widget: wx.Window, parent: wx.Window = None): self.add_roi_btn = wx.ToggleButton(self, label="ROI", size=(40, -1)) _add_icon(self.add_roi_btn, "mdi:vector-rectangle") - # Add LUT channel selector - self.lut_selector = _LutChannelSelector(self) + # how many luts need to be present before lut toolbar appears + self._toolbar_display_thresh = 7 # Toolbar for LUT management - self.lut_toolbar = wx.BoxSizer(wx.HORIZONTAL) - self.lut_toolbar.Add(self.lut_selector, 0, wx.EXPAND | wx.ALL, 5) - self.lut_toolbar.AddStretchSpacer() + self._lut_toolbar_panel = wx.Panel(self) + self._lut_toolbar = wx.BoxSizer(wx.HORIZONTAL) + self._lut_toolbar_panel.SetSizer(self._lut_toolbar) + + # Add LUT channel selector + self.lut_selector = _LutChannelSelector(self._lut_toolbar_panel) + + self._lut_toolbar.Add(self.lut_selector, 0, wx.EXPAND | wx.ALL, 5) + self._lut_toolbar.AddStretchSpacer() + + self.lut_selector.Hide() + self._lut_toolbar_panel.Hide() + self._lut_toolbar_shown = False # Create a scrolled panel for LUTs self.luts_scroll = wx.ScrolledWindow(self, style=wx.VSCROLL) @@ -687,7 +697,7 @@ def __init__(self, canvas_widget: wx.Window, parent: wx.Window = None): inner.Add(self._canvas, 1, wx.EXPAND | wx.ALL) inner.Add(self._hover_info_label, 0, wx.EXPAND | wx.BOTTOM) inner.Add(self.dims_sliders, 0, wx.EXPAND | wx.BOTTOM) - inner.Add(self.lut_toolbar, 0, wx.EXPAND | wx.BOTTOM, 5) + inner.Add(self._lut_toolbar_panel, 0, wx.EXPAND | wx.BOTTOM, 5) inner.Add(self.luts_scroll, 0, wx.EXPAND | wx.BOTTOM, 5) inner.Add(self._btns, 0, wx.EXPAND) self._inner_sizer = inner @@ -710,6 +720,16 @@ def update_lut_scroll_size(self): self.luts_scroll.FitInside() self.Layout() + def toggle_lut_toolbar(self, show: bool) -> None: + if show and not self._lut_toolbar_shown: + self.lut_selector.Show() + self._lut_toolbar_panel.Show() + self._lut_toolbar_shown = True + elif not show and self._lut_toolbar_shown: + self.lut_selector.Hide() + self._lut_toolbar_panel.Hide() + self._lut_toolbar_shown = False + self._inner_sizer.Layout() class WxArrayView(ArrayView): def __init__( @@ -799,6 +819,9 @@ def add_lut_view(self, channel: ChannelKey) -> WxLutView: self._lut_selector().add_channel(view) self._wxwidget.update_lut_scroll_size() + if len(self._luts) >= self._wxwidget._toolbar_display_thresh: + self._wxwidget.toggle_lut_toolbar(True) + return view # TODO: Fix type @@ -827,6 +850,9 @@ def remove_lut_view(self, lut: LutView) -> None: self._lut_selector().remove_channel(channel) self._wxwidget.update_lut_scroll_size() + if len(self._luts) < self._wxwidget._toolbar_display_thresh: + self._wxwidget.toggle_lut_toolbar(False) + def create_sliders(self, coords: Mapping[Hashable, Sequence]) -> None: self._wxwidget.dims_sliders.create_sliders(coords) self._wxwidget.Layout() From 26e186f1cc0975efd6f79b98a884933004c6400c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 16 May 2025 21:08:12 +0000 Subject: [PATCH 14/23] style(pre-commit.ci): auto fixes [...] --- src/ndv/views/_wx/_array_view.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/ndv/views/_wx/_array_view.py b/src/ndv/views/_wx/_array_view.py index 046e7f76..0407ec2b 100644 --- a/src/ndv/views/_wx/_array_view.py +++ b/src/ndv/views/_wx/_array_view.py @@ -1,9 +1,9 @@ from __future__ import annotations import warnings +from enum import Enum from pathlib import Path from sys import version_info -from enum import Enum from typing import TYPE_CHECKING, cast import psygnal @@ -115,7 +115,9 @@ def __init__(self, parent: wx.Window, channels: None | list[ChannelKey] = None): # Select All / None buttons btn_sizer = wx.BoxSizer(wx.HORIZONTAL) self._select_all_btn = wx.Button(self._popup, label="Select All", size=(80, -1)) - self._select_none_btn = wx.Button(self._popup, label="Select None", size=(80, -1)) + self._select_none_btn = wx.Button( + self._popup, label="Select None", size=(80, -1) + ) self._select_all_btn.Bind(wx.EVT_BUTTON, self._on_select_all) self._select_none_btn.Bind(wx.EVT_BUTTON, self._on_select_none) btn_sizer.Add(self._select_all_btn, 0, wx.ALL, 5) @@ -139,7 +141,9 @@ def set_displayed_channels(self, displayed_channels: list[ChannelKey]): self._update_selection_info() def add_channel(self, view: WxLutView): - if (type(view.channel) is int) or (type(view.channel) is str and view.channel.isdigit()): + if (type(view.channel) is int) or ( + type(view.channel) is str and view.channel.isdigit() + ): if view.channel not in self._channels: self._channels.append(view.channel) self._displayed_channels.add(view.channel) @@ -731,6 +735,7 @@ def toggle_lut_toolbar(self, show: bool) -> None: self._lut_toolbar_shown = False self._inner_sizer.Layout() + class WxArrayView(ArrayView): def __init__( self, From cb89ae7aa86b9bfea9edded1b269c5e57b0df576 Mon Sep 17 00:00:00 2001 From: Nodar Gogoberidze Date: Fri, 16 May 2025 17:19:55 -0400 Subject: [PATCH 15/23] Fix: no built-in for signal introspection --- src/ndv/views/_wx/_array_view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ndv/views/_wx/_array_view.py b/src/ndv/views/_wx/_array_view.py index 0407ec2b..595923aa 100644 --- a/src/ndv/views/_wx/_array_view.py +++ b/src/ndv/views/_wx/_array_view.py @@ -758,7 +758,7 @@ def __init__( wdg.ndims_btn.Bind(wx.EVT_TOGGLEBUTTON, self._on_ndims_toggled) wdg.add_roi_btn.Bind(wx.EVT_TOGGLEBUTTON, self._on_add_roi_toggled) - wdg.lut_selector.selectionChanged.connect(self._wxwidget.Layout) + wdg.lut_selector.selectionChanged.connect(lambda: self._wxwidget.Layout()) def _on_channel_mode_changed(self, event: wx.CommandEvent) -> None: mode = self._wxwidget.channel_mode_combo.GetValue() From 857aee6887988d738660b02dd61e1ec9b3846f50 Mon Sep 17 00:00:00 2001 From: Nodar Gogoberidze Date: Mon, 19 May 2025 11:54:01 -0400 Subject: [PATCH 16/23] Add initial display option tests --- tests/views/_wx/test_array_view.py | 73 ++++++++++++++++++++++++++---- 1 file changed, 64 insertions(+), 9 deletions(-) diff --git a/tests/views/_wx/test_array_view.py b/tests/views/_wx/test_array_view.py index f5021cb1..4637b7b5 100644 --- a/tests/views/_wx/test_array_view.py +++ b/tests/views/_wx/test_array_view.py @@ -7,6 +7,7 @@ from ndv.models._data_display_model import _ArrayDataDisplayModel from ndv.models._viewer_model import ArrayViewerModel +from ndv.models import ChannelMode from ndv.views._app import get_histogram_canvas_class from ndv.views._wx._array_view import WxArrayView @@ -18,6 +19,16 @@ def viewer(wxapp: wx.App) -> WxArrayView: return viewer +def _processEvent(wxapp: wx.App, evt: wx.PyEventBinder, wdg: wx.Control) -> None: + ev = wx.PyCommandEvent(evt.typeId, wdg.GetId()) + wx.PostEvent(wdg.GetEventHandler(), ev) + # Borrowed from: + # https://github.com/wxWidgets/Phoenix/blob/master/unittests/wtc.py#L41 + evtLoop = wxapp.GetTraits().CreateEventLoop() + wx.EventLoopActivator(evtLoop) + evtLoop.YieldFor(wx.EVT_CATEGORY_ALL) + + def test_array_options(viewer: WxArrayView) -> None: wxwdg = viewer._wxwidget wxwdg.Show() @@ -45,14 +56,6 @@ def test_array_options(viewer: WxArrayView) -> None: def test_histogram(wxapp: wx.App, viewer: WxArrayView) -> None: - def processEvent(evt: wx.PyEventBinder, wdg: wx.Control) -> None: - ev = wx.PyCommandEvent(evt.typeId, wdg.GetId()) - wx.PostEvent(wdg.GetEventHandler(), ev) - # Borrowed from: - # https://github.com/wxWidgets/Phoenix/blob/master/unittests/wtc.py#L41 - evtLoop = wxapp.GetTraits().CreateEventLoop() - wx.EventLoopActivator(evtLoop) - evtLoop.YieldFor(wx.EVT_CATEGORY_ALL) channel = None lut = viewer._luts[channel] @@ -62,7 +65,7 @@ def processEvent(evt: wx.PyEventBinder, wdg: wx.Control) -> None: histogram_mock = Mock() viewer.histogramRequested.connect(histogram_mock) btn.SetValue(True) - processEvent(wx.EVT_TOGGLEBUTTON, btn) + _processEvent(wxapp, wx.EVT_TOGGLEBUTTON, btn) histogram_mock.assert_called_once_with(channel) # Test adding the histogram widget puts it on the relevant lut @@ -70,3 +73,55 @@ def processEvent(evt: wx.PyEventBinder, wdg: wx.Control) -> None: histogram = get_histogram_canvas_class()() # will raise if not supported viewer.add_histogram(channel, histogram) assert len(lut._wxwidget._histogram_sizer.GetChildren()) == 2 + + +# == Tests for display of channels == + +def test_display_options_visibility(wxapp: wx.App, viewer: WxArrayView) -> None: + # display options button should appear only after thresh is reached + # -2 to account for add_lut_view(None) in fixture + for ch in range(viewer._wxwidget._toolbar_display_thresh - 2): + viewer.add_lut_view(ch) + + assert viewer._wxwidget.lut_selector.IsEnabled() + + assert not viewer._wxwidget._lut_toolbar_shown + assert not viewer._wxwidget.lut_selector.IsShown() + assert not viewer._wxwidget._lut_toolbar_panel.IsShown() + + viewer.add_lut_view(ch + 1) + + assert viewer._wxwidget._lut_toolbar_shown + assert viewer._wxwidget.lut_selector.IsShown() + assert viewer._wxwidget._lut_toolbar_panel.IsShown() + +def test_display_options_selection(wxapp: wx.App, viewer: WxArrayView) -> None: + # display options button should appear after thresh reached + num_channels = viewer._wxwidget._toolbar_display_thresh - 1 + for ch in range(num_channels): + viewer.add_lut_view(ch) + + assert len(viewer._wxwidget.luts.Children) == len(viewer._luts) + + # all channels should initially be displayed + for ch, lut_view in viewer._luts.items(): + if type(ch) is int or (type(ch) is str and ch.isdigit()): + assert lut_view._wxwidget.IsShown() + + # display off for a single channel + checklist = viewer._wxwidget.lut_selector._checklist + checklist.Check(0, False) + _processEvent(wxapp, wx.EVT_CHECKLISTBOX, checklist) + + assert not checklist.IsChecked(0) + assert not viewer._luts[0]._wxwidget.IsShown() + + #channel_mode = viewer._wxwidget.channel_mode_combo + #viewer.set_channel_mode(ChannelMode.GRAYSCALE) + #_processEvent(wxapp, wx.EVT_COMBOBOX, channel_mode) + + ## all channels should be hidden + #for ch, lut_view in viewer._luts.items(): + # if type(ch) is int or (type(ch) is str and ch.isdigit()): + # print('checking', ch) + # assert not lut_view._wxwidget.IsShown() From 97601f20a5c4e63271d68bb261412780cad20eb2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 19 May 2025 16:12:46 +0000 Subject: [PATCH 17/23] style(pre-commit.ci): auto fixes [...] --- tests/views/_wx/test_array_view.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/views/_wx/test_array_view.py b/tests/views/_wx/test_array_view.py index 4637b7b5..eaecc5d9 100644 --- a/tests/views/_wx/test_array_view.py +++ b/tests/views/_wx/test_array_view.py @@ -7,7 +7,6 @@ from ndv.models._data_display_model import _ArrayDataDisplayModel from ndv.models._viewer_model import ArrayViewerModel -from ndv.models import ChannelMode from ndv.views._app import get_histogram_canvas_class from ndv.views._wx._array_view import WxArrayView @@ -56,7 +55,6 @@ def test_array_options(viewer: WxArrayView) -> None: def test_histogram(wxapp: wx.App, viewer: WxArrayView) -> None: - channel = None lut = viewer._luts[channel] btn = lut._wxwidget.histogram_btn @@ -77,6 +75,7 @@ def test_histogram(wxapp: wx.App, viewer: WxArrayView) -> None: # == Tests for display of channels == + def test_display_options_visibility(wxapp: wx.App, viewer: WxArrayView) -> None: # display options button should appear only after thresh is reached # -2 to account for add_lut_view(None) in fixture @@ -95,6 +94,7 @@ def test_display_options_visibility(wxapp: wx.App, viewer: WxArrayView) -> None: assert viewer._wxwidget.lut_selector.IsShown() assert viewer._wxwidget._lut_toolbar_panel.IsShown() + def test_display_options_selection(wxapp: wx.App, viewer: WxArrayView) -> None: # display options button should appear after thresh reached num_channels = viewer._wxwidget._toolbar_display_thresh - 1 @@ -116,12 +116,12 @@ def test_display_options_selection(wxapp: wx.App, viewer: WxArrayView) -> None: assert not checklist.IsChecked(0) assert not viewer._luts[0]._wxwidget.IsShown() - #channel_mode = viewer._wxwidget.channel_mode_combo - #viewer.set_channel_mode(ChannelMode.GRAYSCALE) - #_processEvent(wxapp, wx.EVT_COMBOBOX, channel_mode) + # channel_mode = viewer._wxwidget.channel_mode_combo + # viewer.set_channel_mode(ChannelMode.GRAYSCALE) + # _processEvent(wxapp, wx.EVT_COMBOBOX, channel_mode) ## all channels should be hidden - #for ch, lut_view in viewer._luts.items(): + # for ch, lut_view in viewer._luts.items(): # if type(ch) is int or (type(ch) is str and ch.isdigit()): # print('checking', ch) # assert not lut_view._wxwidget.IsShown() From 6796096a54f1e654b70678485f3b5485db465b5d Mon Sep 17 00:00:00 2001 From: Nodar Gogoberidze Date: Mon, 19 May 2025 12:47:33 -0400 Subject: [PATCH 18/23] Fix merge conflict remnants --- src/ndv/views/_wx/_array_view.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ndv/views/_wx/_array_view.py b/src/ndv/views/_wx/_array_view.py index 432eab70..ba37633a 100644 --- a/src/ndv/views/_wx/_array_view.py +++ b/src/ndv/views/_wx/_array_view.py @@ -347,7 +347,7 @@ def __init__( ) -> None: super().__init__() self._wxwidget = wdg = _WxLUTWidget(parent, default_luts) - self._channel = channel + self.channel = channel self.histogram: HistogramCanvas | None = None self._display_status: _DisplayStatus = _DisplayStatus.VISIBLE # TODO: use emit_fast @@ -811,7 +811,7 @@ def _lut_selector(self) -> _LutChannelSelector: return self._wxwidget.lut_selector def add_lut_view(self, channel: ChannelKey) -> WxLutView: - scrollwdg = self.frontend_widget() + scrollwdg = self._lut_area() view = ( WxRGBView(scrollwdg, channel) if channel == "RGB" From 4aacff09bac17f257e668d520b8e371dcdcecfb3 Mon Sep 17 00:00:00 2001 From: Nodar Gogoberidze Date: Mon, 19 May 2025 15:03:18 -0400 Subject: [PATCH 19/23] Remvoe unused method --- src/ndv/views/_wx/_array_view.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/ndv/views/_wx/_array_view.py b/src/ndv/views/_wx/_array_view.py index ba37633a..f1f31da6 100644 --- a/src/ndv/views/_wx/_array_view.py +++ b/src/ndv/views/_wx/_array_view.py @@ -134,11 +134,6 @@ def __init__(self, parent: wx.Window, channels: None | list[ChannelKey] = None): sizer.Add(self._selection_info, 0, wx.ALIGN_CENTER_VERTICAL) self.SetSizer(sizer) - def set_displayed_channels(self, displayed_channels: list[ChannelKey]): - self._displayed_channels = set(displayed_channels) - self._update_checklist() - self._update_selection_info() - def add_channel(self, view: WxLutView): if (type(view.channel) is int) or ( type(view.channel) is str and view.channel.isdigit() From f3d83606754ba39dd3c025005d7ea7a3ea712e76 Mon Sep 17 00:00:00 2001 From: Nodar Gogoberidze Date: Mon, 19 May 2025 15:24:39 -0400 Subject: [PATCH 20/23] Put channel removal under test, and fix 0 check bug --- src/ndv/views/_wx/_array_view.py | 2 +- tests/views/_wx/test_array_view.py | 20 +++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/ndv/views/_wx/_array_view.py b/src/ndv/views/_wx/_array_view.py index f1f31da6..bec759df 100644 --- a/src/ndv/views/_wx/_array_view.py +++ b/src/ndv/views/_wx/_array_view.py @@ -847,7 +847,7 @@ def remove_lut_view(self, lut: LutView) -> None: wxwdg.Destroy() # Remove from our dictionaries - if channel_to_remove: + if channel_to_remove is not None: del self._luts[channel_to_remove] self._lut_selector().remove_channel(channel) diff --git a/tests/views/_wx/test_array_view.py b/tests/views/_wx/test_array_view.py index eaecc5d9..72513a44 100644 --- a/tests/views/_wx/test_array_view.py +++ b/tests/views/_wx/test_array_view.py @@ -123,5 +123,23 @@ def test_display_options_selection(wxapp: wx.App, viewer: WxArrayView) -> None: ## all channels should be hidden # for ch, lut_view in viewer._luts.items(): # if type(ch) is int or (type(ch) is str and ch.isdigit()): - # print('checking', ch) # assert not lut_view._wxwidget.IsShown() + +def test_removed_channels(wxapp: wx.App, viewer: WxArrayView) -> None: + # display options button should appear only after thresh is reached + # -2 to account for add_lut_view(None) in fixture + for ch in range(viewer._wxwidget._toolbar_display_thresh - 1): + viewer.add_lut_view(ch) + + for ch in range(viewer._wxwidget._toolbar_display_thresh - 1): + lut_view = viewer._luts[ch] + viewer.remove_lut_view(lut_view) + + assert not viewer._wxwidget._lut_toolbar_shown + assert not viewer._wxwidget.lut_selector.IsShown() + assert not viewer._wxwidget._lut_toolbar_panel.IsShown() + + # len == 1 to account for the None key + assert len(viewer._luts) == 1 + assert len(viewer._wxwidget.luts.Children) == 1 + assert len(viewer._wxwidget.lut_selector._checklist.Children) == 0 From d375414efaf1fb9f13a08e2f5a4dceed05eaea86 Mon Sep 17 00:00:00 2001 From: Nodar Gogoberidze Date: Mon, 19 May 2025 16:08:38 -0400 Subject: [PATCH 21/23] Test channel selection popup --- tests/views/_wx/test_array_view.py | 32 +++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/tests/views/_wx/test_array_view.py b/tests/views/_wx/test_array_view.py index 72513a44..0652c2c6 100644 --- a/tests/views/_wx/test_array_view.py +++ b/tests/views/_wx/test_array_view.py @@ -18,8 +18,18 @@ def viewer(wxapp: wx.App) -> WxArrayView: return viewer -def _processEvent(wxapp: wx.App, evt: wx.PyEventBinder, wdg: wx.Control) -> None: - ev = wx.PyCommandEvent(evt.typeId, wdg.GetId()) +def _processEvent( + wxapp: wx.App, + evt: wx.PyEventBinder, + wdg: wx.Control, + **kwargs +) -> None: + if evt == wx.EVT_ACTIVATE: + active = kwargs.get("active", True) + ev = wx.ActivateEvent(eventType=evt.typeId, active=active) + else: + ev = wx.PyCommandEvent(evt.typeId, wdg.GetId()) + wx.PostEvent(wdg.GetEventHandler(), ev) # Borrowed from: # https://github.com/wxWidgets/Phoenix/blob/master/unittests/wtc.py#L41 @@ -127,7 +137,6 @@ def test_display_options_selection(wxapp: wx.App, viewer: WxArrayView) -> None: def test_removed_channels(wxapp: wx.App, viewer: WxArrayView) -> None: # display options button should appear only after thresh is reached - # -2 to account for add_lut_view(None) in fixture for ch in range(viewer._wxwidget._toolbar_display_thresh - 1): viewer.add_lut_view(ch) @@ -143,3 +152,20 @@ def test_removed_channels(wxapp: wx.App, viewer: WxArrayView) -> None: assert len(viewer._luts) == 1 assert len(viewer._wxwidget.luts.Children) == 1 assert len(viewer._wxwidget.lut_selector._checklist.Children) == 0 + +def test_dropdown_popup(wxapp: wx.App, viewer: WxArrayView) -> None: + for ch in range(viewer._wxwidget._toolbar_display_thresh - 1): + viewer.add_lut_view(ch) + + assert not viewer._wxwidget.lut_selector._popup.IsShown() + + ch_selection_dropdown = viewer._wxwidget.lut_selector._dropdown_btn + _processEvent(wxapp, wx.EVT_BUTTON, ch_selection_dropdown) + + assert viewer._wxwidget.lut_selector._popup.IsShown() + + _processEvent( + wxapp, wx.EVT_ACTIVATE, viewer._wxwidget.lut_selector._popup, active=False + ) + + assert not viewer._wxwidget.lut_selector._popup.IsShown() From 66182ce760d7d2d67c197ee094dd3c6f09aa0c3b Mon Sep 17 00:00:00 2001 From: Nodar Gogoberidze Date: Mon, 19 May 2025 16:25:02 -0400 Subject: [PATCH 22/23] Test select [all|none] --- tests/views/_wx/test_array_view.py | 33 +++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/tests/views/_wx/test_array_view.py b/tests/views/_wx/test_array_view.py index 0652c2c6..8390830e 100644 --- a/tests/views/_wx/test_array_view.py +++ b/tests/views/_wx/test_array_view.py @@ -118,14 +118,22 @@ def test_display_options_selection(wxapp: wx.App, viewer: WxArrayView) -> None: if type(ch) is int or (type(ch) is str and ch.isdigit()): assert lut_view._wxwidget.IsShown() - # display off for a single channel checklist = viewer._wxwidget.lut_selector._checklist + + # display off for a single channel checklist.Check(0, False) _processEvent(wxapp, wx.EVT_CHECKLISTBOX, checklist) assert not checklist.IsChecked(0) assert not viewer._luts[0]._wxwidget.IsShown() + # display on again for a single channel + checklist.Check(0, True) + _processEvent(wxapp, wx.EVT_CHECKLISTBOX, checklist) + + assert checklist.IsChecked(0) + assert viewer._luts[0]._wxwidget.IsShown() + # channel_mode = viewer._wxwidget.channel_mode_combo # viewer.set_channel_mode(ChannelMode.GRAYSCALE) # _processEvent(wxapp, wx.EVT_COMBOBOX, channel_mode) @@ -169,3 +177,26 @@ def test_dropdown_popup(wxapp: wx.App, viewer: WxArrayView) -> None: ) assert not viewer._wxwidget.lut_selector._popup.IsShown() + +def test_none_all(wxapp: wx.App, viewer: WxArrayView) -> None: + for ch in range(viewer._wxwidget._toolbar_display_thresh - 1): + viewer.add_lut_view(ch) + + none_btn = viewer._wxwidget.lut_selector._select_none_btn + all_btn = viewer._wxwidget.lut_selector._select_all_btn + + # select none + _processEvent(wxapp, wx.EVT_BUTTON, none_btn) + + # all channels should be hidden + for ch, lut_view in viewer._luts.items(): + if type(ch) is int or (type(ch) is str and ch.isdigit()): + assert not lut_view._wxwidget.IsShown() + + # select all + _processEvent(wxapp, wx.EVT_BUTTON, all_btn) + + # all channels should be displayed + for ch, lut_view in viewer._luts.items(): + if type(ch) is int or (type(ch) is str and ch.isdigit()): + assert lut_view._wxwidget.IsShown() From 774c7c7395780c89d1311a55b8ce646cba3eebc7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 19 May 2025 20:25:52 +0000 Subject: [PATCH 23/23] style(pre-commit.ci): auto fixes [...] --- tests/views/_wx/test_array_view.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/views/_wx/test_array_view.py b/tests/views/_wx/test_array_view.py index 8390830e..121d28c6 100644 --- a/tests/views/_wx/test_array_view.py +++ b/tests/views/_wx/test_array_view.py @@ -19,10 +19,7 @@ def viewer(wxapp: wx.App) -> WxArrayView: def _processEvent( - wxapp: wx.App, - evt: wx.PyEventBinder, - wdg: wx.Control, - **kwargs + wxapp: wx.App, evt: wx.PyEventBinder, wdg: wx.Control, **kwargs ) -> None: if evt == wx.EVT_ACTIVATE: active = kwargs.get("active", True) @@ -143,6 +140,7 @@ def test_display_options_selection(wxapp: wx.App, viewer: WxArrayView) -> None: # if type(ch) is int or (type(ch) is str and ch.isdigit()): # assert not lut_view._wxwidget.IsShown() + def test_removed_channels(wxapp: wx.App, viewer: WxArrayView) -> None: # display options button should appear only after thresh is reached for ch in range(viewer._wxwidget._toolbar_display_thresh - 1): @@ -161,6 +159,7 @@ def test_removed_channels(wxapp: wx.App, viewer: WxArrayView) -> None: assert len(viewer._wxwidget.luts.Children) == 1 assert len(viewer._wxwidget.lut_selector._checklist.Children) == 0 + def test_dropdown_popup(wxapp: wx.App, viewer: WxArrayView) -> None: for ch in range(viewer._wxwidget._toolbar_display_thresh - 1): viewer.add_lut_view(ch) @@ -178,6 +177,7 @@ def test_dropdown_popup(wxapp: wx.App, viewer: WxArrayView) -> None: assert not viewer._wxwidget.lut_selector._popup.IsShown() + def test_none_all(wxapp: wx.App, viewer: WxArrayView) -> None: for ch in range(viewer._wxwidget._toolbar_display_thresh - 1): viewer.add_lut_view(ch)