diff --git a/src-ui-wx/setup/ControllerModelDialog.cpp b/src-ui-wx/setup/ControllerModelDialog.cpp index 5d6fe61fd7..9a1e6d6890 100644 --- a/src-ui-wx/setup/ControllerModelDialog.cpp +++ b/src-ui-wx/setup/ControllerModelDialog.cpp @@ -257,6 +257,25 @@ class BaseCMObject const static int STYLE_STRINGS = 2; const static int STYLE_CHANNELS = 4; const static int STYLE_LABEL = 8; + + // Live visualizer filter - lowercase, empty means "no filter, show + // everything at full opacity". Set by the dialog's filter SearchCtrl on + // every keystroke. Read by ModelCMObject::Draw at paint time to decide + // whether to dim the model (non-matching rows fade to stay spatially + // present but visually de-emphasized). Static so Draw can read it + // without plumbing an extra parameter through the whole call chain. + // + // Match is a per-call case-insensitive substring test. The filter is + // already lowercased (by the SearchCtrl handler) so only the candidate + // name needs lowercasing here. A typical controller has tens of models, + // not thousands - the per-frame cost of Lower() on model names is + // negligible compared to the paint itself, so no separate cache. + static wxString _visualizerFilterLower; + static bool MatchesVisualizerFilter(const std::string& name) { + if (_visualizerFilterLower.IsEmpty()) return true; + return wxString(name).Lower().Contains(_visualizerFilterLower); + } + enum class HITLOCATION { NONE, LEFT, RIGHT, @@ -371,6 +390,10 @@ class BaseCMObject } }; +// Static member definition for BaseCMObject::_visualizerFilterLower. +// Initially empty (no filter applied). +wxString BaseCMObject::_visualizerFilterLower; + class PortCMObject : public BaseCMObject { public: @@ -1381,6 +1404,24 @@ class ModelCMObject : public BaseCMObject Model* m = _mm->GetModel(_name); UDControllerPortModel* udcpm = GetUDModel(); + // If a visualizer filter is active and this model doesn't match, paint + // it with a dimmed / de-emphasized palette so the controller layout + // stays spatially intact but the user's eye is drawn to the matching + // models. + // + // Scope: the SAME ModelCMObject::Draw is used by both panes of this + // dialog. The left (controller) pane calls Draw with a positive + // portMargin (space reserved for port labels). The right (available + // models) pane calls Draw with portMargin==0 - so we gate dimming + // on portMargin>0 to keep the right pane's filter (TextCtrl_ModelFilter) + // completely independent from the visualizer filter. + // + // Printing paths also pass portMargin==0, so they're naturally + // excluded. The previous !_dragging check was dead code - drag state + // is tracked by ControllerModelDialog::_dragging (a pointer) not by + // BaseCMObject::_dragging (a bool that's never set). + const bool dimForFilter = portMargin > 0 && !MatchesVisualizerFilter(_name); + if (!border) { dc.SetPen(*wxTRANSPARENT_PEN); } else { @@ -1417,6 +1458,41 @@ class ModelCMObject : public BaseCMObject dc.SetPen(wxPen(dc.GetPen().GetColour(), 3)); } + // Apply the dimming overrides LAST so they win over brush/pen choices + // above. Solid colours (not alpha) so the visual is consistent across + // every wx backend we ship, and derived from the active theme so + // "dimmed" truly means less prominent in BOTH light mode (pale grey + // approaching the light background) and dark mode (dark grey + // approaching the dark background). The previous fixed 240/200/170 + // palette looked right in light mode but became MORE prominent than + // normal items in dark mode. + if (dimForFilter) { + const wxColour bg = __backgroundBrush.GetColour(); + const wxColour fg = __textForeground; + auto blend = [](unsigned char bc, unsigned char fc, int fgWeightPct) -> unsigned char { + return static_cast( + (bc * (100 - fgWeightPct) + fc * fgWeightPct) / 100); + }; + // Dark mode needs a little more foreground weight to stay + // legible since the starting point is near-black. + const bool dark = IsDarkMode(); + const int brushW = dark ? 15 : 8; + const int penW = dark ? 30 : 22; + const int textW = dark ? 45 : 35; + dc.SetBrush(wxBrush(wxColour( + blend(bg.Red(), fg.Red(), brushW), + blend(bg.Green(), fg.Green(), brushW), + blend(bg.Blue(), fg.Blue(), brushW)))); + dc.SetPen(wxPen(wxColour( + blend(bg.Red(), fg.Red(), penW), + blend(bg.Green(), fg.Green(), penW), + blend(bg.Blue(), fg.Blue(), penW)))); + dc.SetTextForeground(wxColour( + blend(bg.Red(), fg.Red(), textW), + blend(bg.Green(), fg.Green(), textW), + blend(bg.Blue(), fg.Blue(), textW))); + } + auto location = _location * scale; wxSize sz = _size; @@ -2058,6 +2134,20 @@ ControllerModelDialog::ControllerModelDialog(wxWindow* parent, UDController* cud Connect(ID_SCROLLBAR3, wxEVT_SCROLL_CHANGED, (wxObjectEventFunction)&ControllerModelDialog::OnScrollBar_ModelsScrollChanged); //*) + // Reset the static visualizer filter to empty on every dialog construction + // so a stale value from a previous dialog session can't carry over and + // unexpectedly dim models on the newly-opened visualizer. The SearchCtrl + // itself starts empty, so this just keeps the static in sync. + BaseCMObject::_visualizerFilterLower.Clear(); + + // The model-pool filter (TextCtrl_ModelFilter) is the single filter for + // this dialog: it hides non-matching unassigned models in the pool AND + // dims non-matching assigned models on the controller visualizer (see + // OnTextCtrl_ModelFilterText). No separate visualizer filter box. + TextCtrl_ModelFilter->SetToolTip(_( + "Filters the model pool and dims non-matching models on the " + "controller visualizer. Empty filter = normal view.")); + ::SetColours(false); PanelController->SetBackgroundStyle(wxBG_STYLE_PAINT); @@ -2161,6 +2251,11 @@ ControllerModelDialog::ControllerModelDialog(wxWindow* parent, UDController* cud ControllerModelDialog::~ControllerModelDialog() { + // Clear the static visualizer filter - otherwise a stale value would + // linger past the lifetime of this dialog and dim models the next time + // the dialog is constructed before the user typed anything. + BaseCMObject::_visualizerFilterLower.Clear(); + SaveWindowPosition("ControllerModelDialogPosition", this); auto* config = GetXLightsConfig(); config->Write("ControllerModelSashPosition", SplitterWindow1->GetSashPosition()); @@ -4792,15 +4887,24 @@ void ControllerModelDialog::OnSlider_ScaleCmdSliderUpdated(wxScrollEvent& event) void ControllerModelDialog::OnTextCtrl_ModelFilterText(wxCommandEvent& event) { + // Single filter for both panes: rebuild the model pool (hiding + // non-matching unassigned models) and update the visualizer dim filter + // that ModelCMObject::Draw reads at paint time (dimming non-matching + // assigned models on the controller). Lowercased once here so the + // Draw-time match is a cheap Contains() per model. + BaseCMObject::_visualizerFilterLower = TextCtrl_ModelFilter->GetValue().Lower(); ScrollBar_Models->SetThumbPosition(0); ReloadModels(); + PanelController->Refresh(); } void ControllerModelDialog::OnTextCtrl_ModelFilterCancel(wxCommandEvent& event) { TextCtrl_ModelFilter->SetValue(wxEmptyString); + BaseCMObject::_visualizerFilterLower.Clear(); ScrollBar_Models->SetThumbPosition(0); ReloadModels(); + PanelController->Refresh(); } double ControllerModelDialog::getFontSize()