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 ),
84{
85 wxBoxSizer* mainSizer = new wxBoxSizer( wxVERTICAL );
86
87#if !wxCHECK_VERSION( 3, 3, 0 )
88 // on some platforms wxPGGlobalVars is initialized automatically,
89 // but others need an explicit init
90 if( !wxPGGlobalVars )
91 wxPGInitResourceModule();
92#endif
93
94 // See https://gitlab.com/kicad/code/kicad/-/issues/12297
95 // and https://github.com/wxWidgets/wxWidgets/issues/11787
96 if( wxPGGlobalVars->m_mapEditorClasses.empty() )
97 {
98 wxPGEditor_TextCtrl = nullptr;
99 wxPGEditor_Choice = nullptr;
100 wxPGEditor_ComboBox = nullptr;
101 wxPGEditor_TextCtrlAndButton = nullptr;
102 wxPGEditor_CheckBox = nullptr;
103 wxPGEditor_ChoiceAndButton = nullptr;
104 wxPGEditor_SpinCtrl = nullptr;
105 wxPGEditor_DatePickerCtrl = nullptr;
106 }
107
108 if( !Pgm().m_PropertyGridInitialized )
109 {
110 delete wxPGGlobalVars->m_defaultRenderer;
111 wxPGGlobalVars->m_defaultRenderer = new PG_CELL_RENDERER();
113 }
114
115 m_caption = new wxStaticText( this, wxID_ANY, _( "No objects selected" ) );
116 mainSizer->Add( m_caption, 0, wxALL | wxEXPAND, 5 );
117
118 m_grid = new PROPERTIES_PANEL_GRID( this );
119 m_grid->SetUnspecifiedValueAppearance( wxPGCell( wxT( "<...>" ) ) );
120
121#if wxCHECK_VERSION( 3, 3, 0 )
122 m_grid->SetValidationFailureBehavior( wxPGVFBFlags::MarkCell );
123#else
124 m_grid->SetValidationFailureBehavior( wxPG_VFB_MARK_CELL );
125#endif
126
127#if wxCHECK_VERSION( 3, 3, 0 )
128 m_grid->AddActionTrigger( wxPGKeyboardAction::NextProperty, WXK_RETURN );
129 m_grid->AddActionTrigger( wxPGKeyboardAction::NextProperty, WXK_NUMPAD_ENTER );
130 m_grid->AddActionTrigger( wxPGKeyboardAction::NextProperty, WXK_DOWN );
131 m_grid->AddActionTrigger( wxPGKeyboardAction::PrevProperty, WXK_UP );
132 m_grid->AddActionTrigger( wxPGKeyboardAction::Edit, WXK_SPACE );
133#else
134 m_grid->AddActionTrigger( wxPG_ACTION_NEXT_PROPERTY, WXK_RETURN );
135 m_grid->AddActionTrigger( wxPG_ACTION_NEXT_PROPERTY, WXK_NUMPAD_ENTER );
136 m_grid->AddActionTrigger( wxPG_ACTION_NEXT_PROPERTY, WXK_DOWN );
137 m_grid->AddActionTrigger( wxPG_ACTION_PREV_PROPERTY, WXK_UP );
138 m_grid->AddActionTrigger( wxPG_ACTION_EDIT, WXK_SPACE );
139#endif
140
141 m_grid->DedicateKey( WXK_RETURN );
142 m_grid->DedicateKey( WXK_NUMPAD_ENTER );
143 m_grid->DedicateKey( WXK_DOWN );
144 m_grid->DedicateKey( WXK_UP );
145 mainSizer->Add( m_grid, 1, wxEXPAND, 5 );
146
147 m_grid->SetCellDisabledTextColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) );
148
149#ifdef __WXGTK__
150 // Needed for dark mode, on wx 3.0 at least.
151 m_grid->SetCaptionTextColour( wxSystemSettings::GetColour( wxSYS_COLOUR_CAPTIONTEXT ) );
152#endif
153
154 SetFont( KIUI::GetDockedPaneFont( this ) );
155
156 SetSizer( mainSizer );
157 Layout();
158
159 m_grid->CenterSplitter();
160
161 // Actual edits are allowed or vetoed per-property based on whether or not it's
162 // something where the label/key should be editable (user fields, custom properties, ...)
163 m_grid->MakeColumnEditable( 0 );
164
165 Bind( wxEVT_PG_ITEM_EXPANDED,
166 [&]( wxPropertyGridEvent& )
167 {
169 } );
170
171 Bind( wxEVT_PG_ITEM_COLLAPSED,
172 [&]( wxPropertyGridEvent& )
173 {
175 } );
176
177 Bind( wxEVT_PG_LABEL_EDIT_BEGIN, &PROPERTIES_PANEL::onLabelEditBegin, this );
178 Bind( wxEVT_PG_LABEL_EDIT_ENDING, &PROPERTIES_PANEL::onLabelEditEnding, this );
179 Bind( wxEVT_PG_RIGHT_CLICK, &PROPERTIES_PANEL::onRightClick, this );
180
181 Connect( wxEVT_CHAR_HOOK, wxKeyEventHandler( PROPERTIES_PANEL::onCharHook ), nullptr, this );
182 Connect( wxEVT_PG_CHANGED, wxPropertyGridEventHandler( PROPERTIES_PANEL::valueChanged ), nullptr, this );
183 Connect( wxEVT_PG_CHANGING, wxPropertyGridEventHandler( PROPERTIES_PANEL::valueChanging ), nullptr, this );
184 Connect( wxEVT_SHOW, wxShowEventHandler( PROPERTIES_PANEL::onShow ), nullptr, this );
185
186 Bind( wxEVT_PG_COL_END_DRAG,
187 [&]( wxPropertyGridEvent& )
188 {
189 m_splitter_key_proportion = static_cast<float>( m_grid->GetSplitterPosition() ) / m_grid->GetSize().x;
191 } );
192
193 Bind( wxEVT_SIZE,
194 [&]( wxSizeEvent& aEvent )
195 {
196 CallAfter( [this]()
197 {
200 } );
201 aEvent.Skip();
202 } );
203
204 m_frame->Bind( EDA_LANG_CHANGED, &PROPERTIES_PANEL::OnLanguageChanged, this );
205}
206
207
209{
210 m_frame->Unbind( EDA_LANG_CHANGED, &PROPERTIES_PANEL::OnLanguageChanged, this );
211}
212
213
214void PROPERTIES_PANEL::OnLanguageChanged( wxCommandEvent& aEvent )
215{
216 if( m_grid->IsEditorFocused() )
217 m_grid->CommitChangesFromEditor();
218
219 m_grid->Clear();
220 m_displayed.clear(); // no ownership of pointers
221
222 UpdateData();
223
224 aEvent.Skip();
225}
226
227
229 m_panel( aPanel )
230{
231 m_panel->m_SuppressGridChangeEvents++;
232}
233
234
236{
237 m_panel->m_SuppressGridChangeEvents--;
238}
239
240
242{
243 SUPPRESS_GRID_CHANGED_EVENTS raii( this );
244
245 // wxPG defers property deletion while one of its events is being processed
246 // (m_processedEvent != nullptr), and wxPropertyGridPageState::DoClear() then
247 // leaves the old rows in place; re-appending the new set on top of them
248 // duplicates every row. This happens when e.g. the context menu (opened from
249 // a grid right-click) removes a property. Re-run once the event has unwound.
250 if( static_cast<PROPERTIES_PANEL_GRID*>( m_grid )->IsProcessingWxPGEvent() )
251 {
253 CallAfter( [this, aSelection]()
254 {
255 rebuildProperties( aSelection );
256 } );
257 return;
258 }
259
260 auto reset =
261 [&]()
262 {
263 if( m_grid->IsEditorFocused() )
264 m_grid->CommitChangesFromEditor();
265
266 m_grid->Clear();
267 m_displayed.clear();
268 };
269
270 if( aSelection.Empty() )
271 {
272 m_caption->SetLabel( _( "No objects selected" ) );
274 reset();
275 return;
276 }
277 else if( aSelection.Size() == 1 )
278 {
279 m_caption->SetLabel( aSelection.Front()->GetFriendlyName() );
280 }
281 else
282 {
283 m_caption->SetLabel( wxString::Format( _( "%d objects selected" ), aSelection.Size() ) );
284 }
285
286 // Get all the selected types
287 std::set<TYPE_ID> types;
288
289 for( EDA_ITEM* item : aSelection )
290 types.insert( TYPE_HASH( *item ) );
291
292 wxCHECK( !types.empty(), /* void */ ); // already guarded above, but Coverity doesn't know that
293
295 std::map<wxString, PROPERTY_BASE*> commonProps;
296 const std::vector<PROPERTY_BASE*>& allProperties = propMgr.GetProperties( *types.begin() );
297
298 for( PROPERTY_BASE* property : allProperties )
299 commonProps.emplace( property->Name(), property );
300
301 std::map<wxString, int> displayOrder;
302 for( const auto& entry : propMgr.GetDisplayOrder( *types.begin() ) )
303 displayOrder.emplace( entry.first->Name(), entry.second );
304
305 std::vector<wxString> groupDisplayOrder = propMgr.GetGroupDisplayOrder( *types.begin() );
306 std::set<wxString> groups( groupDisplayOrder.begin(), groupDisplayOrder.end() );
307
308 // Get all possible properties
309 for( auto itType = std::next( types.begin() ); itType != types.end(); ++itType )
310 {
311 TYPE_ID type = *itType;
312
313 for( const wxString& group : propMgr.GetGroupDisplayOrder( type ) )
314 {
315 if( !groups.count( group ) )
316 {
317 groupDisplayOrder.emplace_back( group );
318 groups.insert( group );
319 }
320 }
321
322 for( auto it = commonProps.begin(); it != commonProps.end(); )
323 {
324 if( PROPERTY_BASE* prop = propMgr.GetProperty( type, it->first ) )
325 {
326 // A dummy property won't have the enum values, etc., so replace them with a "real" property
327 if( it->second->IgnoreValue() )
328 it->second = prop;
329
330 ++it;
331 }
332 else
333 {
334 it = commonProps.erase( it );
335 }
336 }
337 }
338
339 for( const EDA_ITEM* item : aSelection )
340 {
341 for( PROPERTY_BASE* prop : item->GetDynamicProperties() )
342 {
343 bool commonToAll = true;
344
345 for( const EDA_ITEM* other : aSelection )
346 {
347 std::vector<PROPERTY_BASE*> otherProps = other->GetDynamicProperties();
348
349 if( std::ranges::none_of( otherProps,
350 [&]( PROPERTY_BASE* p )
351 {
352 return p->Name() == prop->Name();
353 } ) )
354 {
355 commonToAll = false;
356 break;
357 }
358 }
359
360 if( !commonToAll || commonProps.contains( prop->Name() ) )
361 continue;
362
363 commonProps.emplace( prop->Name(), prop );
364
365 auto maxOrderIt = std::ranges::max_element( displayOrder,
366 []( const auto& aL, const auto& aR )
367 {
368 return aL.second < aR.second;
369 } );
370 int nextOrder = maxOrderIt == displayOrder.end() ? 0 : maxOrderIt->second + 1;
371 displayOrder.emplace( prop->Name(), nextOrder );
372
373 if( const wxString& dynGroup = prop->Group(); !dynGroup.IsEmpty() && !groups.contains( dynGroup ) )
374 {
375 groupDisplayOrder.emplace_back( dynGroup );
376 groups.insert( dynGroup );
377 }
378 }
379 }
380
381 // Always show the Custom Properties group, even when it has no members, because it has the
382 // clearest path to add a custom property (the custom "+" button)
383 if( !groups.contains( _HKI( "Custom Properties" ) ) )
384 {
385 groupDisplayOrder.emplace_back( _HKI( "Custom Properties" ) );
386 groups.insert( _HKI( "Custom Properties" ) );
387 }
388
389 // Show category groups that have an action button if they are forced
390 // (e.g. we show "add custom property" even when no custom properties exist)
391 for( const CATEGORY_BUTTON& entry : m_categoryButtons )
392 {
393 if( !entry.forceCategory || groups.contains( entry.groupKey ) )
394 continue;
395
396 if( entry.enableFunc && !entry.enableFunc() )
397 continue;
398
399 groupDisplayOrder.emplace_back( entry.groupKey );
400 groups.insert( entry.groupKey );
401 }
402
403 bool isLibraryEditor = m_frame->IsType( FRAME_FOOTPRINT_EDITOR )
404 || m_frame->IsType( FRAME_SCH_SYMBOL_EDITOR );
405
406 bool isDesignEditor = m_frame->IsType( FRAME_PCB_EDITOR )
407 || m_frame->IsType( FRAME_SCH );
408
409 std::set<wxString> availableProps;
410
411 // Find a set of properties that is common to all selected items
412 for( auto& [name, property] : commonProps )
413 {
414 if( property->IsHiddenFromPropertiesManager() )
415 continue;
416
417 if( isLibraryEditor && property->IsHiddenFromLibraryEditors() )
418 continue;
419
420 if( isDesignEditor && property->IsHiddenFromDesignEditors() )
421 continue;
422
423 wxVariant dummy;
424 wxPGChoices choices;
425 bool writable;
426
427 if( extractValueAndWritability( aSelection, name, dummy, writable, choices ) )
428 availableProps.insert( name );
429 }
430
431 bool writeable = true;
432 std::set<wxString> existingProps;
433
434 for( wxPropertyGridIterator it = m_grid->GetIterator(); !it.AtEnd(); it.Next() )
435 {
436 wxPGProperty* pgProp = it.GetProperty();
437 wxString name = pgProp->GetName();
438
439 // Store the existing name before checking available properties so we can
440 // remove the properties when they are no longer available
441 existingProps.insert( name );
442
443 if( !availableProps.count( name ) )
444 continue;
445
446 wxVariant commonVal;
447 wxPGChoices choices;
448
449 extractValueAndWritability( aSelection, name, commonVal, writeable, choices );
450
451 if( choices.GetCount() > 0 )
452 pgProp->SetChoices( choices );
453
454 pgProp->SetValue( commonVal );
455 pgProp->ChangeFlag( wxPG_PROP_READONLY, !writeable );
456 }
457
458 if( !existingProps.empty() && existingProps == availableProps )
459 return;
460
461 // Some difference exists: start from scratch
462 reset();
463
464 std::map<wxPGProperty*, int> pgPropOrders;
465 std::map<wxString, std::vector<wxPGProperty*>> pgPropGroups;
466
467 for( const wxString& name : availableProps )
468 {
469 PROPERTY_BASE* property = commonProps[name];
470 wxPGProperty* pgProp = createPGProperty( property );
471 wxVariant commonVal;
472 wxPGChoices choices;
473
474 if( !extractValueAndWritability( aSelection, name, commonVal, writeable, choices ) )
475 continue;
476
477 if( pgProp )
478 {
479 if( choices.GetCount() )
480 pgProp->SetChoices( choices );
481
482 pgProp->SetValue( commonVal );
483 pgProp->ChangeFlag( wxPG_PROP_READONLY, !writeable );
484 m_displayed.push_back( property );
485
486 wxASSERT( displayOrder.count( name ) );
487 pgPropOrders[pgProp] = displayOrder[name];
488 pgPropGroups[property->Group()].emplace_back( pgProp );
489 }
490 }
491
492 const wxString unspecifiedGroupCaption = _( "Basic Properties" );
493
494 for( const wxString& groupName : groupDisplayOrder )
495 {
496 if( groupName != _HKI( "Custom Properties" ) && !pgPropGroups.contains( groupName ) )
497 continue;
498
499 std::vector<wxPGProperty*> properties;
500
501 if( pgPropGroups.contains( groupName ) )
502 properties = pgPropGroups[groupName];
503
504 wxString groupCaption = wxGetTranslation( groupName );
505
506 auto groupItem = new wxPropertyCategory( groupName.IsEmpty() ? unspecifiedGroupCaption
507 : groupCaption );
508
509 m_grid->Append( groupItem );
510
511 std::sort( properties.begin(), properties.end(),
512 [&]( wxPGProperty*& aFirst, wxPGProperty*& aSecond )
513 {
514 return pgPropOrders[aFirst] < pgPropOrders[aSecond];
515 } );
516
517 for( wxPGProperty* property : properties )
518 m_grid->Append( property );
519 }
520
523}
524
525
526bool PROPERTIES_PANEL::getItemValue( EDA_ITEM* aItem, PROPERTY_BASE* aProperty, wxVariant& aValue )
527{
528 const wxAny& any = aItem->Get( aProperty );
529 bool converted = false;
530
531 if( aProperty->HasChoices() )
532 {
533 // handle enums as ints, since there are no default conversion functions for wxAny
534 int tmp;
535 converted = any.GetAs<int>( &tmp );
536
537 if( converted )
538 aValue = wxVariant( tmp );
539 }
540
541 if( !converted ) // all other types
542 converted = any.GetAs( &aValue );
543
544 if( !converted )
545 {
546 wxString propName = aProperty->Name();
547 propName.Replace( ' ', '_' );
548 wxFAIL_MSG( wxString::Format( wxS( "Could not convert wxAny to wxVariant for %s::%s" ),
549 aItem->GetClass(),
550 propName ) );
551 }
552
553 return converted;
554}
555
556
557bool PROPERTIES_PANEL::extractValueAndWritability( const SELECTION& aSelection, const wxString& aPropName,
558 wxVariant& aValue, bool& aWritable, wxPGChoices& aChoices )
559{
561 bool different = false;
562 bool first = true;
563
564 aWritable = true;
565
566 for( EDA_ITEM* item : aSelection )
567 {
568 PROPERTY_BASE* property = propMgr.GetProperty( item, aPropName );
569
570 if( !property )
571 return false;
572
573 if( !propMgr.IsAvailableFor( TYPE_HASH( *item ), property, item ) )
574 return false;
575
576 if( property->IsHiddenFromPropertiesManager() )
577 return false;
578
579 wxPGChoices choices = property->GetChoices( item );
580
581 if( first )
582 {
583 aChoices = choices;
584 first = false;
585 }
586 else
587 {
588 wxArrayString labels = choices.GetLabels();
589 wxArrayInt values = choices.GetValuesForStrings( labels );
590
591 if( labels != aChoices.GetLabels() || values != aChoices.GetValuesForStrings( labels ) )
592 return false;
593 }
594
595 // If read-only for any of the selection, read-only for the whole selection.
596 if( !propMgr.IsWriteableFor( TYPE_HASH( *item ), property, item ) )
597 aWritable = false;
598
599 wxVariant value;
600
601 if( getItemValue( item, property, value ) )
602 {
603 if( property->IgnoreValue() )
604 continue;
605
606 // Null value indicates different property values between items
607 if( !different && !aValue.IsNull() && value != aValue )
608 {
609 different = true;
610 aValue.MakeNull();
611 }
612 else if( !different )
613 {
614 aValue = value;
615 }
616 }
617 else
618 {
619 // getItemValue returned false -- not available for this item
620 return false;
621 }
622 }
623
624 return true;
625}
626
627
628void PROPERTIES_PANEL::onShow( wxShowEvent& aEvent )
629{
630 if( aEvent.IsShown() )
631 UpdateData();
632
633 aEvent.Skip();
634}
635
636
637void PROPERTIES_PANEL::onLabelEditBegin( wxPropertyGridEvent& aEvent )
638{
639 wxPGProperty* pgProp = aEvent.GetProperty();
640
641 if( !pgProp || !isKeyEditable( pgProp ) )
642 {
643 aEvent.Veto();
644 return;
645 }
646
647 // Remember the original label so an invalid rename can be undone.
648 m_editingOriginalLabel = pgProp->GetLabel();
649}
650
651
652void PROPERTIES_PANEL::onLabelEditEnding( wxPropertyGridEvent& aEvent )
653{
654 wxPGProperty* pgProp = aEvent.GetProperty();
655
656 if( !pgProp || !isKeyEditable( pgProp ) )
657 return;
658
659 const wxString oldName = pgProp->GetBaseName();
660
661 wxTextCtrl* labelEditor = m_grid->GetLabelEditor();
662 wxString newName;
663
664 if( labelEditor )
665 newName = labelEditor->GetValue();
666
667 if( !m_pendingNewKey.IsEmpty() && oldName == m_pendingNewKey )
668 {
669 const wxString pendingKey = m_pendingNewKey;
670
671 m_pendingNewKey.Clear();
672
674 {
675 if( newName.IsEmpty() || isKeyNameInUse( newName ) )
676 onNewItemLeftBlank( pendingKey );
677 else
678 onKeyRenamed( oldName, newName );
679 }
680 else if( newName.IsEmpty() || isKeyNameInUse( newName ) )
681 {
682 CallAfter(
683 [this, pendingKey]()
684 {
685 onNewItemLeftBlank( pendingKey );
686 } );
687 }
688 else
689 {
690 CallAfter(
691 [this, oldName, newName]()
692 {
693 onKeyRenamed( oldName, newName );
694 } );
695 }
696
697 return;
698 }
699
700 if( newName == oldName )
701 return;
702
703 if( newName.IsEmpty() || isKeyNameInUse( newName ) )
704 {
705 const wxString originalLabel = m_editingOriginalLabel;
706
707 CallAfter(
708 [this, oldName, originalLabel, newName]()
709 {
710 for( wxPropertyGridIterator it = m_grid->GetIterator(); !it.AtEnd(); it.Next() )
711 {
712 wxPGProperty* p = it.GetProperty();
713
714 if( p->GetBaseName() == oldName && p->GetLabel() == newName )
715 {
716 p->SetLabel( originalLabel );
717 m_grid->Refresh();
718 break;
719 }
720 }
721 } );
722
723 return;
724 }
725
726 CallAfter(
727 [this, oldName, newName]()
728 {
729 onKeyRenamed( oldName, newName );
730 } );
731}
732
733
734void PROPERTIES_PANEL::onRightClick( wxPropertyGridEvent& aEvent )
735{
736 wxPGProperty* pgProp = aEvent.GetProperty();
737
738 if( !pgProp )
739 return;
740
741 m_contextMenuPropertyName = pgProp->GetBaseName();
742
743 wxMenu* menu = new wxMenu;
744
745 if( !buildContextMenu( *menu, pgProp ) )
746 {
747 delete menu;
748 return;
749 }
750
751 // Defer showing the menu until the property event has finished
752 const wxPoint pos = ScreenToClient( wxGetMousePosition() );
753 CallAfter( [this, menu, pos]()
754 {
755 PopupMenu( menu, pos );
756 delete menu;
757 } );
758}
759
760
762{
763 if( !m_grid->GetLabelEditor() )
764 {
765 m_pendingNewKey.Clear();
766 return;
767 }
768
769 const wxString newName = m_grid->GetLabelEditor()->GetValue();
770 const wxString pendingKey = m_pendingNewKey;
771
773 m_grid->EndLabelEdit( true );
774 m_resolvingPendingKey = false;
775
776 if( pendingKey.IsEmpty() )
777 return;
778
779 if( newName.IsEmpty() || isKeyNameInUse( newName ) )
780 onNewItemLeftBlank( pendingKey );
781 else if( newName != pendingKey )
782 onKeyRenamed( pendingKey, newName );
783}
784
785
786void PROPERTIES_PANEL::beginLabelEdit( const wxString& aKey, bool aStartBlank )
787{
788 for( wxPropertyGridIterator it = m_grid->GetIterator(); !it.AtEnd(); it.Next() )
789 {
790 wxPGProperty* p = it.GetProperty();
791
792 if( !p->IsCategory() && p->GetBaseName() == aKey )
793 {
794 m_grid->SelectProperty( p );
795 m_grid->BeginLabelEdit( 0 );
796
797 if( aStartBlank && m_grid->GetLabelEditor() )
798 m_grid->GetLabelEditor()->SetValue( wxEmptyString );
799
800 break;
801 }
802 }
803}
804
805
806
807
808void PROPERTIES_PANEL::onCharHook( wxKeyEvent& aEvent )
809{
810 if( aEvent.GetKeyCode() == WXK_TAB
811 && ( aEvent.GetModifiers() == wxMOD_NONE || aEvent.GetModifiers() == wxMOD_SHIFT ) )
812 {
813 // wxPropertyGrid hard-codes Tab to focus the current editor and then navigate out of
814 // the grid, so it never steps between properties. Intercept Tab here to commit any
815 // pending edit and move selection to the next (or previous, with Shift) editable
816 // property, wrapping around at the ends.
817 m_grid->CommitChangesFromEditor();
818
819 const bool forward = !aEvent.ShiftDown();
820
821 auto isTabStop =
822 []( wxPGProperty* aProp )
823 {
824 return aProp && !aProp->IsCategory() && aProp->IsVisible() && aProp->IsEnabled()
825 && !aProp->HasFlag( wxPG_PROP_READONLY ) && aProp->GetEditorClass();
826 };
827
828 auto findTarget =
829 [&]( wxPropertyGridIterator aIt )
830 {
831 while( !aIt.AtEnd() )
832 {
833 if( isTabStop( aIt.GetProperty() ) )
834 return aIt.GetProperty();
835
836 if( forward )
837 aIt.Next();
838 else
839 aIt.Prev();
840 }
841
842 return static_cast<wxPGProperty*>( nullptr );
843 };
844
845 wxPGProperty* current = m_grid->GetSelectedProperty();
846 wxPGProperty* target = nullptr;
847
848 if( current )
849 {
850 wxPropertyGridIterator it = m_grid->GetIterator( wxPG_ITERATE_VISIBLE, current );
851
852 if( forward )
853 it.Next();
854 else
855 it.Prev();
856
857 target = findTarget( it );
858 }
859
860 // Wrap around (or pick a starting property if nothing is selected)
861 if( !target )
862 {
863 wxPropertyGridIterator it = m_grid->GetIterator( wxPG_ITERATE_VISIBLE,
864 forward ? wxTOP : wxBOTTOM );
865 target = findTarget( it );
866 }
867
868 if( target )
869 m_grid->SelectProperty( target, true );
870 else
871 aEvent.Skip();
872
873 return;
874 }
875
876 if( aEvent.GetKeyCode() == 'C' && aEvent.GetModifiers() == wxMOD_CONTROL )
877 {
878 if( wxPGProperty* prop = m_grid->GetSelectedProperty() )
879 {
880 if( wxTheClipboard->Open() )
881 {
882 wxTheClipboard->SetData( new wxTextDataObject( prop->GetValueAsString() ) );
883 wxTheClipboard->Close();
884 return;
885 }
886 }
887 }
888
889 if( aEvent.GetKeyCode() == WXK_SPACE )
890 {
891 if( wxPGProperty* prop = m_grid->GetSelectedProperty() )
892 {
893 if( prop->GetValueType() == wxT( "bool" ) )
894 {
895 m_grid->SetPropertyValue( prop, !prop->GetValue().GetBool() );
896 return;
897 }
898 }
899 }
900
901 if( aEvent.GetKeyCode() == WXK_RETURN || aEvent.GetKeyCode() == WXK_NUMPAD_ENTER
902 || aEvent.GetKeyCode() == WXK_DOWN || aEvent.GetKeyCode() == WXK_UP )
903 {
904 m_grid->CommitChangesFromEditor();
905
906 CallAfter( [this]()
907 {
908 m_grid->SelectProperty( m_grid->GetSelectedProperty(), true );
909 } );
910 }
911
912 aEvent.Skip();
913}
914
915
917{
919 m_grid->CenterSplitter();
920 else
921 m_grid->SetSplitterPosition( m_splitter_key_proportion * m_grid->GetSize().x );
922}
923
924
926{
927 m_splitter_key_proportion = aProportion;
929}
930
931
932void PROPERTIES_PANEL::addCategoryButton( const wxString& aGroupKey, const wxString& aTooltip, BITMAPS aBitmap,
933 std::function<void()> aAction, std::function<bool()> aEnableFunc,
934 bool aForceCategory )
935{
936 BITMAP_BUTTON* button = new BITMAP_BUTTON( m_grid, wxID_ANY );
937 button->SetBitmap( KiBitmapBundle( aBitmap ) );
938 button->SetToolTip( aTooltip );
939 button->Hide();
940
941 button->Bind( wxEVT_BUTTON,
942 [this, button]( wxCommandEvent& )
943 {
944 for( CATEGORY_BUTTON& entry : m_categoryButtons )
945 {
946 if( entry.button == button )
947 {
948 entry.action();
949 return;
950 }
951 }
952 } );
953
954 m_categoryButtons.emplace_back( CATEGORY_BUTTON{ .button = button,
955 .groupKey = aGroupKey,
956 .action = std::move( aAction ),
957 .enableFunc = std::move( aEnableFunc ),
958 .forceCategory = aForceCategory } );
959}
960
961
962wxPGProperty* PROPERTIES_PANEL::categoryForGroup( const wxString& aGroupKey ) const
963{
964 const wxString caption = wxGetTranslation( aGroupKey );
965
966 for( wxPropertyGridIterator it = m_grid->GetIterator( wxPG_ITERATE_VISIBLE ); !it.AtEnd(); it.Next() )
967 {
968 if( wxPGProperty* pgProp = it.GetProperty(); pgProp->IsCategory() && pgProp->GetLabel() == caption )
969 return pgProp;
970 }
971
972 return nullptr;
973}
974
975
980
981
983{
984 for( CATEGORY_BUTTON& entry : m_categoryButtons )
985 entry.button->Hide();
986}
987
988
990{
991 const int rightEdgeBase = m_grid->GetClientSize().x - m_grid->FromDIP( 4 );
992 int nextRightEdge = rightEdgeBase;
993 wxString currentRow;
994 bool haveCurrentRow = false;
995
996 for( CATEGORY_BUTTON& entry : m_categoryButtons )
997 {
998 if( bool sameRow = haveCurrentRow && currentRow == entry.groupKey; !sameRow )
999 {
1000 currentRow = entry.groupKey;
1001 haveCurrentRow = true;
1002 nextRightEdge = rightEdgeBase;
1003 }
1004
1005 wxPGProperty* category = categoryForGroup( entry.groupKey );
1006 bool visible = false;
1007 int rowY = 0;
1008 int rowHeight = m_grid->GetRowHeight();
1009
1010 if( category && ( !entry.enableFunc || entry.enableFunc() ) )
1011 {
1012 rowY = m_grid->CalcScrolledPosition( wxPoint( 0, category->GetY() ) ).y;
1013 visible = ( rowY + rowHeight > 0 ) && ( rowY < m_grid->GetClientSize().y );
1014 }
1015
1016 if( !visible )
1017 {
1018 entry.button->Hide();
1019 continue;
1020 }
1021
1022 const wxSize btnSize = entry.button->GetSize();
1023 const int btnY = rowY + ( rowHeight - btnSize.y ) / 2;
1024 const int btnX = nextRightEdge - btnSize.x;
1025
1026 if( !entry.button->IsShown() )
1027 entry.button->Show();
1028
1029 entry.button->SetPosition( wxPoint( std::max( 0, btnX ), std::max( 0, btnY ) ) );
1030 entry.button->Raise();
1031
1032 nextRightEdge = btnX - m_grid->FromDIP( 2 );
1033 }
1034}
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:390
Enhanced renderer to work around some limitations in wxWidgets 3.0 capabilities.
bool IsProcessingWxPGEvent() const
void ScrollWindow(int aDx, int aDy, const wxRect *aRect=nullptr) override
True while a wxPropertyGrid event (e.g. right-click) is being processed.
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...
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
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 positionCategoryButtons()
Find the caption row for the given untranslated group name, if present.
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:247
wxPGChoices GetChoices(INSPECTABLE *aObject) const
Definition property.h:269
const wxString & Name() const
Definition property.h:221
Provide class metadata.Helper macro to map type hashes to names.
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
const std::vector< wxString > & GetGroupDisplayOrder(TYPE_ID aType) const
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)
Definition property.h:74
size_t TYPE_ID
Unique type identifier.
std::vector< FAB_LAYER_COLOR > dummy