KiCad PCB EDA Suite
Loading...
Searching...
No Matches
properties_panel.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KICAD, a free EDA CAD application.
3 *
4 * Copyright (C) 2020-2023 CERN
5 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
6 * @author Maciej Suminski <[email protected]>
7 *
8 * This program is free software; you can redistribute it and/or
9 * modify it under the terms of the GNU General Public License
10 * as published by the Free Software Foundation; either version 3
11 * of the License, or (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program. If not, see <https://www.gnu.org/licenses/>.
20 */
21
22#include "properties_panel.h"
23#include <bitmaps.h>
24#include <tool/selection.h>
25#include <eda_base_frame.h>
26#include <eda_item.h>
27#include <i18n_utility.h>
28#include <import_export.h>
29#include <pgm_base.h>
31#include <properties/property.h>
34
35#include <algorithm>
36#include <iterator>
37#include <set>
38
39#include <wx/clipbrd.h>
40#include <wx/dataobj.h>
41#include <wx/settings.h>
42#include <wx/stattext.h>
43#include <wx/propgrid/advprops.h>
44#include <wx/menu.h>
45#include <wx/utils.h>
46
47
48// This is provided by wx >3.3.0
49#if !wxCHECK_VERSION( 3, 3, 0 )
50extern APIIMPORT wxPGGlobalVarsClass* wxPGGlobalVars;
51#endif
52
53
54class PROPERTIES_PANEL_GRID : public wxPropertyGrid
55{
56public:
57 PROPERTIES_PANEL_GRID( wxWindow* aParent ) :
58 wxPropertyGrid( aParent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxPG_DEFAULT_STYLE | wxPG_TOOLTIPS )
59 {
60 }
61
62 void ScrollWindow( int aDx, int aDy, const wxRect* aRect = nullptr ) override
63 {
64 wxPropertyGrid::ScrollWindow( aDx, aDy, aRect );
65
66 if( PROPERTIES_PANEL* panel = static_cast<PROPERTIES_PANEL*>( GetParent() ) )
67 panel->positionCategoryButtons();
68 }
69
71 bool IsProcessingWxPGEvent() const { return m_processedEvent != nullptr; }
72
73#if wxUSE_STATUSBAR
74 wxStatusBar* GetStatusBar() override { return nullptr; }
75#endif
76};
77
78
80 wxPanel( aParent ),
82 m_frame( aFrame ),
85{
86 wxBoxSizer* mainSizer = new wxBoxSizer( wxVERTICAL );
87
88#if !wxCHECK_VERSION( 3, 3, 0 )
89 // on some platforms wxPGGlobalVars is initialized automatically,
90 // but others need an explicit init
91 if( !wxPGGlobalVars )
92 wxPGInitResourceModule();
93#endif
94
95 // See https://gitlab.com/kicad/code/kicad/-/issues/12297
96 // and https://github.com/wxWidgets/wxWidgets/issues/11787
97 if( wxPGGlobalVars->m_mapEditorClasses.empty() )
98 {
99 wxPGEditor_TextCtrl = nullptr;
100 wxPGEditor_Choice = nullptr;
101 wxPGEditor_ComboBox = nullptr;
102 wxPGEditor_TextCtrlAndButton = nullptr;
103 wxPGEditor_CheckBox = nullptr;
104 wxPGEditor_ChoiceAndButton = nullptr;
105 wxPGEditor_SpinCtrl = nullptr;
106 wxPGEditor_DatePickerCtrl = nullptr;
107 }
108
109 if( !Pgm().m_PropertyGridInitialized )
110 {
111 delete wxPGGlobalVars->m_defaultRenderer;
112 wxPGGlobalVars->m_defaultRenderer = new PG_CELL_RENDERER();
114 }
115
116 m_caption = new wxStaticText( this, wxID_ANY, _( "No objects selected" ) );
117 mainSizer->Add( m_caption, 0, wxALL | wxEXPAND, 5 );
118
119 m_grid = new PROPERTIES_PANEL_GRID( this );
120 m_grid->SetUnspecifiedValueAppearance( wxPGCell( wxT( "<...>" ) ) );
121
122#if wxCHECK_VERSION( 3, 3, 0 )
123 m_grid->SetValidationFailureBehavior( wxPGVFBFlags::StayInProperty );
124#else
125 m_grid->SetValidationFailureBehavior( wxPG_VFB_STAY_IN_PROPERTY );
126#endif
127
128#if wxCHECK_VERSION( 3, 3, 0 )
129 m_grid->AddActionTrigger( wxPGKeyboardAction::NextProperty, WXK_RETURN );
130 m_grid->AddActionTrigger( wxPGKeyboardAction::NextProperty, WXK_NUMPAD_ENTER );
131 m_grid->AddActionTrigger( wxPGKeyboardAction::NextProperty, WXK_DOWN );
132 m_grid->AddActionTrigger( wxPGKeyboardAction::PrevProperty, WXK_UP );
133 m_grid->AddActionTrigger( wxPGKeyboardAction::Edit, WXK_SPACE );
134#else
135 m_grid->AddActionTrigger( wxPG_ACTION_NEXT_PROPERTY, WXK_RETURN );
136 m_grid->AddActionTrigger( wxPG_ACTION_NEXT_PROPERTY, WXK_NUMPAD_ENTER );
137 m_grid->AddActionTrigger( wxPG_ACTION_NEXT_PROPERTY, WXK_DOWN );
138 m_grid->AddActionTrigger( wxPG_ACTION_PREV_PROPERTY, WXK_UP );
139 m_grid->AddActionTrigger( wxPG_ACTION_EDIT, WXK_SPACE );
140#endif
141
142 m_grid->DedicateKey( WXK_RETURN );
143 m_grid->DedicateKey( WXK_NUMPAD_ENTER );
144 m_grid->DedicateKey( WXK_DOWN );
145 m_grid->DedicateKey( WXK_UP );
146 mainSizer->Add( m_grid, 1, wxEXPAND, 5 );
147
148 m_grid->SetCellDisabledTextColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) );
149
150#ifdef __WXGTK__
151 // Needed for dark mode, on wx 3.0 at least.
152 m_grid->SetCaptionTextColour( wxSystemSettings::GetColour( wxSYS_COLOUR_CAPTIONTEXT ) );
153#endif
154
155 SetFont( KIUI::GetDockedPaneFont( this ) );
156
157 SetSizer( mainSizer );
158 Layout();
159
160 m_grid->CenterSplitter();
161
162 // Actual edits are allowed or vetoed per-property based on whether or not it's
163 // something where the label/key should be editable (user fields, custom properties, ...)
164 m_grid->MakeColumnEditable( 0 );
165
166 Bind( wxEVT_PG_ITEM_EXPANDED,
167 [&]( wxPropertyGridEvent& )
168 {
170 } );
171
172 Bind( wxEVT_PG_ITEM_COLLAPSED,
173 [&]( wxPropertyGridEvent& )
174 {
176 } );
177
178 Bind( wxEVT_PG_LABEL_EDIT_BEGIN, &PROPERTIES_PANEL::onLabelEditBegin, this );
179 Bind( wxEVT_PG_LABEL_EDIT_ENDING, &PROPERTIES_PANEL::onLabelEditEnding, this );
180 Bind( wxEVT_PG_RIGHT_CLICK, &PROPERTIES_PANEL::onRightClick, this );
181
182 Connect( wxEVT_CHAR_HOOK, wxKeyEventHandler( PROPERTIES_PANEL::onCharHook ), nullptr, this );
183 Connect( wxEVT_PG_CHANGED, wxPropertyGridEventHandler( PROPERTIES_PANEL::valueChanged ), nullptr, this );
184 Connect( wxEVT_PG_CHANGING, wxPropertyGridEventHandler( PROPERTIES_PANEL::valueChanging ), nullptr, this );
185 Connect( wxEVT_SHOW, wxShowEventHandler( PROPERTIES_PANEL::onShow ), nullptr, this );
186
187 Bind( wxEVT_PG_COL_END_DRAG,
188 [&]( wxPropertyGridEvent& )
189 {
190 m_splitter_key_proportion = static_cast<float>( m_grid->GetSplitterPosition() ) / m_grid->GetSize().x;
192 } );
193
194 Bind( wxEVT_SIZE,
195 [&]( wxSizeEvent& aEvent )
196 {
197 CallAfter( [this]()
198 {
201 } );
202 aEvent.Skip();
203 } );
204
205 m_frame->Bind( EDA_LANG_CHANGED, &PROPERTIES_PANEL::OnLanguageChanged, this );
206
207 Bind( wxEVT_TIMER,
208 [this]( wxTimerEvent& )
209 {
211 },
212 m_deferredRebuildTimer.GetId() );
213}
214
215
217{
218 m_frame->Unbind( EDA_LANG_CHANGED, &PROPERTIES_PANEL::OnLanguageChanged, this );
219}
220
221
222void PROPERTIES_PANEL::OnLanguageChanged( wxCommandEvent& aEvent )
223{
224 if( m_grid->IsEditorFocused() )
225 m_grid->CommitChangesFromEditor();
226
227 m_grid->Clear();
228 m_displayed.clear(); // no ownership of pointers
229
230 UpdateData();
231
232 aEvent.Skip();
233}
234
235
237 m_panel( aPanel )
238{
239 m_panel->m_SuppressGridChangeEvents++;
240}
241
242
244{
245 m_panel->m_SuppressGridChangeEvents--;
246}
247
248
250{
251 SUPPRESS_GRID_CHANGED_EVENTS raii( this );
252
253 // wxPG defers property deletion while one of its events is being processed
254 // (m_processedEvent != nullptr), and wxPropertyGridPageState::DoClear() then
255 // leaves the old rows in place; re-appending the new set on top of them
256 // duplicates every row. This happens when e.g. the context menu (opened from
257 // a grid right-click) removes a property. Re-run once the event has unwound.
258 if( static_cast<PROPERTIES_PANEL_GRID*>( m_grid )->IsProcessingWxPGEvent() )
259 {
261 m_deferredSelection = aSelection;
262 m_deferredRebuildTimer.StartOnce( 1 );
263 return;
264 }
265
266 auto reset =
267 [&]()
268 {
269 if( m_grid->IsEditorFocused() )
270 m_grid->CommitChangesFromEditor();
271
272 m_grid->Clear();
273 m_displayed.clear();
274 };
275
276 if( aSelection.Empty() )
277 {
278 m_caption->SetLabel( _( "No objects selected" ) );
280 reset();
281 return;
282 }
283 else if( aSelection.Size() == 1 )
284 {
285 m_caption->SetLabel( aSelection.Front()->GetFriendlyName() );
286 }
287 else
288 {
289 m_caption->SetLabel( wxString::Format( _( "%d objects selected" ), aSelection.Size() ) );
290 }
291
292 // Get all the selected types
293 std::set<TYPE_ID> types;
294
295 for( EDA_ITEM* item : aSelection )
296 types.insert( TYPE_HASH( *item ) );
297
298 wxCHECK( !types.empty(), /* void */ ); // already guarded above, but Coverity doesn't know that
299
301 std::map<wxString, PROPERTY_BASE*> commonProps;
302 const std::vector<PROPERTY_BASE*>& allProperties = propMgr.GetProperties( *types.begin() );
303
304 for( PROPERTY_BASE* property : allProperties )
305 commonProps.emplace( property->Name(), property );
306
307 std::map<wxString, int> displayOrder;
308 for( const auto& entry : propMgr.GetDisplayOrder( *types.begin() ) )
309 displayOrder.emplace( entry.first->Name(), entry.second );
310
311 std::vector<wxString> groupDisplayOrder = propMgr.GetGroupDisplayOrder( *types.begin() );
312 std::set<wxString> groups( groupDisplayOrder.begin(), groupDisplayOrder.end() );
313
314 // Get all possible properties
315 for( auto itType = std::next( types.begin() ); itType != types.end(); ++itType )
316 {
317 TYPE_ID type = *itType;
318
319 for( const wxString& group : propMgr.GetGroupDisplayOrder( type ) )
320 {
321 if( !groups.count( group ) )
322 {
323 groupDisplayOrder.emplace_back( group );
324 groups.insert( group );
325 }
326 }
327
328 for( auto it = commonProps.begin(); it != commonProps.end(); )
329 {
330 if( PROPERTY_BASE* prop = propMgr.GetProperty( type, it->first ) )
331 {
332 // A dummy property won't have the enum values, etc., so replace them with a "real" property
333 if( it->second->IgnoreValue() )
334 it->second = prop;
335
336 ++it;
337 }
338 else
339 {
340 it = commonProps.erase( it );
341 }
342 }
343 }
344
345 std::vector<const INSPECTABLE*> inspectables( aSelection.begin(), aSelection.end() );
346
347 for( PROPERTY_BASE* prop : propMgr.GetCommonDynamicProperties( inspectables ) )
348 {
349 if( commonProps.contains( prop->Name() ) )
350 continue;
351
352 commonProps.emplace( prop->Name(), prop );
353
354 auto maxOrderIt = std::ranges::max_element( displayOrder,
355 []( const auto& aL, const auto& aR )
356 {
357 return aL.second < aR.second;
358 } );
359 int nextOrder = maxOrderIt == displayOrder.end() ? 0 : maxOrderIt->second + 1;
360 displayOrder.emplace( prop->Name(), nextOrder );
361
362 if( const wxString& dynGroup = prop->Group(); !dynGroup.IsEmpty() && !groups.contains( dynGroup ) )
363 {
364 groupDisplayOrder.emplace_back( dynGroup );
365 groups.insert( dynGroup );
366 }
367 }
368
369 // Always show the Custom Properties group, even when it has no members, because it has the
370 // clearest path to add a custom property (the custom "+" button)
371 if( !groups.contains( _HKI( "Custom Properties" ) ) )
372 {
373 groupDisplayOrder.emplace_back( _HKI( "Custom Properties" ) );
374 groups.insert( _HKI( "Custom Properties" ) );
375 }
376
377 // Show category groups that have an action button if they are forced
378 // (e.g. we show "add custom property" even when no custom properties exist)
379 for( const CATEGORY_BUTTON& entry : m_categoryButtons )
380 {
381 if( !entry.forceCategory || groups.contains( entry.groupKey ) )
382 continue;
383
384 if( entry.enableFunc && !entry.enableFunc() )
385 continue;
386
387 groupDisplayOrder.emplace_back( entry.groupKey );
388 groups.insert( entry.groupKey );
389 }
390
391 bool isLibraryEditor = m_frame->IsType( FRAME_FOOTPRINT_EDITOR )
392 || m_frame->IsType( FRAME_SCH_SYMBOL_EDITOR );
393
394 bool isDesignEditor = m_frame->IsType( FRAME_PCB_EDITOR )
395 || m_frame->IsType( FRAME_SCH );
396
397 std::set<wxString> availableProps;
398
399 // Find a set of properties that is common to all selected items
400 for( auto& [name, property] : commonProps )
401 {
402 if( property->IsHiddenFromPropertiesManager() )
403 continue;
404
405 if( isLibraryEditor && property->IsHiddenFromLibraryEditors() )
406 continue;
407
408 if( isDesignEditor && property->IsHiddenFromDesignEditors() )
409 continue;
410
411 wxVariant dummy;
412 wxPGChoices choices;
413 bool writable;
414
415 if( extractValueAndWritability( aSelection, name, dummy, writable, choices ) )
416 availableProps.insert( name );
417 }
418
419 bool writeable = true;
420 std::set<wxString> existingProps;
421
422 for( wxPropertyGridIterator it = m_grid->GetIterator(); !it.AtEnd(); it.Next() )
423 {
424 wxPGProperty* pgProp = it.GetProperty();
425 wxString name = pgProp->GetName();
426
427 // Store the existing name before checking available properties so we can
428 // remove the properties when they are no longer available
429 existingProps.insert( name );
430
431 if( !availableProps.count( name ) )
432 continue;
433
434 wxVariant commonVal;
435 wxPGChoices choices;
436
437 extractValueAndWritability( aSelection, name, commonVal, writeable, choices );
438
439 if( choices.GetCount() > 0 )
440 pgProp->SetChoices( choices );
441
442 pgProp->SetValue( commonVal );
443 pgProp->ChangeFlag( wxPG_PROP_READONLY, !writeable );
444
445 // If it's no longer writable any pending edits will be lost. Life's tough.
446 if( !writeable && pgProp == m_grid->GetSelection() && m_grid->IsEditorFocused() )
447 m_grid->CommitChangesFromEditor();
448 }
449
450 if( !existingProps.empty() && existingProps == availableProps )
451 return;
452
453 // Some difference exists: start from scratch
454 reset();
455
456 std::map<wxPGProperty*, int> pgPropOrders;
457 std::map<wxString, std::vector<wxPGProperty*>> pgPropGroups;
458
459 for( const wxString& name : availableProps )
460 {
461 PROPERTY_BASE* property = commonProps[name];
462 wxPGProperty* pgProp = createPGProperty( property );
463 wxVariant commonVal;
464 wxPGChoices choices;
465
466 if( !extractValueAndWritability( aSelection, name, commonVal, writeable, choices ) )
467 continue;
468
469 if( pgProp )
470 {
471 if( choices.GetCount() )
472 pgProp->SetChoices( choices );
473
474 pgProp->SetValue( commonVal );
475 pgProp->ChangeFlag( wxPG_PROP_READONLY, !writeable );
476 m_displayed.push_back( property );
477
478 wxASSERT( displayOrder.count( name ) );
479 pgPropOrders[pgProp] = displayOrder[name];
480 pgPropGroups[property->Group()].emplace_back( pgProp );
481 }
482 }
483
484 const wxString unspecifiedGroupCaption = _( "Basic Properties" );
485
486 for( const wxString& groupName : groupDisplayOrder )
487 {
488 if( !pgPropGroups.contains( groupName )
489 && std::ranges::none_of( m_categoryButtons,
490 [&]( const CATEGORY_BUTTON& aButton )
491 {
492 return aButton.groupKey == groupName
493 && ( aButton.forceCategory || groupName == _HKI( "Custom Properties" ) )
494 && ( !aButton.enableFunc || aButton.enableFunc() );
495 } ) )
496 {
497 continue;
498 }
499
500 std::vector<wxPGProperty*> properties;
501
502 if( pgPropGroups.contains( groupName ) )
503 properties = pgPropGroups[groupName];
504
505 wxString groupCaption = wxGetTranslation( groupName );
506
507 auto groupItem = new wxPropertyCategory( groupName.IsEmpty() ? unspecifiedGroupCaption
508 : groupCaption );
509
510 m_grid->Append( groupItem );
511
512 std::sort( properties.begin(), properties.end(),
513 [&]( wxPGProperty*& aFirst, wxPGProperty*& aSecond )
514 {
515 return pgPropOrders[aFirst] < pgPropOrders[aSecond];
516 } );
517
518 for( wxPGProperty* property : properties )
519 m_grid->Append( property );
520 }
521
524}
525
526
527bool PROPERTIES_PANEL::getItemValue( EDA_ITEM* aItem, PROPERTY_BASE* aProperty, wxVariant& aValue )
528{
529 const wxAny& any = aItem->Get( aProperty );
530 bool converted = false;
531
532 if( aProperty->HasChoices() )
533 {
534 // handle enums as ints, since there are no default conversion functions for wxAny
535 int tmp;
536 converted = any.GetAs<int>( &tmp );
537
538 if( converted )
539 aValue = wxVariant( tmp );
540 }
541
542 if( !converted ) // all other types
543 converted = any.GetAs( &aValue );
544
545 if( !converted )
546 {
547 wxString propName = aProperty->Name();
548 propName.Replace( ' ', '_' );
549 wxFAIL_MSG( wxString::Format( wxS( "Could not convert wxAny to wxVariant for %s::%s" ),
550 aItem->GetClass(),
551 propName ) );
552 }
553
554 return converted;
555}
556
557
558bool PROPERTIES_PANEL::extractValueAndWritability( const SELECTION& aSelection, const wxString& aPropName,
559 wxVariant& aValue, bool& aWritable, wxPGChoices& aChoices )
560{
562 bool different = false;
563 bool first = true;
564
565 aWritable = true;
566
567 for( EDA_ITEM* item : aSelection )
568 {
569 PROPERTY_BASE* property = propMgr.GetProperty( item, aPropName );
570
571 if( !property )
572 return false;
573
574 if( !propMgr.IsAvailableFor( TYPE_HASH( *item ), property, item ) )
575 return false;
576
577 if( property->IsHiddenFromPropertiesManager() )
578 return false;
579
580 if( property->IgnoreValue() )
581 continue;
582
583 wxPGChoices choices = property->GetChoices( item );
584
585 if( first )
586 {
587 aChoices = choices;
588 first = false;
589 }
590 else
591 {
592 wxArrayString labels = choices.GetLabels();
593 wxArrayInt values = choices.GetValuesForStrings( labels );
594
595 if( labels != aChoices.GetLabels() || values != aChoices.GetValuesForStrings( labels ) )
596 return false;
597 }
598
599 // If read-only for any of the selection, read-only for the whole selection.
600 if( !propMgr.IsWriteableFor( TYPE_HASH( *item ), property, item ) )
601 aWritable = false;
602
603 wxVariant value;
604
605 if( getItemValue( item, property, value ) )
606 {
607 // Null value indicates different property values between items
608 if( !different && !aValue.IsNull() && value != aValue )
609 {
610 different = true;
611 aValue.MakeNull();
612 }
613 else if( !different )
614 {
615 aValue = value;
616 }
617 }
618 else
619 {
620 // getItemValue returned false -- not available for this item
621 return false;
622 }
623 }
624
625 return true;
626}
627
628
629void PROPERTIES_PANEL::onShow( wxShowEvent& aEvent )
630{
631 if( aEvent.IsShown() )
632 UpdateData();
633
634 aEvent.Skip();
635}
636
637
638void PROPERTIES_PANEL::onLabelEditBegin( wxPropertyGridEvent& aEvent )
639{
640 wxPGProperty* pgProp = aEvent.GetProperty();
641
642 if( !pgProp || !isKeyEditable( pgProp ) )
643 {
644 aEvent.Veto();
645 return;
646 }
647
648 // Remember the original label so an invalid rename can be undone.
649 m_editingOriginalLabel = pgProp->GetLabel();
650}
651
652
653void PROPERTIES_PANEL::onLabelEditEnding( wxPropertyGridEvent& aEvent )
654{
655 wxPGProperty* pgProp = aEvent.GetProperty();
656
657 if( !pgProp || !isKeyEditable( pgProp ) )
658 return;
659
660 const wxString oldName = pgProp->GetBaseName();
661
662 wxTextCtrl* labelEditor = m_grid->GetLabelEditor();
663 wxString newName;
664
665 if( labelEditor )
666 newName = labelEditor->GetValue();
667
668 if( !m_pendingNewKey.IsEmpty() && oldName == m_pendingNewKey )
669 {
670 const wxString pendingKey = m_pendingNewKey;
671
672 m_pendingNewKey.Clear();
673
675 {
676 if( newName.IsEmpty() || isKeyNameInUse( newName ) )
677 onNewItemLeftBlank( pendingKey );
678 else
679 onKeyRenamed( oldName, newName );
680 }
681 else if( newName.IsEmpty() || isKeyNameInUse( newName ) )
682 {
683 CallAfter(
684 [this, pendingKey]()
685 {
686 onNewItemLeftBlank( pendingKey );
687 } );
688 }
689 else
690 {
691 CallAfter(
692 [this, oldName, newName]()
693 {
694 onKeyRenamed( oldName, newName );
695 } );
696 }
697
698 return;
699 }
700
701 if( newName == oldName )
702 return;
703
704 if( newName.IsEmpty() || isKeyNameInUse( newName ) )
705 {
706 const wxString originalLabel = m_editingOriginalLabel;
707
708 CallAfter(
709 [this, oldName, originalLabel, newName]()
710 {
711 for( wxPropertyGridIterator it = m_grid->GetIterator(); !it.AtEnd(); it.Next() )
712 {
713 wxPGProperty* p = it.GetProperty();
714
715 if( p->GetBaseName() == oldName && p->GetLabel() == newName )
716 {
717 p->SetLabel( originalLabel );
718 m_grid->Refresh();
719 break;
720 }
721 }
722 } );
723
724 return;
725 }
726
727 CallAfter(
728 [this, oldName, newName]()
729 {
730 onKeyRenamed( oldName, newName );
731 } );
732}
733
734
735void PROPERTIES_PANEL::onRightClick( wxPropertyGridEvent& aEvent )
736{
737 wxPGProperty* pgProp = aEvent.GetProperty();
738
739 if( !pgProp )
740 return;
741
742 m_contextMenuPropertyName = pgProp->GetBaseName();
743
744 wxMenu* menu = new wxMenu;
745
746 if( !buildContextMenu( *menu, pgProp ) )
747 {
748 delete menu;
749 return;
750 }
751
752 // Defer showing the menu until the property event has finished
753 const wxPoint pos = ScreenToClient( wxGetMousePosition() );
754 CallAfter( [this, menu, pos]()
755 {
756 PopupMenu( menu, pos );
757 delete menu;
758 } );
759}
760
761
763{
764 if( !m_grid->GetLabelEditor() )
765 {
766 m_pendingNewKey.Clear();
767 return;
768 }
769
770 const wxString newName = m_grid->GetLabelEditor()->GetValue();
771 const wxString pendingKey = m_pendingNewKey;
772
774 m_grid->EndLabelEdit( true );
775 m_resolvingPendingKey = false;
776
777 if( pendingKey.IsEmpty() )
778 return;
779
780 if( newName.IsEmpty() || isKeyNameInUse( newName ) )
781 onNewItemLeftBlank( pendingKey );
782 else if( newName != pendingKey )
783 onKeyRenamed( pendingKey, newName );
784}
785
786
787void PROPERTIES_PANEL::beginLabelEdit( const wxString& aKey, bool aStartBlank )
788{
789 for( wxPropertyGridIterator it = m_grid->GetIterator(); !it.AtEnd(); it.Next() )
790 {
791 wxPGProperty* p = it.GetProperty();
792
793 if( !p->IsCategory() && p->GetBaseName() == aKey )
794 {
795 m_grid->SelectProperty( p );
796 m_grid->BeginLabelEdit( 0 );
797
798 if( aStartBlank && m_grid->GetLabelEditor() )
799 m_grid->GetLabelEditor()->SetValue( wxEmptyString );
800
801 break;
802 }
803 }
804}
805
806
807
808
809void PROPERTIES_PANEL::onCharHook( wxKeyEvent& aEvent )
810{
811 if( aEvent.GetKeyCode() == WXK_TAB
812 && ( aEvent.GetModifiers() == wxMOD_NONE || aEvent.GetModifiers() == wxMOD_SHIFT ) )
813 {
814 // wxPropertyGrid hard-codes Tab to focus the current editor and then navigate out of
815 // the grid, so it never steps between properties. Intercept Tab here to commit any
816 // pending edit and move selection to the next (or previous, with Shift) editable
817 // property, wrapping around at the ends.
818
819 // wxPropertyGrid::PerformValidation() fails to return true when the event is vetoed
820 // (though the code looks like it does it). Anyway, for now make sure that we catch
821 // it.
822 m_ValueChangeVetoed = false;
823
824 if( !m_grid->CommitChangesFromEditor() || m_ValueChangeVetoed )
825 return;
826
827 const bool forward = !aEvent.ShiftDown();
828
829 auto isTabStop =
830 []( wxPGProperty* aProp )
831 {
832 return aProp && !aProp->IsCategory() && aProp->IsVisible() && aProp->IsEnabled()
833 && !aProp->HasFlag( wxPG_PROP_READONLY ) && aProp->GetEditorClass();
834 };
835
836 auto findTarget =
837 [&]( wxPropertyGridIterator aIt )
838 {
839 while( !aIt.AtEnd() )
840 {
841 if( isTabStop( aIt.GetProperty() ) )
842 return aIt.GetProperty();
843
844 if( forward )
845 aIt.Next();
846 else
847 aIt.Prev();
848 }
849
850 return static_cast<wxPGProperty*>( nullptr );
851 };
852
853 wxPGProperty* current = m_grid->GetSelectedProperty();
854 wxPGProperty* target = nullptr;
855
856 if( current )
857 {
858 wxPropertyGridIterator it = m_grid->GetIterator( wxPG_ITERATE_VISIBLE, current );
859
860 if( forward )
861 it.Next();
862 else
863 it.Prev();
864
865 target = findTarget( it );
866 }
867
868 // Wrap around (or pick a starting property if nothing is selected)
869 if( !target )
870 {
871 wxPropertyGridIterator it = m_grid->GetIterator( wxPG_ITERATE_VISIBLE,
872 forward ? wxTOP : wxBOTTOM );
873 target = findTarget( it );
874 }
875
876 if( target )
877 m_grid->SelectProperty( target, true );
878 else
879 aEvent.Skip();
880
881 return;
882 }
883
884 if( aEvent.GetKeyCode() == 'C' && aEvent.GetModifiers() == wxMOD_CONTROL )
885 {
886 // Let an open editor run it's own internal copy action
887 if( !m_grid->IsEditorFocused() )
888 {
889 if( wxPGProperty* prop = m_grid->GetSelectedProperty() )
890 {
891 if( wxTheClipboard->Open() )
892 {
893 wxTheClipboard->SetData( new wxTextDataObject( prop->GetValueAsString() ) );
894 wxTheClipboard->Close();
895 return;
896 }
897 }
898 }
899 }
900
901 if( aEvent.GetKeyCode() == WXK_SPACE )
902 {
903 if( wxPGProperty* prop = m_grid->GetSelectedProperty() )
904 {
905 if( prop->GetValueType() == wxT( "bool" ) )
906 {
907 m_grid->SetPropertyValue( prop, !prop->GetValue().GetBool() );
908 return;
909 }
910 }
911 }
912
913 if( aEvent.GetKeyCode() == WXK_RETURN || aEvent.GetKeyCode() == WXK_NUMPAD_ENTER
914 || aEvent.GetKeyCode() == WXK_DOWN || aEvent.GetKeyCode() == WXK_UP )
915 {
916 // wxPropertyGrid::PerformValidation() fails to return true when the event is vetoed
917 // (though the code looks like it does it). Anyway, for now make sure that we catch
918 // it.
919 m_ValueChangeVetoed = false;
920
921 if( !m_grid->CommitChangesFromEditor() || m_ValueChangeVetoed )
922 return;
923
924 CallAfter(
925 [this]()
926 {
927 if( wxPGProperty* prop = m_grid->GetSelectedProperty() )
928 m_grid->SelectProperty( prop, true );
929 } );
930 }
931
932 aEvent.Skip();
933}
934
935
937{
939 m_grid->CenterSplitter();
940 else
941 m_grid->SetSplitterPosition( m_splitter_key_proportion * m_grid->GetSize().x );
942}
943
944
946{
947 m_splitter_key_proportion = aProportion;
949}
950
951
952void PROPERTIES_PANEL::addCategoryButton( const wxString& aGroupKey, const wxString& aTooltip, BITMAPS aBitmap,
953 std::function<void()> aAction, std::function<bool()> aEnableFunc,
954 bool aForceCategory )
955{
956 BITMAP_BUTTON* button = new BITMAP_BUTTON( m_grid, wxID_ANY );
957 button->SetBitmap( KiBitmapBundle( aBitmap ) );
958 button->SetToolTip( aTooltip );
959 button->Hide();
960
961 button->Bind( wxEVT_BUTTON,
962 [this, button]( wxCommandEvent& )
963 {
964 for( CATEGORY_BUTTON& entry : m_categoryButtons )
965 {
966 if( entry.button == button )
967 {
968 entry.action();
969 return;
970 }
971 }
972 } );
973
974 m_categoryButtons.emplace_back( CATEGORY_BUTTON{ .button = button,
975 .groupKey = aGroupKey,
976 .action = std::move( aAction ),
977 .enableFunc = std::move( aEnableFunc ),
978 .forceCategory = aForceCategory } );
979}
980
981
982wxPGProperty* PROPERTIES_PANEL::categoryForGroup( const wxString& aGroupKey ) const
983{
984 const wxString caption = wxGetTranslation( aGroupKey );
985
986 for( wxPropertyGridIterator it = m_grid->GetIterator( wxPG_ITERATE_VISIBLE ); !it.AtEnd(); it.Next() )
987 {
988 if( wxPGProperty* pgProp = it.GetProperty(); pgProp->IsCategory() && pgProp->GetLabel() == caption )
989 return pgProp;
990 }
991
992 return nullptr;
993}
994
995
1000
1001
1003{
1004 for( CATEGORY_BUTTON& entry : m_categoryButtons )
1005 entry.button->Hide();
1006}
1007
1008
1010{
1011 const int rightEdgeBase = m_grid->GetClientSize().x - m_grid->FromDIP( 4 );
1012 int nextRightEdge = rightEdgeBase;
1013 wxString currentRow;
1014 bool haveCurrentRow = false;
1015
1016 for( CATEGORY_BUTTON& entry : m_categoryButtons )
1017 {
1018 if( bool sameRow = haveCurrentRow && currentRow == entry.groupKey; !sameRow )
1019 {
1020 currentRow = entry.groupKey;
1021 haveCurrentRow = true;
1022 nextRightEdge = rightEdgeBase;
1023 }
1024
1025 wxPGProperty* category = categoryForGroup( entry.groupKey );
1026 bool visible = false;
1027 int rowY = 0;
1028 int rowHeight = m_grid->GetRowHeight();
1029
1030 if( category && ( !entry.enableFunc || entry.enableFunc() ) )
1031 {
1032 rowY = m_grid->CalcScrolledPosition( wxPoint( 0, category->GetY() ) ).y;
1033 visible = ( rowY + rowHeight > 0 ) && ( rowY < m_grid->GetClientSize().y );
1034 }
1035
1036 if( !visible )
1037 {
1038 entry.button->Hide();
1039 continue;
1040 }
1041
1042 const wxSize btnSize = entry.button->GetSize();
1043 const int btnY = rowY + ( rowHeight - btnSize.y ) / 2;
1044 const int btnX = nextRightEdge - btnSize.x;
1045
1046 if( !entry.button->IsShown() )
1047 entry.button->Show();
1048
1049 entry.button->SetPosition( wxPoint( std::max( 0, btnX ), std::max( 0, btnY ) ) );
1050 entry.button->Raise();
1051
1052 nextRightEdge = btnX - m_grid->FromDIP( 2 );
1053 }
1054}
const char * name
wxBitmapBundle KiBitmapBundle(BITMAPS aBitmap, int aMinHeight)
Definition bitmap.cpp:106
BITMAPS
A list of all bitmap identifiers.
A bitmap button widget that behaves like an AUI toolbar item's button when it is drawn.
void SetBitmap(const wxBitmapBundle &aBmp)
Set the bitmap shown when the button is enabled.
The base frame for deriving all KiCad main window classes.
A base class for most all the KiCad significant classes used in schematics and boards.
Definition eda_item.h:98
virtual wxString GetFriendlyName() const
Definition eda_item.cpp:565
wxAny Get(PROPERTY_BASE *aProperty) const
virtual wxString GetClass() const =0
Return the class name.
bool m_PropertyGridInitialized
Definition pgm_base.h:392
Enhanced renderer to work around some limitations in wxWidgets 3.0 capabilities.
bool IsProcessingWxPGEvent() const
True while a wxPropertyGrid event (e.g. right-click) is being processed.
void ScrollWindow(int aDx, int aDy, const wxRect *aRect=nullptr) override
PROPERTIES_PANEL_GRID(wxWindow *aParent)
wxString m_contextMenuPropertyName
float m_splitter_key_proportion
Proportion of the grid column splitter that is used for the key column (0.0 - 1.0)
virtual bool isKeyEditable(const wxPGProperty *aPGProp) const
virtual void onKeyRenamed(const wxString &aOldName, const wxString &aNewName)
PROPERTIES_PANEL(wxWindow *aParent, EDA_BASE_FRAME *aFrame)
virtual void valueChanging(wxPropertyGridEvent &aEvent)
wxPropertyGrid * m_grid
wxString m_editingOriginalLabel
virtual void onLabelEditEnding(wxPropertyGridEvent &aEvent)
virtual bool buildContextMenu(wxMenu &aMenu, wxPGProperty *aPGProp)
void beginLabelEdit(const wxString &aKey, bool aStartBlank=false)
virtual bool isKeyNameInUse(const wxString &aName) const
bool m_resolvingPendingKey
True while settling a pending label edit synchronously (see settlePendingLabelEdit).
wxStaticText * m_caption
void SetSplitterProportion(float aProportion)
void onRightClick(wxPropertyGridEvent &aEvent)
void settlePendingLabelEdit()
Synchronously ends any in-progress label edit; potentially canceling a newly added row.
std::vector< PROPERTY_BASE * > m_displayed
virtual void OnLanguageChanged(wxCommandEvent &aEvent)
virtual void UpdateData()=0
friend class PROPERTIES_PANEL_GRID
virtual void onLabelEditBegin(wxPropertyGridEvent &aEvent)
virtual void valueChanged(wxPropertyGridEvent &aEvent)
virtual void onNewItemLeftBlank(const wxString &aKey)
bool extractValueAndWritability(const SELECTION &aSelection, const wxString &aPropName, wxVariant &aValue, bool &aWritable, wxPGChoices &aChoices)
Processes a selection and determines whether the given property should be available or not and what t...
wxTimer m_deferredRebuildTimer
Not CallAfter(): on macOS the pending-event queue is drained from inside tool events,...
SELECTION m_deferredSelection
virtual bool getItemValue(EDA_ITEM *aItem, PROPERTY_BASE *aProperty, wxVariant &aValue)
Utility to fetch a property value and convert to wxVariant Precondition: aItem is known to have prope...
wxPGProperty * categoryForGroup(const wxString &aGroupKey) const
Find the caption row for the given untranslated group name, if present.
virtual void rebuildProperties(const SELECTION &aSelection)
Generates the property grid for a given selection of items.
void onCharHook(wxKeyEvent &aEvent)
virtual wxPGProperty * createPGProperty(const PROPERTY_BASE *aProperty) const =0
void addCategoryButton(const wxString &aGroupKey, const wxString &aTooltip, BITMAPS aBitmap, std::function< void()> aAction, std::function< bool()> aEnableFunc=nullptr, bool aForceCategory=false)
Registers an overlay button on a category caption row.
void onShow(wxShowEvent &aEvent)
void hideCategoryButtons()
Hides all overlay buttons (used when no selection or a rebuild is deferred).
std::vector< CATEGORY_BUTTON > m_categoryButtons
void updateCategoryButtons()
Updates overlay button visibility/positions; called after rebuilds and on scroll.
EDA_BASE_FRAME * m_frame
wxString m_pendingNewKey
Key of a freshly-added blank field/custom property awaiting a name from the user.
virtual bool HasChoices() const
Return true if this PROPERTY has a limited set of possible values.
Definition property.h:259
wxPGChoices GetChoices(INSPECTABLE *aObject) const
Definition property.h:281
const wxString & Name() const
Definition property.h:221
Provide class metadata.
const std::vector< PROPERTY_BASE * > & GetProperties(TYPE_ID aType) const
Return all properties for a specific type.
bool IsWriteableFor(TYPE_ID aItemClass, PROPERTY_BASE *aProp, INSPECTABLE *aItem)
Checks overriden availability and original availability of a property, returns false if the property ...
static PROPERTY_MANAGER & Instance()
PROPERTY_BASE * GetProperty(TYPE_ID aType, const wxString &aProperty) const
Return a property for a specific type.
bool IsAvailableFor(TYPE_ID aItemClass, PROPERTY_BASE *aProp, INSPECTABLE *aItem)
Checks overriden availability and original availability of a property, returns false if the property ...
const std::map< PROPERTY_BASE *, int > & GetDisplayOrder(TYPE_ID aType) const
std::vector< PROPERTY_BASE * > GetCommonDynamicProperties(const std::vector< const INSPECTABLE * > &aObjects) const
Return the dynamic properties of the first object whose names every other object also has,...
const std::vector< wxString > & GetGroupDisplayOrder(TYPE_ID aType) const
ITER end()
Definition selection.h:76
ITER begin()
Definition selection.h:75
EDA_ITEM * Front() const
Definition selection.h:176
int Size() const
Returns the number of selected parts.
Definition selection.h:120
bool Empty() const
Checks if there is anything selected.
Definition selection.h:114
SUPPRESS_GRID_CHANGED_EVENTS(PROPERTIES_PANEL *aPanel)
A type-safe container of any type.
Definition ki_any.h:92
#define _(s)
Base window classes and related definitions.
@ FRAME_PCB_EDITOR
Definition frame_type.h:38
@ FRAME_SCH_SYMBOL_EDITOR
Definition frame_type.h:31
@ FRAME_SCH
Definition frame_type.h:30
@ FRAME_FOOTPRINT_EDITOR
Definition frame_type.h:39
Some functions to handle hotkeys in KiCad.
#define APIIMPORT
KICOMMON_API wxFont GetDockedPaneFont(wxWindow *aWindow)
#define _HKI(x)
Definition page_info.cpp:40
PGM_BASE & Pgm()
The global program "get" accessor.
see class PGM_BASE
APIIMPORT wxPGGlobalVarsClass * wxPGGlobalVars
#define TYPE_HASH(x)
Macro to generate unique identifier for a type.
Definition property.h:74
size_t TYPE_ID
Unique type identifier.
std::vector< FAB_LAYER_COLOR > dummy