From e8cec41355de8be85bfbfed6994deeaa882642ac Mon Sep 17 00:00:00 2001 From: Seth Hillbrand Date: Fri, 29 Aug 2025 07:06:41 -0700 Subject: [PATCH] ADDED: Indication of filter blocking If the selection filter has blocked all selections under the cursor, show a subtle flash on which filter(s) did the blocking. Helpful for people getting frustrated by not being able to select locked items. Fixes https://gitlab.com/kicad/code/kicad/-/issues/20487 --- eeschema/sch_base_frame.cpp | 8 + eeschema/sch_base_frame.h | 3 + eeschema/tools/sch_selection_tool.cpp | 89 +++++++++-- eeschema/tools/sch_selection_tool.h | 5 +- .../widgets/panel_sch_selection_filter.cpp | 143 +++++++++++++++++ eeschema/widgets/panel_sch_selection_filter.h | 35 ++++- include/project/board_project_settings.h | 15 ++ include/project/sch_project_settings.h | 14 ++ pcbnew/pcb_base_edit_frame.cpp | 8 + pcbnew/pcb_base_edit_frame.h | 3 + pcbnew/tools/board_inspection_tool.cpp | 2 +- pcbnew/tools/pcb_control.cpp | 2 +- pcbnew/tools/pcb_selection_tool.cpp | 80 +++++++++- pcbnew/tools/pcb_selection_tool.h | 6 +- pcbnew/widgets/panel_selection_filter.cpp | 146 ++++++++++++++++++ pcbnew/widgets/panel_selection_filter.h | 35 ++++- qa/qa_utils/mocks.cpp | 7 +- 17 files changed, 569 insertions(+), 32 deletions(-) diff --git a/eeschema/sch_base_frame.cpp b/eeschema/sch_base_frame.cpp index 9ce3902ab7..1948a405c3 100644 --- a/eeschema/sch_base_frame.cpp +++ b/eeschema/sch_base_frame.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -942,3 +943,10 @@ SCH_SELECTION_TOOL* SCH_BASE_FRAME::GetSelectionTool() return nullptr; } + + +void SCH_BASE_FRAME::HighlightSelectionFilter( const SCH_SELECTION_FILTER_OPTIONS& aOptions ) +{ + SCH_SELECTION_FILTER_EVENT evt( aOptions ); + wxPostEvent( this, evt ); +} diff --git a/eeschema/sch_base_frame.h b/eeschema/sch_base_frame.h index 00ad8e08e8..139b606a31 100644 --- a/eeschema/sch_base_frame.h +++ b/eeschema/sch_base_frame.h @@ -55,6 +55,7 @@ class LIB_ID; class SYMBOL_LIB_TABLE; class EESCHEMA_SETTINGS; class SYMBOL_EDITOR_SETTINGS; +struct SCH_SELECTION_FILTER_OPTIONS; #ifndef __linux__ class NL_SCHEMATIC_PLUGIN; @@ -289,6 +290,8 @@ public: void GetLibraryItemsForListDialog( wxArrayString& aHeaders, std::vector& aItemsToDisplay ); + void HighlightSelectionFilter( const SCH_SELECTION_FILTER_OPTIONS& aOptions ); + protected: void handleActivateEvent( wxActivateEvent& aEvent ) override; diff --git a/eeschema/tools/sch_selection_tool.cpp b/eeschema/tools/sch_selection_tool.cpp index 0126557a4d..d224f7dba6 100644 --- a/eeschema/tools/sch_selection_tool.cpp +++ b/eeschema/tools/sch_selection_tool.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -480,9 +481,13 @@ int SCH_SELECTION_TOOL::Main( const TOOL_EVENT& aEvent ) schframe->ClearFocus(); // Collect items at the clicked location (doesn't select them yet) - SCH_COLLECTOR collector; + SCH_COLLECTOR collector; + SCH_SELECTION_FILTER_OPTIONS rejected; + CollectHits( collector, evt->Position() ); - narrowSelection( collector, evt->Position(), false ); + size_t preFilterCount = collector.GetCount(); + rejected.SetAll( false ); + narrowSelection( collector, evt->Position(), false, false, &rejected ); if( m_selection.GetSize() != 0 && dynamic_cast( m_selection.GetItem( 0 ) ) && m_additive && collector.GetCount() == 1 && dynamic_cast( collector[0] ) ) @@ -564,6 +569,12 @@ int SCH_SELECTION_TOOL::Main( const TOOL_EVENT& aEvent ) if( !selCancelled ) { + if( collector.GetCount() == 0 && preFilterCount > 0 ) + { + if( SCH_BASE_FRAME* frame = dynamic_cast( m_frame ) ) + frame->HighlightSelectionFilter( rejected ); + } + selectPoint( collector, evt->Position(), nullptr, nullptr, m_additive, m_subtractive, m_exclusive_or ); m_selection.SetIsHover( false ); @@ -1028,7 +1039,7 @@ int SCH_SELECTION_TOOL::Main( const TOOL_EVENT& aEvent ) if( CollectHits( collector, evt->Position() ) ) { - narrowSelection( collector, evt->Position(), false ); + narrowSelection( collector, evt->Position(), false, false, nullptr ); if( collector.GetCount() == 1 && !hasModifier() ) { @@ -1339,7 +1350,8 @@ bool SCH_SELECTION_TOOL::CollectHits( SCH_COLLECTOR& aCollector, const VECTOR2I& void SCH_SELECTION_TOOL::narrowSelection( SCH_COLLECTOR& collector, const VECTOR2I& aWhere, - bool aCheckLocked, bool aSelectedOnly ) + bool aCheckLocked, bool aSelectedOnly, + SCH_SELECTION_FILTER_OPTIONS* aRejected ) { SYMBOL_EDIT_FRAME* symbolEditorFrame = dynamic_cast( m_frame ); @@ -1378,11 +1390,13 @@ void SCH_SELECTION_TOOL::narrowSelection( SCH_COLLECTOR& collector, const VECTOR if( aCheckLocked && collector[i]->IsLocked() ) { + if( aRejected ) + aRejected->lockedItems = true; collector.Remove( i ); continue; } - if( !itemPassesFilter( collector[i] ) ) + if( !itemPassesFilter( collector[i], aRejected ) ) { collector.Remove( i ); continue; @@ -1539,7 +1553,18 @@ bool SCH_SELECTION_TOOL::SelectPoint( const VECTOR2I& aWhere, if( !CollectHits( collector, aWhere, aScanTypes ) ) return false; - narrowSelection( collector, aWhere, aCheckLocked, aSubtract ); + size_t preFilterCount = collector.GetCount(); + SCH_SELECTION_FILTER_OPTIONS rejected; + rejected.SetAll( false ); + narrowSelection( collector, aWhere, aCheckLocked, aSubtract, &rejected ); + + if( collector.GetCount() == 0 && preFilterCount > 0 ) + { + if( SCH_BASE_FRAME* frame = dynamic_cast( m_frame ) ) + frame->HighlightSelectionFilter( rejected ); + + return false; + } return selectPoint( collector, aWhere, aItem, aSelectionCancelledFlag, aAdd, aSubtract, aExclusiveOr ); @@ -1589,7 +1614,7 @@ int SCH_SELECTION_TOOL::SelectAll( const TOOL_EVENT& aEvent ) for( EDA_ITEM* item : collection ) { - if( Selectable( item ) && itemPassesFilter( item ) ) + if( Selectable( item ) && itemPassesFilter( item, nullptr ) ) { if( item->Type() == SCH_LINE_T ) item->SetFlags( STARTPOINT | ENDPOINT ); @@ -1949,7 +1974,7 @@ void SCH_SELECTION_TOOL::filterCollectedItems( SCH_COLLECTOR& aCollector, bool a for( EDA_ITEM* item : aCollector ) { - if( !itemPassesFilter( item ) ) + if( !itemPassesFilter( item, nullptr ) ) rejected.insert( item ); } @@ -1958,7 +1983,7 @@ void SCH_SELECTION_TOOL::filterCollectedItems( SCH_COLLECTOR& aCollector, bool a } -bool SCH_SELECTION_TOOL::itemPassesFilter( EDA_ITEM* aItem ) +bool SCH_SELECTION_TOOL::itemPassesFilter( EDA_ITEM* aItem, SCH_SELECTION_FILTER_OPTIONS* aRejected ) { if( !aItem ) return false; @@ -1977,20 +2002,32 @@ bool SCH_SELECTION_TOOL::itemPassesFilter( EDA_ITEM* aItem ) case SCH_SYMBOL_T: case SCH_SHEET_T: if( !m_filter.symbols ) + { + if( aRejected ) + aRejected->symbols = true; return false; + } break; case SCH_PIN_T: case SCH_SHEET_PIN_T: if( !m_filter.pins ) + { + if( aRejected ) + aRejected->pins = true; return false; + } break; case SCH_JUNCTION_T: if( !m_filter.wires ) + { + if( aRejected ) + aRejected->wires = true; return false; + } break; @@ -2001,13 +2038,21 @@ bool SCH_SELECTION_TOOL::itemPassesFilter( EDA_ITEM* aItem ) case LAYER_WIRE: case LAYER_BUS: if( !m_filter.wires ) + { + if( aRejected ) + aRejected->wires = true; return false; + } break; default: if( !m_filter.graphics ) + { + if( aRejected ) + aRejected->graphics = true; return false; + } } break; @@ -2015,7 +2060,11 @@ bool SCH_SELECTION_TOOL::itemPassesFilter( EDA_ITEM* aItem ) case SCH_SHAPE_T: if( !m_filter.graphics ) + { + if( aRejected ) + aRejected->graphics = true; return false; + } break; @@ -2025,7 +2074,11 @@ bool SCH_SELECTION_TOOL::itemPassesFilter( EDA_ITEM* aItem ) case SCH_TABLECELL_T: case SCH_FIELD_T: if( !m_filter.text ) + { + if( aRejected ) + aRejected->text = true; return false; + } break; @@ -2033,25 +2086,41 @@ bool SCH_SELECTION_TOOL::itemPassesFilter( EDA_ITEM* aItem ) case SCH_GLOBAL_LABEL_T: case SCH_HIER_LABEL_T: if( !m_filter.labels ) + { + if( aRejected ) + aRejected->labels = true; return false; + } break; case SCH_BITMAP_T: if( !m_filter.images ) + { + if( aRejected ) + aRejected->images = true; return false; + } break; case SCH_RULE_AREA_T: if( !m_filter.ruleAreas ) + { + if( aRejected ) + aRejected->ruleAreas = true; return false; + } break; default: if( !m_filter.otherItems ) + { + if( aRejected ) + aRejected->otherItems = true; return false; + } break; } @@ -2335,7 +2404,7 @@ bool SCH_SELECTION_TOOL::selectMultiple() if( m_frame->GetRenderSettings()->m_ShowPinsElectricalType ) item->SetFlags( SHOW_ELEC_TYPE ); - if( Selectable( item ) && itemPassesFilter( item ) && !item->GetParent()->HasFlag( SELECTION_CANDIDATE ) + if( Selectable( item ) && itemPassesFilter( item, nullptr ) && !item->GetParent()->HasFlag( SELECTION_CANDIDATE ) && item->HitTest( selectionRect, !isGreedy ) ) { selectItem( item, 0 ); diff --git a/eeschema/tools/sch_selection_tool.h b/eeschema/tools/sch_selection_tool.h index 9e4ceb86a7..9cf35d43b9 100644 --- a/eeschema/tools/sch_selection_tool.h +++ b/eeschema/tools/sch_selection_tool.h @@ -237,7 +237,8 @@ private: * @param aSelectedOnly If true, remove non-selected items from #collector */ void narrowSelection( SCH_COLLECTOR& collector, const VECTOR2I& aWhere, bool aCheckLocked, - bool aSelectedOnly = false ); + bool aSelectedOnly = false, + SCH_SELECTION_FILTER_OPTIONS* aRejected = nullptr ); /** * Perform a click-type selection at a point (usually the cursor position). @@ -341,7 +342,7 @@ private: /** * Return true if the given item passes the stateful selection filter */ - bool itemPassesFilter( EDA_ITEM* aItem ); + bool itemPassesFilter( EDA_ITEM* aItem, SCH_SELECTION_FILTER_OPTIONS* aRejected = nullptr ); /** * In general we don't want to select both a parent and any of it's children. This includes diff --git a/eeschema/widgets/panel_sch_selection_filter.cpp b/eeschema/widgets/panel_sch_selection_filter.cpp index fe0d72ceb2..475ffbe3dd 100644 --- a/eeschema/widgets/panel_sch_selection_filter.cpp +++ b/eeschema/widgets/panel_sch_selection_filter.cpp @@ -23,8 +23,12 @@ #include #include #include +#include +#include +wxDEFINE_EVENT( EVT_SCH_SELECTION_FILTER_FLASH, SCH_SELECTION_FILTER_EVENT ); + PANEL_SCH_SELECTION_FILTER::PANEL_SCH_SELECTION_FILTER( wxWindow* aParent ) : PANEL_SCH_SELECTION_FILTER_BASE( aParent ), m_frame( dynamic_cast( aParent ) ), @@ -83,12 +87,27 @@ PANEL_SCH_SELECTION_FILTER::PANEL_SCH_SELECTION_FILTER( wxWindow* aParent ) : } m_frame->Bind( EDA_LANG_CHANGED, &PANEL_SCH_SELECTION_FILTER::OnLanguageChanged, this ); + + m_flashSteps = 10; + m_defaultBg = GetBackgroundColour(); + m_flashTimer.SetOwner( this ); + Bind( wxEVT_TIMER, &PANEL_SCH_SELECTION_FILTER::onFlashTimer, this ); + aParent->Bind( EVT_SCH_SELECTION_FILTER_FLASH, &PANEL_SCH_SELECTION_FILTER::OnFlashEvent, this ); } PANEL_SCH_SELECTION_FILTER::~PANEL_SCH_SELECTION_FILTER() { + // Stop any active flashing + m_flashTimer.Stop(); + if( !m_flashCounters.empty() ) + { + Unbind( wxEVT_PAINT, &PANEL_SCH_SELECTION_FILTER::onPanelPaint, this ); + } + m_flashCounters.clear(); + m_frame->Unbind( EDA_LANG_CHANGED, &PANEL_SCH_SELECTION_FILTER::OnLanguageChanged, this ); + m_frame->Unbind( EVT_SCH_SELECTION_FILTER_FLASH, &PANEL_SCH_SELECTION_FILTER::OnFlashEvent, this ); } @@ -220,3 +239,127 @@ void PANEL_SCH_SELECTION_FILTER::OnLanguageChanged( wxCommandEvent& aEvent ) aEvent.Skip(); } + + +void PANEL_SCH_SELECTION_FILTER::flashCheckbox( wxCheckBox* aBox ) +{ + if( !aBox ) + return; + + // If already flashing, just reset the counter + if( m_flashCounters.find( aBox ) != m_flashCounters.end() ) + { + m_flashCounters[aBox] = m_flashSteps; + return; + } + + m_flashCounters[aBox] = m_flashSteps; + m_defaultBg = aBox->GetBackgroundColour(); + + // Bind paint event to this panel if not already bound + if( m_flashCounters.size() == 1 ) + { + Bind( wxEVT_PAINT, &PANEL_SCH_SELECTION_FILTER::onPanelPaint, this ); + } + + Refresh(); +} + + +void PANEL_SCH_SELECTION_FILTER::onFlashTimer( wxTimerEvent& aEvent ) +{ + for( auto it = m_flashCounters.begin(); it != m_flashCounters.end(); ) + { + int step = --( it->second ); + + if( step <= 0 ) + it = m_flashCounters.erase( it ); + else + ++it; + } + + if( m_flashCounters.empty() ) + { + m_flashTimer.Stop(); + // Unbind paint event when no more flashing + Unbind( wxEVT_PAINT, &PANEL_SCH_SELECTION_FILTER::onPanelPaint, this ); + } + + Refresh(); +} + + +void PANEL_SCH_SELECTION_FILTER::onPanelPaint( wxPaintEvent& aEvent ) +{ + wxPaintDC dc( this ); + + // First, let the default painting happen + aEvent.Skip(); + + // Then draw our highlights on top + for( auto& pair : m_flashCounters ) + { + wxCheckBox* checkbox = pair.first; + int step = pair.second; + + if( step > 0 ) + { + // Get the checkbox position relative to this panel + wxPoint checkboxPos = checkbox->GetPosition(); + wxSize checkboxSize = checkbox->GetSize(); + wxRect checkboxRect( checkboxPos, checkboxSize ); + + // Calculate blended color based on current flash step + wxColour highlight = wxSystemSettings::GetColour( wxSYS_COLOUR_HIGHLIGHT ); + wxColour blend( + ( highlight.Red() * step + m_defaultBg.Red() * ( m_flashSteps - step ) ) / m_flashSteps, + ( highlight.Green() * step + m_defaultBg.Green() * ( m_flashSteps - step ) ) / m_flashSteps, + ( highlight.Blue() * step + m_defaultBg.Blue() * ( m_flashSteps - step ) ) / m_flashSteps ); + + // Draw semi-transparent overlay + wxColour overlayColor( blend.Red(), blend.Green(), blend.Blue(), 128 ); + dc.SetBrush( wxBrush( overlayColor ) ); + dc.SetPen( wxPen( overlayColor ) ); + dc.DrawRectangle( checkboxRect ); + } + } +} + + +void PANEL_SCH_SELECTION_FILTER::OnFlashEvent( SCH_SELECTION_FILTER_EVENT& aEvent ) +{ + const SCH_SELECTION_FILTER_OPTIONS& aOptions = aEvent.m_options; + + if( aOptions.lockedItems ) + flashCheckbox( m_cbLockedItems ); + + if( aOptions.symbols ) + flashCheckbox( m_cbSymbols ); + + if( aOptions.text ) + flashCheckbox( m_cbText ); + + if( aOptions.wires ) + flashCheckbox( m_cbWires ); + + if( aOptions.labels ) + flashCheckbox( m_cbLabels ); + + if( aOptions.pins ) + flashCheckbox( m_cbPins ); + + if( aOptions.graphics ) + flashCheckbox( m_cbGraphics ); + + if( aOptions.images ) + flashCheckbox( m_cbImages ); + + if( aOptions.ruleAreas ) + flashCheckbox( m_cbRuleAreas ); + + if( aOptions.otherItems ) + flashCheckbox( m_cbOtherItems ); + + if( !m_flashCounters.empty() ) + m_flashTimer.Start( 50 ); +} diff --git a/eeschema/widgets/panel_sch_selection_filter.h b/eeschema/widgets/panel_sch_selection_filter.h index 846a04d371..a4eb84be06 100644 --- a/eeschema/widgets/panel_sch_selection_filter.h +++ b/eeschema/widgets/panel_sch_selection_filter.h @@ -22,11 +22,31 @@ #define KICAD_PANEL_SCH_SELECTION_FILTER_H #include +#include +#include class SCH_BASE_FRAME; class SCH_SELECTION_TOOL; struct SCH_SELECTION_FILTER_OPTIONS; +// Forward declare the event type +class SCH_SELECTION_FILTER_EVENT; +wxDECLARE_EVENT( EVT_SCH_SELECTION_FILTER_FLASH, SCH_SELECTION_FILTER_EVENT ); + +class SCH_SELECTION_FILTER_EVENT : public wxCommandEvent +{ +public: + SCH_SELECTION_FILTER_EVENT( const SCH_SELECTION_FILTER_OPTIONS& aOptions = SCH_SELECTION_FILTER_OPTIONS(), int id = 0 ) : + wxCommandEvent( EVT_SCH_SELECTION_FILTER_FLASH, id ), m_options( aOptions ) {} + + wxEvent* Clone() const override + { + return new SCH_SELECTION_FILTER_EVENT( *this ); + } + + SCH_SELECTION_FILTER_OPTIONS m_options; +}; + class PANEL_SCH_SELECTION_FILTER : public PANEL_SCH_SELECTION_FILTER_BASE { @@ -48,10 +68,19 @@ private: void onPopupSelection( wxCommandEvent& aEvent ); + void flashCheckbox( wxCheckBox* aBox ); + void onFlashTimer( wxTimerEvent& aEvent ); + void OnFlashEvent( SCH_SELECTION_FILTER_EVENT& aEvent ); + void onPanelPaint( wxPaintEvent& aEvent ); + private: - SCH_BASE_FRAME* m_frame; - SCH_SELECTION_TOOL* m_tool; - wxCheckBox* m_onlyCheckbox; + SCH_BASE_FRAME* m_frame; + SCH_SELECTION_TOOL* m_tool; + wxCheckBox* m_onlyCheckbox; + std::map m_flashCounters; + wxTimer m_flashTimer; + int m_flashSteps; + wxColour m_defaultBg; }; #endif //KICAD_PANEL_SCH_SELECTION_FILTER_H diff --git a/include/project/board_project_settings.h b/include/project/board_project_settings.h index f8bee6f6e9..b63d7aa833 100644 --- a/include/project/board_project_settings.h +++ b/include/project/board_project_settings.h @@ -86,6 +86,21 @@ struct KICOMMON_API PCB_SELECTION_FILTER_OPTIONS return ( footprints && text && tracks && vias && pads && graphics && zones && keepouts && dimensions && otherItems ); } + + void SetAll( bool aState ) + { + footprints = aState; + text = aState; + tracks = aState; + vias = aState; + pads = aState; + graphics = aState; + zones = aState; + keepouts = aState; + dimensions = aState; + otherItems = aState; + lockedItems = aState; + } }; /** diff --git a/include/project/sch_project_settings.h b/include/project/sch_project_settings.h index 4191823dc9..0861236c9d 100644 --- a/include/project/sch_project_settings.h +++ b/include/project/sch_project_settings.h @@ -77,6 +77,20 @@ struct SCH_SELECTION_FILTER_OPTIONS ruleAreas = true; otherItems = true; } + + void SetAll( bool aState ) + { + lockedItems = aState; + symbols = aState; + text = aState; + wires = aState; + labels = aState; + pins = aState; + graphics = aState; + images = aState; + ruleAreas = aState; + otherItems = aState; + } }; #endif //KICAD_SCH_PROJECT_SETTINGS_H diff --git a/pcbnew/pcb_base_edit_frame.cpp b/pcbnew/pcb_base_edit_frame.cpp index 51815aa11d..318d803fe1 100644 --- a/pcbnew/pcb_base_edit_frame.cpp +++ b/pcbnew/pcb_base_edit_frame.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -406,3 +407,10 @@ void PCB_BASE_EDIT_FRAME::configureToolbars() RegisterCustomToolbarControlFactory( ACTION_TOOLBAR_CONTROLS::layerSelector, layerSelectorFactory ); } + + +void PCB_BASE_EDIT_FRAME::HighlightSelectionFilter( const PCB_SELECTION_FILTER_OPTIONS& aOptions ) +{ + PCB_SELECTION_FILTER_EVENT evt( aOptions ); + wxPostEvent( this, evt ); +} diff --git a/pcbnew/pcb_base_edit_frame.h b/pcbnew/pcb_base_edit_frame.h index 0d75808d46..f568d3766f 100644 --- a/pcbnew/pcb_base_edit_frame.h +++ b/pcbnew/pcb_base_edit_frame.h @@ -38,6 +38,7 @@ class PCB_TABLE; class PCB_TEXT; class PCB_SHAPE; class FILEDLG_HOOK_NEW_LIBRARY; +struct PCB_SELECTION_FILTER_OPTIONS; /** * Common, abstract interface for edit frames. @@ -245,6 +246,8 @@ public: void GetContextualTextVars( BOARD_ITEM* aSourceItem, const wxString& aCrossRef, wxArrayString* aTokens ); + void HighlightSelectionFilter( const PCB_SELECTION_FILTER_OPTIONS& aOptions ); + protected: void configureToolbars() override; diff --git a/pcbnew/tools/board_inspection_tool.cpp b/pcbnew/tools/board_inspection_tool.cpp index ab896ba19d..f349d04041 100644 --- a/pcbnew/tools/board_inspection_tool.cpp +++ b/pcbnew/tools/board_inspection_tool.cpp @@ -1806,7 +1806,7 @@ int BOARD_INSPECTION_TOOL::HighlightItem( const TOOL_EVENT& aEvent ) bool saved = filter.lockedItems; filter.lockedItems = true; - selectionTool->FilterCollectedItems( collector, true ); + selectionTool->FilterCollectedItems( collector, true, nullptr ); filter.lockedItems = saved; diff --git a/pcbnew/tools/pcb_control.cpp b/pcbnew/tools/pcb_control.cpp index 96173caccf..d72e4ee9f1 100644 --- a/pcbnew/tools/pcb_control.cpp +++ b/pcbnew/tools/pcb_control.cpp @@ -879,7 +879,7 @@ int PCB_CONTROL::InteractiveDelete( const TOOL_EVENT& aEvent ) } selectionTool->FilterCollectorForHierarchy( collector, false ); - selectionTool->FilterCollectedItems( collector, false ); + selectionTool->FilterCollectedItems( collector, false, nullptr ); if( collector.GetCount() > 1 ) selectionTool->GuessSelectionCandidates( collector, aPos ); diff --git a/pcbnew/tools/pcb_selection_tool.cpp b/pcbnew/tools/pcb_selection_tool.cpp index 55a4568f3a..9a4f8dc4b6 100644 --- a/pcbnew/tools/pcb_selection_tool.cpp +++ b/pcbnew/tools/pcb_selection_tool.cpp @@ -38,6 +38,7 @@ using namespace std::placeholders; #include #include #include +#include #include #include #include @@ -769,8 +770,20 @@ bool PCB_SELECTION_TOOL::selectPoint( const VECTOR2I& aWhere, bool aOnDrag, bool m_selection.ClearReferencePoint(); + size_t preFilterCount = collector.GetCount(); + PCB_SELECTION_FILTER_OPTIONS rejected; + rejected.SetAll( false ); + // Apply the stateful filter (remove items disabled by the Selection Filter) - FilterCollectedItems( collector, false ); + FilterCollectedItems( collector, false, &rejected ); + + if( collector.GetCount() == 0 && preFilterCount > 0 ) + { + if( PCB_BASE_EDIT_FRAME* editFrame = dynamic_cast( m_frame ) ) + editFrame->HighlightSelectionFilter( rejected ); + + return false; + } // Allow the client to do tool- or action-specific filtering to see if we can get down // to a single item @@ -1288,7 +1301,7 @@ void PCB_SELECTION_TOOL::SelectMultiple( KIGFX::PREVIEW::SELECTION_AREA& aArea, } // Apply the stateful filter - FilterCollectedItems( collector, true ); + FilterCollectedItems( collector, true, nullptr ); FilterCollectorForHierarchy( collector, true ); @@ -1296,7 +1309,7 @@ void PCB_SELECTION_TOOL::SelectMultiple( KIGFX::PREVIEW::SELECTION_AREA& aArea, if( collector.GetCount() == 0 ) { collector = padsCollector; - FilterCollectedItems( collector, true ); + FilterCollectedItems( collector, true, nullptr ); FilterCollectorForHierarchy( collector, true ); } @@ -1390,7 +1403,7 @@ int PCB_SELECTION_TOOL::SelectAll( const TOOL_EVENT& aEvent ) { BOARD_ITEM* item = static_cast( viewItem ); - if( item && Selectable( item ) && itemPassesFilter( item, true ) ) + if( item && Selectable( item ) && itemPassesFilter( item, true, nullptr ) ) collection.Append( item ); } @@ -2114,7 +2127,7 @@ void PCB_SELECTION_TOOL::SelectAllItemsOnNet( int aNetCode, bool aSelect ) PCB_VIA_T, PCB_SHAPE_T } ) ) { - if( itemPassesFilter( item, true ) ) + if( itemPassesFilter( item, true, nullptr ) ) aSelect ? select( item ) : unselect( item ); } } @@ -2719,7 +2732,8 @@ int PCB_SELECTION_TOOL::filterSelection( const TOOL_EVENT& aEvent ) } -void PCB_SELECTION_TOOL::FilterCollectedItems( GENERAL_COLLECTOR& aCollector, bool aMultiSelect ) +void PCB_SELECTION_TOOL::FilterCollectedItems( GENERAL_COLLECTOR& aCollector, bool aMultiSelect, + PCB_SELECTION_FILTER_OPTIONS* aRejected ) { if( aCollector.GetCount() == 0 ) return; @@ -2733,7 +2747,7 @@ void PCB_SELECTION_TOOL::FilterCollectedItems( GENERAL_COLLECTOR& aCollector, bo BOARD_ITEM* item = static_cast( i ); - if( !itemPassesFilter( item, aMultiSelect ) ) + if( !itemPassesFilter( item, aMultiSelect, aRejected ) ) rejected.insert( item ); } @@ -2742,7 +2756,8 @@ void PCB_SELECTION_TOOL::FilterCollectedItems( GENERAL_COLLECTOR& aCollector, bo } -bool PCB_SELECTION_TOOL::itemPassesFilter( BOARD_ITEM* aItem, bool aMultiSelect ) +bool PCB_SELECTION_TOOL::itemPassesFilter( BOARD_ITEM* aItem, bool aMultiSelect, + PCB_SELECTION_FILTER_OPTIONS* aRejected ) { if( !m_filter.lockedItems ) { @@ -2755,6 +2770,8 @@ bool PCB_SELECTION_TOOL::itemPassesFilter( BOARD_ITEM* aItem, bool aMultiSelect } else { + if( aRejected ) + aRejected->lockedItems = true; return false; } } @@ -2770,7 +2787,11 @@ bool PCB_SELECTION_TOOL::itemPassesFilter( BOARD_ITEM* aItem, bool aMultiSelect if( static_cast( aItem )->GetItems().empty() ) { if( !m_filter.otherItems ) + { + if( aRejected ) + aRejected->otherItems = true; return false; + } } else { @@ -2782,26 +2803,42 @@ bool PCB_SELECTION_TOOL::itemPassesFilter( BOARD_ITEM* aItem, bool aMultiSelect { case PCB_FOOTPRINT_T: if( !m_filter.footprints ) + { + if( aRejected ) + aRejected->footprints = true; return false; + } break; case PCB_PAD_T: if( !m_filter.pads ) + { + if( aRejected ) + aRejected->pads = true; return false; + } break; case PCB_TRACE_T: case PCB_ARC_T: if( !m_filter.tracks ) + { + if( aRejected ) + aRejected->tracks = true; return false; + } break; case PCB_VIA_T: if( !m_filter.vias ) + { + if( aRejected ) + aRejected->vias = true; return false; + } break; @@ -2812,6 +2849,13 @@ bool PCB_SELECTION_TOOL::itemPassesFilter( BOARD_ITEM* aItem, bool aMultiSelect if( ( !m_filter.zones && !zone->GetIsRuleArea() ) || ( !m_filter.keepouts && zone->GetIsRuleArea() ) ) { + if( aRejected ) + { + if( zone->GetIsRuleArea() ) + aRejected->keepouts = true; + else + aRejected->zones = true; + } return false; } @@ -2827,17 +2871,29 @@ bool PCB_SELECTION_TOOL::itemPassesFilter( BOARD_ITEM* aItem, bool aMultiSelect case PCB_SHAPE_T: case PCB_TARGET_T: if( !m_filter.graphics ) + { + if( aRejected ) + aRejected->graphics = true; return false; + } break; case PCB_REFERENCE_IMAGE_T: if( !m_filter.graphics ) + { + if( aRejected ) + aRejected->graphics = true; return false; + } // a reference image living in a footprint must not be selected inside the board editor if( !m_isFootprintEditor && aItem->GetParentFootprint() ) + { + if( aRejected ) + aRejected->text = true; return false; + } break; @@ -2857,13 +2913,21 @@ bool PCB_SELECTION_TOOL::itemPassesFilter( BOARD_ITEM* aItem, bool aMultiSelect case PCB_DIM_ORTHOGONAL_T: case PCB_DIM_LEADER_T: if( !m_filter.dimensions ) + { + if( aRejected ) + aRejected->dimensions = true; return false; + } break; default: if( !m_filter.otherItems ) + { + if( aRejected ) + aRejected->otherItems = true; return false; + } } return true; diff --git a/pcbnew/tools/pcb_selection_tool.h b/pcbnew/tools/pcb_selection_tool.h index 2ce0c7d4a4..72207a8bad 100644 --- a/pcbnew/tools/pcb_selection_tool.h +++ b/pcbnew/tools/pcb_selection_tool.h @@ -250,7 +250,8 @@ public: /** * Apply the SELECTION_FITLER_OPTIONS to the collector. */ - void FilterCollectedItems( GENERAL_COLLECTOR& aCollector, bool aMultiSelect ); + void FilterCollectedItems( GENERAL_COLLECTOR& aCollector, bool aMultiSelect, + PCB_SELECTION_FILTER_OPTIONS* aRejected = nullptr ); /** * Drop footprints that are not directly selected @@ -426,7 +427,8 @@ private: int filterSelection( const TOOL_EVENT& aEvent ); ///< Return true if the given item passes the current SELECTION_FILTER_OPTIONS. - bool itemPassesFilter( BOARD_ITEM* aItem, bool aMultiSelect ); + bool itemPassesFilter( BOARD_ITEM* aItem, bool aMultiSelect, + PCB_SELECTION_FILTER_OPTIONS* aRejected = nullptr ); /** * Take necessary action mark an item as unselected. diff --git a/pcbnew/widgets/panel_selection_filter.cpp b/pcbnew/widgets/panel_selection_filter.cpp index 87b4139675..b362a66e7c 100644 --- a/pcbnew/widgets/panel_selection_filter.cpp +++ b/pcbnew/widgets/panel_selection_filter.cpp @@ -22,8 +22,12 @@ #include #include #include +#include +#include +wxDEFINE_EVENT( EVT_PCB_SELECTION_FILTER_FLASH, PCB_SELECTION_FILTER_EVENT ); + PANEL_SELECTION_FILTER::PANEL_SELECTION_FILTER( wxWindow* aParent ) : PANEL_SELECTION_FILTER_BASE( aParent ), m_frame( dynamic_cast( aParent ) ), @@ -66,12 +70,27 @@ PANEL_SELECTION_FILTER::PANEL_SELECTION_FILTER( wxWindow* aParent ) : m_frame->Bind( EDA_LANG_CHANGED, &PANEL_SELECTION_FILTER::OnLanguageChanged, this ); SetMinSize( GetBestSize() ); + + m_flashSteps = 10; + m_defaultBg = GetBackgroundColour(); + m_flashTimer.SetOwner( this ); + Bind( wxEVT_TIMER, &PANEL_SELECTION_FILTER::onFlashTimer, this ); + aParent->Bind( EVT_PCB_SELECTION_FILTER_FLASH, &PANEL_SELECTION_FILTER::OnFlashEvent, this ); } PANEL_SELECTION_FILTER::~PANEL_SELECTION_FILTER() { + // Stop any active flashing + m_flashTimer.Stop(); + if( !m_flashCounters.empty() ) + { + Unbind( wxEVT_PAINT, &PANEL_SELECTION_FILTER::onPanelPaint, this ); + } + m_flashCounters.clear(); + m_frame->Unbind( EDA_LANG_CHANGED, &PANEL_SELECTION_FILTER::OnLanguageChanged, this ); + m_frame->Unbind( EVT_PCB_SELECTION_FILTER_FLASH, &PANEL_SELECTION_FILTER::OnFlashEvent, this ); } @@ -208,3 +227,130 @@ void PANEL_SELECTION_FILTER::OnLanguageChanged( wxCommandEvent& aEvent ) aEvent.Skip(); } + + +void PANEL_SELECTION_FILTER::flashCheckbox( wxCheckBox* aBox ) +{ + if( !aBox ) + return; + + // If already flashing, just reset the counter + if( m_flashCounters.find( aBox ) != m_flashCounters.end() ) + { + m_flashCounters[aBox] = m_flashSteps; + return; + } + + m_flashCounters[aBox] = m_flashSteps; + m_defaultBg = aBox->GetBackgroundColour(); + + // Bind paint event to this panel if not already bound + if( m_flashCounters.size() == 1 ) + { + Bind( wxEVT_PAINT, &PANEL_SELECTION_FILTER::onPanelPaint, this ); + } + + Refresh(); +} + + +void PANEL_SELECTION_FILTER::onFlashTimer( wxTimerEvent& aEvent ) +{ + for( auto it = m_flashCounters.begin(); it != m_flashCounters.end(); ) + { + int step = --( it->second ); + + if( step <= 0 ) + it = m_flashCounters.erase( it ); + else + ++it; + } + + if( m_flashCounters.empty() ) + { + m_flashTimer.Stop(); + // Unbind paint event when no more flashing + Unbind( wxEVT_PAINT, &PANEL_SELECTION_FILTER::onPanelPaint, this ); + } + + Refresh(); +} + + +void PANEL_SELECTION_FILTER::onPanelPaint( wxPaintEvent& aEvent ) +{ + wxPaintDC dc( this ); + + // First, let the default painting happen + aEvent.Skip(); + + // Then draw our highlights on top + for( auto& pair : m_flashCounters ) + { + wxCheckBox* checkbox = pair.first; + int step = pair.second; + + if( step > 0 ) + { + // Get the checkbox position relative to this panel + wxPoint checkboxPos = checkbox->GetPosition(); + wxSize checkboxSize = checkbox->GetSize(); + wxRect checkboxRect( checkboxPos, checkboxSize ); + + // Calculate blended color based on current flash step + wxColour highlight = wxSystemSettings::GetColour( wxSYS_COLOUR_HIGHLIGHT ); + wxColour blend( + ( highlight.Red() * step + m_defaultBg.Red() * ( m_flashSteps - step ) ) / m_flashSteps, + ( highlight.Green() * step + m_defaultBg.Green() * ( m_flashSteps - step ) ) / m_flashSteps, + ( highlight.Blue() * step + m_defaultBg.Blue() * ( m_flashSteps - step ) ) / m_flashSteps ); + + // Draw semi-transparent overlay + wxColour overlayColor( blend.Red(), blend.Green(), blend.Blue(), 128 ); + dc.SetBrush( wxBrush( overlayColor ) ); + dc.SetPen( wxPen( overlayColor ) ); + dc.DrawRectangle( checkboxRect ); + } + } +} + + +void PANEL_SELECTION_FILTER::OnFlashEvent( PCB_SELECTION_FILTER_EVENT& aEvent ) +{ + const PCB_SELECTION_FILTER_OPTIONS& aOptions = aEvent.m_options; + + if( aOptions.lockedItems ) + flashCheckbox( m_cbLockedItems ); + + if( aOptions.footprints ) + flashCheckbox( m_cbFootprints ); + + if( aOptions.text ) + flashCheckbox( m_cbText ); + + if( aOptions.tracks ) + flashCheckbox( m_cbTracks ); + + if( aOptions.vias ) + flashCheckbox( m_cbVias ); + + if( aOptions.pads ) + flashCheckbox( m_cbPads ); + + if( aOptions.graphics ) + flashCheckbox( m_cbGraphics ); + + if( aOptions.zones ) + flashCheckbox( m_cbZones ); + + if( aOptions.keepouts ) + flashCheckbox( m_cbKeepouts ); + + if( aOptions.dimensions ) + flashCheckbox( m_cbDimensions ); + + if( aOptions.otherItems ) + flashCheckbox( m_cbOtherItems ); + + if( !m_flashCounters.empty() ) + m_flashTimer.Start( 50 ); +} diff --git a/pcbnew/widgets/panel_selection_filter.h b/pcbnew/widgets/panel_selection_filter.h index a535ed9996..da0c6292a3 100644 --- a/pcbnew/widgets/panel_selection_filter.h +++ b/pcbnew/widgets/panel_selection_filter.h @@ -22,11 +22,31 @@ #define KICAD_PANEL_SELECTION_FILTER_H #include +#include +#include class PCB_SELECTION_TOOL; struct PCB_SELECTION_FILTER_OPTIONS; +// Forward declare the event type +class PCB_SELECTION_FILTER_EVENT; +wxDECLARE_EVENT( EVT_PCB_SELECTION_FILTER_FLASH, PCB_SELECTION_FILTER_EVENT ); + +class PCB_SELECTION_FILTER_EVENT : public wxCommandEvent +{ +public: + PCB_SELECTION_FILTER_EVENT( const PCB_SELECTION_FILTER_OPTIONS& aOptions = PCB_SELECTION_FILTER_OPTIONS(), int id = 0 ) : + wxCommandEvent( EVT_PCB_SELECTION_FILTER_FLASH, id ), m_options( aOptions ) {} + + wxEvent* Clone() const override + { + return new PCB_SELECTION_FILTER_EVENT( *this ); + } + + PCB_SELECTION_FILTER_OPTIONS m_options; +}; + class PANEL_SELECTION_FILTER : public PANEL_SELECTION_FILTER_BASE { @@ -48,10 +68,19 @@ private: void onPopupSelection( wxCommandEvent& aEvent ); + void flashCheckbox( wxCheckBox* aBox ); + void onFlashTimer( wxTimerEvent& aEvent ); + void OnFlashEvent( PCB_SELECTION_FILTER_EVENT& aEvent ); + void onPanelPaint( wxPaintEvent& aEvent ); + private: - PCB_BASE_EDIT_FRAME* m_frame; - PCB_SELECTION_TOOL* m_tool; - wxCheckBox* m_onlyCheckbox; + PCB_BASE_EDIT_FRAME* m_frame; + PCB_SELECTION_TOOL* m_tool; + wxCheckBox* m_onlyCheckbox; + std::map m_flashCounters; + wxTimer m_flashTimer; + int m_flashSteps; + wxColour m_defaultBg; }; diff --git a/qa/qa_utils/mocks.cpp b/qa/qa_utils/mocks.cpp index 0b6da98e44..65becdf2b2 100644 --- a/qa/qa_utils/mocks.cpp +++ b/qa/qa_utils/mocks.cpp @@ -38,6 +38,7 @@ #include #include #include +struct PCB_SELECTION_FILTER_OPTIONS; #include FP_LIB_TABLE GFootprintTable; @@ -360,12 +361,14 @@ int PCB_SELECTION_TOOL::filterSelection( const TOOL_EVENT& aEvent ) } -void PCB_SELECTION_TOOL::FilterCollectedItems( GENERAL_COLLECTOR& aCollector, bool aMultiSelect ) +void PCB_SELECTION_TOOL::FilterCollectedItems( GENERAL_COLLECTOR& aCollector, bool aMultiSelect, + PCB_SELECTION_FILTER_OPTIONS* aRejected ) { } -bool PCB_SELECTION_TOOL::itemPassesFilter( BOARD_ITEM* aItem, bool aMultiSelect ) +bool PCB_SELECTION_TOOL::itemPassesFilter( BOARD_ITEM* aItem, bool aMultiSelect, + PCB_SELECTION_FILTER_OPTIONS* aRejected ) { return true; }