KiCad PCB EDA Suite
Loading...
Searching...
No Matches
dialog_shim.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) 2012 SoftPLC Corporation, Dick Hollenbeck <[email protected]>
5 * Copyright (C) 2023 CERN
6 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
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 2
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 "dialog_shim.h"
23
24#include <app_monitor.h>
27#include <core/ignore.h>
28#include <kiway_player.h>
29#include <kiway.h>
30#include <pgm_base.h>
32#include <property_holder.h>
34#include <tool/tool_manager.h>
35#include <kiplatform/ui.h>
36#include <widgets/unit_binder.h>
37
38#include <wx/display.h>
39#include <wx/evtloop.h>
40#include <wx/app.h>
41#include <wx/event.h>
42#include <wx/grid.h>
43#include <widgets/wx_grid.h>
44#include <wx/propgrid/propgrid.h>
45#include <wx/checklst.h>
46#include <wx/dataview.h>
47#include <wx/bmpbuttn.h>
48#include <wx/textctrl.h>
49#include <wx/stc/stc.h>
50#include <wx/combobox.h>
51#include <wx/odcombo.h>
52#include <wx/choice.h>
53#include <wx/checkbox.h>
54#include <wx/spinctrl.h>
55#include <wx/splitter.h>
56#include <wx/radiobox.h>
57#include <wx/radiobut.h>
58#include <wx/datectrl.h>
59#if wxUSE_TIMEPICKCTRL
60#include <wx/timectrl.h>
61#endif
62#include <wx/variant.h>
63#include <wx/weakref.h>
64
65#include <algorithm>
66#include <functional>
67#include <nlohmann/json.hpp>
68#include <utility>
69
70BEGIN_EVENT_TABLE( DIALOG_SHIM, wxDialog )
71 EVT_CHAR_HOOK( DIALOG_SHIM::OnCharHook )
72 EVT_ACTIVATE( DIALOG_SHIM::OnActivate )
73END_EVENT_TABLE()
74
75
76
82static std::string getDialogKeyFromTitle( const wxString& aTitle )
83{
84 std::string title = aTitle.ToStdString();
85 size_t parenPos = title.rfind( '(' );
86
87 if( parenPos != std::string::npos && parenPos > 0 )
88 {
89 size_t end = parenPos;
90
91 while( end > 0 && title[end - 1] == ' ' )
92 end--;
93
94 return title.substr( 0, end );
95 }
96
97 return title;
98}
99
100
110static bool isCompoundDateTimePicker( const wxWindow* aWin )
111{
112#if wxUSE_DATEPICKCTRL
113 if( dynamic_cast<const wxDatePickerCtrl*>( aWin ) != nullptr )
114 return true;
115#endif
116
117#if wxUSE_TIMEPICKCTRL
118 if( dynamic_cast<const wxTimePickerCtrl*>( aWin ) != nullptr )
119 return true;
120#endif
121
122 return false;
123}
124
125
126DIALOG_SHIM::DIALOG_SHIM( wxWindow* aParent, wxWindowID id, const wxString& title, const wxPoint& pos,
127 const wxSize& size, long style, const wxString& name ) :
128 wxDialog( aParent, id, title, pos, size, style, name ),
129 KIWAY_HOLDER( nullptr, KIWAY_HOLDER::DIALOG ),
130 m_units( EDA_UNITS::MM ),
131 m_useCalculatedSize( false ),
132 m_firstPaintEvent( true ),
133 m_initialFocusTarget( nullptr ),
134 m_isClosing( false ),
135 m_qmodal_loop( nullptr ),
136 m_qmodal_showing( false ),
137 m_qmodal_parent_disabler( nullptr ),
138 m_parentFrame( nullptr ),
139 m_userPositioned( false ),
140 m_userResized( false ),
141 m_handlingUndoRedo( false ),
142 m_childReleased( false )
143{
144 KIWAY_HOLDER* kiwayHolder = nullptr;
145 m_initialSize = size;
146
147 if( aParent )
148 {
149 kiwayHolder = dynamic_cast<KIWAY_HOLDER*>( aParent );
150
151 while( !kiwayHolder && aParent->GetParent() )
152 {
153 aParent = aParent->GetParent();
154 kiwayHolder = dynamic_cast<KIWAY_HOLDER*>( aParent );
155 }
156 }
157
158 // Inherit units from parent
159 if( kiwayHolder && kiwayHolder->GetType() == KIWAY_HOLDER::FRAME )
160 m_units = static_cast<EDA_BASE_FRAME*>( kiwayHolder )->GetUserUnits();
161 else if( kiwayHolder && kiwayHolder->GetType() == KIWAY_HOLDER::DIALOG )
162 m_units = static_cast<DIALOG_SHIM*>( kiwayHolder )->GetUserUnits();
163
164 // Don't mouse-warp after a dialog run from the context menu
165 if( kiwayHolder && kiwayHolder->GetType() == KIWAY_HOLDER::FRAME )
166 {
167 m_parentFrame = static_cast<EDA_BASE_FRAME*>( kiwayHolder );
168 TOOL_MANAGER* toolMgr = m_parentFrame->GetToolManager();
169
170 if( toolMgr && toolMgr->IsContextMenuActive() )
171 toolMgr->VetoContextMenuMouseWarp();
172 }
173
174 // Set up the message bus
175 if( kiwayHolder )
176 SetKiway( this, &kiwayHolder->Kiway() );
177
178 if( HasKiway() )
179 Kiway().SetBlockingDialog( this );
180
181 Bind( wxEVT_CLOSE_WINDOW, &DIALOG_SHIM::OnCloseWindow, this );
182 Bind( wxEVT_BUTTON, &DIALOG_SHIM::OnButton, this );
183 Bind( wxEVT_SIZE, &DIALOG_SHIM::OnSize, this );
184 Bind( wxEVT_MOVE, &DIALOG_SHIM::OnMove, this );
185 Bind( wxEVT_INIT_DIALOG, &DIALOG_SHIM::onInitDialog, this );
186
187#ifdef __WINDOWS__
188 // On Windows, the app top windows can be brought to the foreground (at least temporarily)
189 // in certain circumstances such as when calling an external tool in Eeschema BOM generation.
190 // So set the parent frame (if exists) to top window to avoid this annoying behavior.
191 if( kiwayHolder && kiwayHolder->GetType() == KIWAY_HOLDER::FRAME )
192 Pgm().App().SetTopWindow( (EDA_BASE_FRAME*) kiwayHolder );
193#endif
194
195 Bind( wxEVT_PAINT, &DIALOG_SHIM::OnPaint, this );
196
197 wxString msg = wxString::Format( "Opening dialog %s", GetTitle() );
198 APP_MONITOR::AddNavigationBreadcrumb( msg, "dialog.open" );
199}
200
201
203{
204 m_isClosing = true;
205
206 Unbind( wxEVT_CLOSE_WINDOW, &DIALOG_SHIM::OnCloseWindow, this );
207 Unbind( wxEVT_BUTTON, &DIALOG_SHIM::OnButton, this );
208 Unbind( wxEVT_PAINT, &DIALOG_SHIM::OnPaint, this );
209 Unbind( wxEVT_SIZE, &DIALOG_SHIM::OnSize, this );
210 Unbind( wxEVT_MOVE, &DIALOG_SHIM::OnMove, this );
211 Unbind( wxEVT_INIT_DIALOG, &DIALOG_SHIM::onInitDialog, this );
212
213 std::function<void( wxWindowList& )> clearOptOuts =
214 [&]( wxWindowList& children )
215 {
216 for( wxWindow* child : children )
217 {
218 delete PROPERTY_HOLDER::SafeCast( child->GetClientData() );
219 child->SetClientData( nullptr );
220 }
221 };
222
223 delete PROPERTY_HOLDER::SafeCast( GetClientData() );
224 SetClientData( nullptr );
225 clearOptOuts( GetChildren() );
226
227 std::function<void( wxWindowList& )> disconnectFocusHandlers =
228 [&]( wxWindowList& children )
229 {
230 for( wxWindow* child : children )
231 {
232 if( isCompoundDateTimePicker( child ) )
233 continue;
234
235 if( wxTextCtrl* textCtrl = dynamic_cast<wxTextCtrl*>( child ) )
236 {
237 textCtrl->Disconnect( wxEVT_SET_FOCUS, wxFocusEventHandler( DIALOG_SHIM::onChildSetFocus ),
238 nullptr, this );
239 }
240 else if( wxStyledTextCtrl* scintilla = dynamic_cast<wxStyledTextCtrl*>( child ) )
241 {
242 scintilla->Disconnect( wxEVT_SET_FOCUS, wxFocusEventHandler( DIALOG_SHIM::onChildSetFocus ),
243 nullptr, this );
244 }
245 else
246 {
247 disconnectFocusHandlers( child->GetChildren() );
248 }
249 }
250 };
251
252 disconnectFocusHandlers( GetChildren() );
253
254 std::function<void( wxWindowList& )> disconnectUndoRedoHandlers =
255 [&]( wxWindowList& children )
256 {
257 for( wxWindow* child : children )
258 {
259 if( isCompoundDateTimePicker( child ) )
260 continue;
261
262 if( wxTextCtrl* textCtrl = dynamic_cast<wxTextCtrl*>( child ) )
263 {
264 textCtrl->Unbind( wxEVT_TEXT, &DIALOG_SHIM::onCommandEvent, this );
265 }
266 else if( wxStyledTextCtrl* scintilla = dynamic_cast<wxStyledTextCtrl*>( child ) )
267 {
268 scintilla->Unbind( wxEVT_STC_CHANGE, &DIALOG_SHIM::onStyledTextChanged, this );
269 }
270 else if( wxComboBox* combo = dynamic_cast<wxComboBox*>( child ) )
271 {
272 combo->Unbind( wxEVT_TEXT, &DIALOG_SHIM::onCommandEvent, this );
273 combo->Unbind( wxEVT_COMBOBOX, &DIALOG_SHIM::onCommandEvent, this );
274 }
275 else if( wxChoice* choice = dynamic_cast<wxChoice*>( child ) )
276 {
277 choice->Unbind( wxEVT_CHOICE, &DIALOG_SHIM::onCommandEvent, this );
278 }
279 else if( wxCheckBox* check = dynamic_cast<wxCheckBox*>( child ) )
280 {
281 check->Unbind( wxEVT_CHECKBOX, &DIALOG_SHIM::onCommandEvent, this );
282 }
283 else if( wxSpinCtrl* spin = dynamic_cast<wxSpinCtrl*>( child ) )
284 {
285 spin->Unbind( wxEVT_SPINCTRL, &DIALOG_SHIM::onSpinEvent, this );
286 spin->Unbind( wxEVT_TEXT, &DIALOG_SHIM::onCommandEvent, this );
287 }
288 else if( wxSpinCtrlDouble* spinD = dynamic_cast<wxSpinCtrlDouble*>( child ) )
289 {
290 spinD->Unbind( wxEVT_SPINCTRLDOUBLE, &DIALOG_SHIM::onSpinDoubleEvent, this );
291 spinD->Unbind( wxEVT_TEXT, &DIALOG_SHIM::onCommandEvent, this );
292 }
293 else if( wxRadioButton* radio = dynamic_cast<wxRadioButton*>( child ) )
294 {
295 radio->Unbind( wxEVT_RADIOBUTTON, &DIALOG_SHIM::onCommandEvent, this );
296 }
297 else if( wxRadioBox* radioBox = dynamic_cast<wxRadioBox*>( child ) )
298 {
299 radioBox->Unbind( wxEVT_RADIOBOX, &DIALOG_SHIM::onCommandEvent, this );
300 }
301 else if( wxGrid* grid = dynamic_cast<wxGrid*>( child ) )
302 {
303 grid->Unbind( wxEVT_GRID_CELL_CHANGED, &DIALOG_SHIM::onGridCellChanged, this );
304 }
305 else if( wxPropertyGrid* propGrid = dynamic_cast<wxPropertyGrid*>( child ) )
306 {
307 propGrid->Unbind( wxEVT_PG_CHANGED, &DIALOG_SHIM::onPropertyGridChanged, this );
308 }
309 else if( wxCheckListBox* checkList = dynamic_cast<wxCheckListBox*>( child ) )
310 {
311 checkList->Unbind( wxEVT_CHECKLISTBOX, &DIALOG_SHIM::onCommandEvent, this );
312 }
313 else if( wxDataViewListCtrl* dataList = dynamic_cast<wxDataViewListCtrl*>( child ) )
314 {
315 dataList->Unbind( wxEVT_DATAVIEW_ITEM_VALUE_CHANGED, &DIALOG_SHIM::onDataViewListChanged, this );
316 }
317 else
318 {
319 disconnectUndoRedoHandlers( child->GetChildren() );
320 }
321 }
322 };
323
324 disconnectUndoRedoHandlers( GetChildren() );
325
326 // The child controls (and the UNIT_BINDERs that live alongside them) outlive this dialog's
327 // member teardown, so sever their back-references now while m_unitBinders is still valid.
328 for( const auto& [window, binder] : m_unitBinders )
329 binder->DetachFromDialogShim();
330
331 // if the dialog is quasi-modal, this will end its event loop
332 if( IsQuasiModal() )
333 EndQuasiModal( wxID_CANCEL );
334
335 if( HasKiway() )
336 Kiway().SetBlockingDialog( nullptr );
337
339}
340
341
342void DIALOG_SHIM::onInitDialog( wxInitDialogEvent& aEvent )
343{
344#ifdef __WXMAC__
345 CallAfter(
346 [this]
347 {
348 if( wxSizer* sz = GetSizer() )
349 sz->Layout();
350 } );
351#endif
352
354 aEvent.Skip();
355}
356
357
359{
360 // must be called from the constructor of derived classes,
361 // when all widgets are initialized, and therefore their size fixed
362
363 // SetSizeHints fixes the minimal size of sizers in the dialog
364 // (SetSizeHints calls Fit(), so no need to call it)
365 GetSizer()->SetSizeHints( this );
366}
367
368
369wxRect ClampRectToDisplay( const wxRect& aRect, const wxRect& aClientArea )
370{
371 wxRect rect = aRect;
372
373 // A window can never be larger than the display that holds it.
374 rect.width = std::min( rect.width, aClientArea.width );
375 rect.height = std::min( rect.height, aClientArea.height );
376
377 rect.x = std::clamp( rect.x, aClientArea.x, aClientArea.GetRight() - rect.width + 1 );
378 rect.y = std::clamp( rect.y, aClientArea.y, aClientArea.GetBottom() - rect.height + 1 );
379
380 return rect;
381}
382
383
385{
386 // A dialog not yet mapped onto a monitor reports no display, so fall back to the parent's
387 // monitor rather than blindly clamping against display zero on a multi-head setup.
388 int displayIdx = wxDisplay::GetFromWindow( this );
389
390 if( displayIdx == wxNOT_FOUND && m_parent )
391 displayIdx = wxDisplay::GetFromWindow( m_parent );
392
393 if( displayIdx == wxNOT_FOUND )
394 displayIdx = 0;
395
396 wxRect clientArea = wxDisplay( (unsigned int) displayIdx ).GetClientArea();
397
398 if( clientArea.width <= 0 || clientArea.height <= 0 )
399 return;
400
401 // The minimum size must shrink first, otherwise SetSize() below cannot honour a cap that
402 // is smaller than a stale minimum restored from a larger monitor.
403 wxSize minSize = GetMinSize();
404 wxSize clampedMin( std::min( minSize.x, clientArea.width ),
405 std::min( minSize.y, clientArea.height ) );
406
407 if( clampedMin != minSize )
408 SetMinSize( clampedMin );
409
410 // Cap the size to the work area and pull the whole dialog back on-screen. Geometry restored
411 // from a different (possibly higher-DPI) monitor can otherwise land off-screen or oversized.
412 wxRect current( GetPosition(), GetSize() );
413 wxRect clamped = ClampRectToDisplay( current, clientArea );
414
415 if( clamped != current )
416 SetSize( clamped.x, clamped.y, clamped.width, clamped.height, 0 );
417}
418
419
420void DIALOG_SHIM::setSizeInDU( int x, int y )
421{
422 wxSize sz( x, y );
423 SetSize( ConvertDialogToPixels( sz ) );
424}
425
426
428{
429 wxSize sz( x, 0 );
430 return ConvertDialogToPixels( sz ).x;
431}
432
433
435{
436 wxSize sz( 0, y );
437 return ConvertDialogToPixels( sz ).y;
438}
439
440
441// our hashtable is an implementation secret, don't need or want it in a header file
442#include <hashtables.h>
443#include <typeinfo>
444#include <grid_tricks.h>
445
446
447void DIALOG_SHIM::SetPosition( const wxPoint& aNewPosition )
448{
449 wxDialog::SetPosition( aNewPosition );
450}
451
452
453void DIALOG_SHIM::focusParentCanvas( bool aDeferUntilFrameActive )
454{
455 wxWindow* toolCanvas = m_parentFrame ? m_parentFrame->GetToolCanvas() : nullptr;
456 wxWindow* target = toolCanvas ? toolCanvas : m_parent;
457
458 if( !target )
459 return;
460
461 target->SetFocus();
462
463 if( !aDeferUntilFrameActive || !toolCanvas )
464 return;
465
466#ifdef __WXGTK__
467 // A quasi-modal dialog is still the active top-level window when its nested event loop exits,
468 // so the SetFocus() above is undone when the dialog is destroyed and GTK restores the frame's
469 // previously-focused widget. Re-assert focus once the event loop has settled, otherwise
470 // keyboard events keep routing to the stale owner until the mouse re-enters the canvas.
472
473 frame->CallAfter(
474 [frame]()
475 {
476 // Skip if another dialog grabbed the activation in the meantime, otherwise we
477 // would raise the frame from behind a chained modal dialog.
478 if( !KIPLATFORM::UI::IsWindowActive( frame ) )
479 return;
480
481 if( wxWindow* canvas = frame->GetToolCanvas() )
482 canvas->SetFocus();
483 } );
484#endif
485}
486
487
488bool DIALOG_SHIM::Show( bool show )
489{
490 bool ret;
491
492 if( show )
493 {
495
496#ifndef __WINDOWS__
497 wxDialog::Raise(); // Needed on OS X and some other window managers (i.e. Unity)
498#endif
499 ret = wxDialog::Show( show );
500
501 wxRect savedDialogRect;
502 std::string key = m_hash_key.empty() ? getDialogKeyFromTitle( GetTitle() ) : m_hash_key;
503
504 if( COMMON_SETTINGS* settings = Pgm().GetCommonSettings() )
505 {
506 auto dlgIt = settings->CsInternals().m_dialogControlValues.find( key );
507
508 if( dlgIt != settings->CsInternals().m_dialogControlValues.end() )
509 {
510 auto geoIt = dlgIt->second.find( "__geometry" );
511
512 if( geoIt != dlgIt->second.end() && geoIt->second.is_object() )
513 {
514 const nlohmann::json& g = geoIt->second;
515 savedDialogRect.SetPosition( wxPoint( g.value( "x", 0 ), g.value( "y", 0 ) ) );
516 savedDialogRect.SetSize( wxSize( g.value( "w", 500 ), g.value( "h", 300 ) ) );
517 }
518 }
519 }
520
521 if( savedDialogRect.GetSize().x != 0 && savedDialogRect.GetSize().y != 0 )
522 {
523 // Convert saved DIP size to logical pixels for the current monitor
524 wxSize restoredSize = FromDIP( savedDialogRect.GetSize() );
525
527 {
528 SetSize( savedDialogRect.GetPosition().x, savedDialogRect.GetPosition().y,
529 wxDialog::GetSize().x, wxDialog::GetSize().y, 0 );
530 }
531 else
532 {
533 SetSize( savedDialogRect.GetPosition().x, savedDialogRect.GetPosition().y,
534 std::max( wxDialog::GetSize().x, restoredSize.x ),
535 std::max( wxDialog::GetSize().y, restoredSize.y ), 0 );
536
537 // Reset minimum size so the user can resize the dialog smaller than
538 // the saved size. We must clear the current minimum and invalidate
539 // the cached best size so GetBestSize() returns the true sizer
540 // minimum rather than being constrained by the restored size.
541 SetMinSize( wxDefaultSize );
542 InvalidateBestSize();
543 SetMinSize( GetBestSize() );
544 }
545
546#ifdef __WXMAC__
547 if( m_parent != nullptr )
548 {
549 if( wxDisplay::GetFromPoint( m_parent->GetPosition() )
550 != wxDisplay::GetFromPoint( savedDialogRect.GetPosition() ) )
551 {
552 Centre();
553 }
554 }
555#endif
556
557 }
558 else if( m_initialSize != wxDefaultSize )
559 {
560 SetSize( m_initialSize );
561 Centre();
562 }
563
564 // Re-center if the title bar would land on no display. Testing a point inside the title
565 // bar (not the window corner) ignores the negative border offset of maximized windows.
566 wxPoint grabPoint = GetPosition();
567 grabPoint.x += GetSize().x / 2;
568 grabPoint.y += FromDIP( 15 );
569
570 if( wxDisplay::GetFromPoint( grabPoint ) == wxNOT_FOUND )
571 Centre();
572
573 m_userPositioned = false;
574 m_userResized = false;
575
576 // Cap size and pull the dialog back on-screen here, after the minimum has been
577 // (re)established above, so the clamp is not overwritten.
579
581 }
582 else
583 {
584
585#ifdef __WXMAC__
586 if ( m_eventLoop )
587 m_eventLoop->Exit( GetReturnCode() ); // Needed for APP-MODAL dlgs on OSX
588#endif
589
590 ret = wxDialog::Show( show );
591
594 }
595
596 return ret;
597}
598
599
601{
602 if( COMMON_SETTINGS* settings = Pgm().GetCommonSettings() )
603 {
604 std::string key = m_hash_key.empty() ? getDialogKeyFromTitle( GetTitle() ) : m_hash_key;
605
606 auto dlgIt = settings->CsInternals().m_dialogControlValues.find( key );
607
608 if( dlgIt == settings->CsInternals().m_dialogControlValues.end() )
609 return;
610
611 dlgIt->second.erase( "__geometry" );
612 }
613}
614
615
616void DIALOG_SHIM::OnSize( wxSizeEvent& aEvent )
617{
618 m_userResized = true;
619 aEvent.Skip();
620}
621
622
623void DIALOG_SHIM::OnMove( wxMoveEvent& aEvent )
624{
625 m_userPositioned = true;
626
627#ifdef __WXMAC__
628 if( m_parent )
629 {
630 int parentDisplay = wxDisplay::GetFromWindow( m_parent );
631 int myDisplay = wxDisplay::GetFromWindow( this );
632
633 if( parentDisplay != wxNOT_FOUND && myDisplay != wxNOT_FOUND )
634 {
635 if( myDisplay != parentDisplay && !m_childReleased )
636 {
637 // Moving to different monitor - release child relationship
639 m_childReleased = true;
640 }
641 else if( myDisplay == parentDisplay && m_childReleased )
642 {
643 // Back on same monitor - restore child relationship
645 m_childReleased = false;
646 }
647 }
648 }
649#endif
650
651 aEvent.Skip();
652}
653
654
655bool DIALOG_SHIM::Enable( bool enable )
656{
657 // so we can do logging of this state change:
658 return wxDialog::Enable( enable );
659}
660
661
662std::string DIALOG_SHIM::generateKey( const wxWindow* aWin ) const
663{
664 auto getSiblingIndex =
665 []( const wxWindow* parent, const wxWindow* child )
666 {
667 wxString childClass = child->GetClassInfo()->GetClassName();
668 int index = 0;
669
670 for( const wxWindow* sibling : parent->GetChildren() )
671 {
672 if( sibling->GetClassInfo()->GetClassName() != childClass )
673 continue;
674
675 if( sibling == child )
676 break;
677
678 index++;
679 }
680
681 return index;
682 };
683
684 auto makeKey =
685 [&]( const wxWindow* window )
686 {
687 std::string key = wxString( window->GetClassInfo()->GetClassName() ).ToStdString();
688
689 if( window->GetParent() )
690 key += "_" + std::to_string( getSiblingIndex( window->GetParent(), window ) );
691
692 return key;
693 };
694
695 std::string key = makeKey( aWin );
696
697 for( const wxWindow* parent = aWin->GetParent(); parent && parent != this; parent = parent->GetParent() )
698 key = makeKey( parent ) + key;
699
700 return key;
701}
702
703
705{
706 COMMON_SETTINGS* settings = Pgm().GetCommonSettings();
707
708 if( !settings )
709 return;
710
711 std::string dialogKey = m_hash_key.empty() ? getDialogKeyFromTitle( GetTitle() ) : m_hash_key;
712 std::map<std::string, nlohmann::json>& dlgMap = settings->CsInternals().m_dialogControlValues[ dialogKey ];
713
714 wxPoint pos = GetPosition();
715 wxSize dipSize = ToDIP( GetSize() );
716 nlohmann::json geom;
717 geom[ "x" ] = pos.x;
718 geom[ "y" ] = pos.y;
719 geom[ "w" ] = dipSize.x;
720 geom[ "h" ] = dipSize.y;
721 dlgMap[ "__geometry" ] = geom;
722
723 std::function<void( wxWindow* )> saveFn =
724 [&]( wxWindow* win )
725 {
726 if( PROPERTY_HOLDER* props = PROPERTY_HOLDER::SafeCast( win->GetClientData() ) )
727 {
728 if( !props->GetPropertyOr( "persist", false ) )
729 return;
730 }
731
732 if( isCompoundDateTimePicker( win ) )
733 return;
734
735 std::string key = generateKey( win );
736
737 if( !key.empty() )
738 {
739 if( m_unitBinders.contains( win ) && !m_unitBinders[ win ]->UnitsInvariant() )
740 {
741 dlgMap[ key ] = m_unitBinders[ win ]->GetValue();
742 }
743 else if( wxComboBox* combo = dynamic_cast<wxComboBox*>( win ) )
744 {
745 dlgMap[ key ] = combo->GetValue();
746 }
747 else if( wxOwnerDrawnComboBox* od_combo = dynamic_cast<wxOwnerDrawnComboBox*>( win ) )
748 {
749 dlgMap[ key ] = od_combo->GetSelection();
750 }
751 else if( wxTextEntry* textEntry = dynamic_cast<wxTextEntry*>( win ) )
752 {
753 dlgMap[ key ] = textEntry->GetValue();
754 }
755 else if( wxChoice* choice = dynamic_cast<wxChoice*>( win ) )
756 {
757 dlgMap[ key ] = choice->GetSelection();
758 }
759 else if( wxCheckBox* check = dynamic_cast<wxCheckBox*>( win ) )
760 {
761 dlgMap[ key ] = check->GetValue();
762 }
763 else if( wxSpinCtrl* spin = dynamic_cast<wxSpinCtrl*>( win ) )
764 {
765 dlgMap[ key ] = spin->GetValue();
766 }
767 else if( wxRadioButton* radio = dynamic_cast<wxRadioButton*>( win ) )
768 {
769 dlgMap[ key ] = radio->GetValue();
770 }
771 else if( wxRadioBox* radioBox = dynamic_cast<wxRadioBox*>( win ) )
772 {
773 dlgMap[ key ] = radioBox->GetSelection();
774 }
775 else if( wxSplitterWindow* splitter = dynamic_cast<wxSplitterWindow*>( win ) )
776 {
777 dlgMap[ key ] = splitter->GetSashPosition();
778 }
779 else if( wxScrolledWindow* scrolled = dynamic_cast<wxScrolledWindow*>( win ) )
780 {
781 dlgMap[ key ] = scrolled->GetScrollPos( wxVERTICAL );
782 }
783 else if( wxNotebook* notebook = dynamic_cast<wxNotebook*>( win ) )
784 {
785 int index = notebook->GetSelection();
786
787 if( index >= 0 && index < (int) notebook->GetPageCount() )
788 dlgMap[ key ] = notebook->GetPageText( notebook->GetSelection() );
789 }
790 else if( wxAuiNotebook* auiNotebook = dynamic_cast<wxAuiNotebook*>( win ) )
791 {
792 int index = auiNotebook->GetSelection();
793
794 if( index >= 0 && index < (int) auiNotebook->GetPageCount() )
795 dlgMap[ key ] = auiNotebook->GetPageText( auiNotebook->GetSelection() );
796 }
797 else if( WX_GRID* grid = dynamic_cast<WX_GRID*>( win ) )
798 {
799 dlgMap[ key ] = grid->GetShownColumnsAsString();
800 }
801 }
802
803 for( wxWindow* child : win->GetChildren() )
804 saveFn( child );
805 };
806
807 if( PROPERTY_HOLDER* props = PROPERTY_HOLDER::SafeCast( GetClientData() ) )
808 {
809 if( !props->GetPropertyOr( "persist", false ) )
810 return;
811 }
812
813 for( wxWindow* child : GetChildren() )
814 saveFn( child );
815}
816
817
819{
820 COMMON_SETTINGS* settings = Pgm().GetCommonSettings();
821
822 if( !settings )
823 return;
824
825 std::string dialogKey = m_hash_key.empty() ? getDialogKeyFromTitle( GetTitle() ) : m_hash_key;
826 auto dlgIt = settings->CsInternals().m_dialogControlValues.find( dialogKey );
827
828 if( dlgIt == settings->CsInternals().m_dialogControlValues.end() )
829 return;
830
831 const std::map<std::string, nlohmann::json>& dlgMap = dlgIt->second;
832
833 std::function<void( wxWindow* )> loadFn =
834 [&]( wxWindow* win )
835 {
836 if( PROPERTY_HOLDER* props = PROPERTY_HOLDER::SafeCast( win->GetClientData() ) )
837 {
838 if( !props->GetPropertyOr( "persist", false ) )
839 return;
840 }
841
842 if( isCompoundDateTimePicker( win ) )
843 return;
844
845 std::string key = generateKey( win );
846
847 if( !key.empty() )
848 {
849 auto it = dlgMap.find( key );
850
851 if( it != dlgMap.end() )
852 {
853 const nlohmann::json& j = it->second;
854
855 if( m_unitBinders.contains( win ) )
856 {
857 if( j.is_number_integer() )
858 {
859 m_unitBinders[ win ]->ChangeValue( j.get<int>() );
860 }
861 else if( j.is_string() )
862 {
863 if( wxTextEntry* textEntry = dynamic_cast<wxTextEntry*>( win ) )
864 textEntry->ChangeValue( wxString::FromUTF8( j.get<std::string>().c_str() ) );
865 }
866 }
867 else if( wxComboBox* combo = dynamic_cast<wxComboBox*>( win ) )
868 {
869 if( j.is_string() )
870 combo->SetValue( wxString::FromUTF8( j.get<std::string>().c_str() ) );
871 }
872 else if( wxOwnerDrawnComboBox* od_combo = dynamic_cast<wxOwnerDrawnComboBox*>( win ) )
873 {
874 if( j.is_number_integer() )
875 {
876 int index = j.get<int>();
877
878 if( index >= 0 && index < (int) od_combo->GetCount() )
879 od_combo->SetSelection( index );
880 }
881 }
882 else if( wxTextEntry* textEntry = dynamic_cast<wxTextEntry*>( win ) )
883 {
884 if( j.is_string() )
885 textEntry->ChangeValue( wxString::FromUTF8( j.get<std::string>().c_str() ) );
886 }
887 else if( wxChoice* choice = dynamic_cast<wxChoice*>( win ) )
888 {
889 if( j.is_number_integer() )
890 {
891 int index = j.get<int>();
892
893 if( index >= 0 && index < (int) choice->GetCount() )
894 choice->SetSelection( index );
895 }
896 }
897 else if( wxCheckBox* check = dynamic_cast<wxCheckBox*>( win ) )
898 {
899 if( j.is_boolean() )
900 check->SetValue( j.get<bool>() );
901 }
902 else if( wxSpinCtrl* spin = dynamic_cast<wxSpinCtrl*>( win ) )
903 {
904 if( j.is_number_integer() )
905 spin->SetValue( j.get<int>() );
906 }
907 else if( wxRadioButton* radio = dynamic_cast<wxRadioButton*>( win ) )
908 {
909 if( j.is_boolean() )
910 {
911 // Only set active radio buttons. Let wxWidgets handle unsetting the inactive
912 // ones. This prevents all from being unset, which trips up wxWidgets in some
913 // cases.
914 if( j.get<bool>() )
915 radio->SetValue( true );
916 }
917 }
918 else if( wxRadioBox* radioBox = dynamic_cast<wxRadioBox*>( win ) )
919 {
920 if( j.is_number_integer() )
921 {
922 int index = j.get<int>();
923
924 if( index >= 0 && index < (int) radioBox->GetCount() )
925 radioBox->SetSelection( index );
926 }
927 }
928 else if( wxSplitterWindow* splitter = dynamic_cast<wxSplitterWindow*>( win ) )
929 {
930 if( j.is_number_integer() )
931 splitter->SetSashPosition( j.get<int>() );
932 }
933 else if( wxScrolledWindow* scrolled = dynamic_cast<wxScrolledWindow*>( win ) )
934 {
935 if( j.is_number_integer() )
936 scrolled->SetScrollPos( wxVERTICAL, j.get<int>() );
937 }
938 else if( wxNotebook* notebook = dynamic_cast<wxNotebook*>( win ) )
939 {
940 if( j.is_string() )
941 {
942 wxString pageTitle = wxString::FromUTF8( j.get<std::string>().c_str() );
943
944 for( int page = 0; page < (int) notebook->GetPageCount(); ++page )
945 {
946 if( notebook->GetPageText( page ) == pageTitle )
947 notebook->ChangeSelection( page );
948 }
949 }
950 }
951 else if( wxAuiNotebook* auiNotebook = dynamic_cast<wxAuiNotebook*>( win ) )
952 {
953 if( j.is_string() )
954 {
955 wxString pageTitle = wxString::FromUTF8( j.get<std::string>().c_str() );
956
957 for( int page = 0; page < (int) auiNotebook->GetPageCount(); ++page )
958 {
959 if( auiNotebook->GetPageText( page ) == pageTitle )
960 auiNotebook->ChangeSelection( page );
961 }
962 }
963 }
964 else if( WX_GRID* grid = dynamic_cast<WX_GRID*>( win ) )
965 {
966 if( j.is_string() )
967 grid->ShowHideColumns( wxString::FromUTF8( j.get<std::string>().c_str() ) );
968 }
969 }
970 }
971
972 for( wxWindow* child : win->GetChildren() )
973 loadFn( child );
974 };
975
976 if( PROPERTY_HOLDER* props = PROPERTY_HOLDER::SafeCast( GetClientData() ) )
977 {
978 if( !props->GetPropertyOr( "persist", false ) )
979 return;
980 }
981
982 for( wxWindow* child : GetChildren() )
983 loadFn( child );
984}
985
986
987void DIALOG_SHIM::OptOut( wxWindow* aWindow )
988{
989 PROPERTY_HOLDER* props = new PROPERTY_HOLDER();
990 props->SetProperty( "persist", false );
991 aWindow->SetClientData( props );
992}
993
994
996{
997 m_noControlUndoRedo.insert( aWindow );
998}
999
1000
1001void DIALOG_SHIM::RegisterUnitBinder( UNIT_BINDER* aUnitBinder, wxWindow* aWindow )
1002{
1003 m_unitBinders[ aWindow ] = aUnitBinder;
1004}
1005
1006
1008{
1009 // Erase by binder identity rather than window key, so that a stale entry whose window has
1010 // already been reused by a newer binder is left untouched.
1011 std::erase_if( m_unitBinders,
1012 [aUnitBinder]( const auto& aEntry )
1013 {
1014 return aEntry.second == aUnitBinder;
1015 } );
1016}
1017
1018
1019// Recursive descent doing a SelectAll() in wxTextCtrls.
1020// MacOS User Interface Guidelines state that when tabbing to a text control all its
1021// text should be selected. Since wxWidgets fails to implement this, we do it here.
1022void DIALOG_SHIM::SelectAllInTextCtrls( wxWindowList& children )
1023{
1024 for( wxWindow* child : children )
1025 {
1026 if( isCompoundDateTimePicker( child ) )
1027 continue;
1028
1029 if( wxTextCtrl* textCtrl = dynamic_cast<wxTextCtrl*>( child ) )
1030 {
1031 m_beforeEditValues[ textCtrl ] = textCtrl->GetValue();
1032 textCtrl->Connect( wxEVT_SET_FOCUS, wxFocusEventHandler( DIALOG_SHIM::onChildSetFocus ),
1033 nullptr, this );
1034
1035 // We don't currently run this on GTK because some window managers don't hide the
1036 // selection in non-active controls, and other window managers do the selection
1037 // automatically anyway.
1038#if defined( __WXMAC__ ) || defined( __WXMSW__ )
1039 if( !textCtrl->GetStringSelection().IsEmpty() )
1040 {
1041 // Respect an existing selection
1042 }
1043 else if( textCtrl->IsEditable() )
1044 {
1045 textCtrl->SelectAll();
1046 }
1047#else
1048 ignore_unused( textCtrl );
1049#endif
1050 }
1051 else if( wxStyledTextCtrl* scintilla = dynamic_cast<wxStyledTextCtrl*>( child ) )
1052 {
1053 m_beforeEditValues[ scintilla ] = scintilla->GetText();
1054 scintilla->Connect( wxEVT_SET_FOCUS,
1055 wxFocusEventHandler( DIALOG_SHIM::onChildSetFocus ),
1056 nullptr, this );
1057
1058 if( !scintilla->GetSelectedText().IsEmpty() )
1059 {
1060 // Respect an existing selection
1061 }
1062 else if( scintilla->GetMarginWidth( 0 ) > 0 )
1063 {
1064 // Don't select-all in Custom Rules, etc.
1065 }
1066 else if( scintilla->IsEditable() )
1067 {
1068 scintilla->SelectAll();
1069 }
1070 }
1071#ifdef __WXMAC__
1072 // Temp hack for square (looking) buttons on OSX. Will likely be made redundant
1073 // by the image store....
1074 else if( dynamic_cast<wxBitmapButton*>( child ) != nullptr )
1075 {
1076 wxSize minSize( 29, 27 );
1077 wxRect rect = child->GetRect();
1078
1079 child->ConvertDialogToPixels( minSize );
1080
1081 rect.Inflate( std::max( 0, minSize.x - rect.GetWidth() ),
1082 std::max( 0, minSize.y - rect.GetHeight() ) );
1083
1084 child->SetMinSize( rect.GetSize() );
1085 child->SetSize( rect );
1086 }
1087#endif
1088 else
1089 {
1090 SelectAllInTextCtrls( child->GetChildren() );
1091 }
1092 }
1093}
1094
1095
1096void DIALOG_SHIM::registerUndoRedoHandlers( wxWindowList& children )
1097{
1098 for( wxWindow* child : children )
1099 {
1100 if( m_noControlUndoRedo.count( child ) )
1101 continue;
1102
1103 if( isCompoundDateTimePicker( child ) )
1104 continue;
1105
1106 if( wxTextCtrl* textCtrl = dynamic_cast<wxTextCtrl*>( child ) )
1107 {
1108 textCtrl->Bind( wxEVT_TEXT, &DIALOG_SHIM::onCommandEvent, this );
1109 m_currentValues[ textCtrl ] = textCtrl->GetValue();
1110 }
1111 else if( wxStyledTextCtrl* scintilla = dynamic_cast<wxStyledTextCtrl*>( child ) )
1112 {
1113 scintilla->Bind( wxEVT_STC_CHANGE, &DIALOG_SHIM::onStyledTextChanged, this );
1114 m_currentValues[ scintilla ] = scintilla->GetText();
1115 }
1116 else if( wxComboBox* combo = dynamic_cast<wxComboBox*>( child ) )
1117 {
1118 combo->Bind( wxEVT_TEXT, &DIALOG_SHIM::onCommandEvent, this );
1119 combo->Bind( wxEVT_COMBOBOX, &DIALOG_SHIM::onCommandEvent, this );
1120 m_currentValues[ combo ] = combo->GetValue();
1121 }
1122 else if( wxChoice* choice = dynamic_cast<wxChoice*>( child ) )
1123 {
1124 choice->Bind( wxEVT_CHOICE, &DIALOG_SHIM::onCommandEvent, this );
1125 m_currentValues[ choice ] = static_cast<long>( choice->GetSelection() );
1126 }
1127 else if( wxCheckBox* check = dynamic_cast<wxCheckBox*>( child ) )
1128 {
1129 check->Bind( wxEVT_CHECKBOX, &DIALOG_SHIM::onCommandEvent, this );
1130 m_currentValues[ check ] = check->GetValue();
1131 }
1132 else if( wxSpinCtrl* spin = dynamic_cast<wxSpinCtrl*>( child ) )
1133 {
1134 spin->Bind( wxEVT_SPINCTRL, &DIALOG_SHIM::onSpinEvent, this );
1135 spin->Bind( wxEVT_TEXT, &DIALOG_SHIM::onCommandEvent, this );
1136 m_currentValues[ spin ] = static_cast<long>( spin->GetValue() );
1137 }
1138 else if( wxSpinCtrlDouble* spinD = dynamic_cast<wxSpinCtrlDouble*>( child ) )
1139 {
1140 spinD->Bind( wxEVT_SPINCTRLDOUBLE, &DIALOG_SHIM::onSpinDoubleEvent, this );
1141 spinD->Bind( wxEVT_TEXT, &DIALOG_SHIM::onCommandEvent, this );
1142 m_currentValues[ spinD ] = spinD->GetValue();
1143 }
1144 else if( wxRadioButton* radio = dynamic_cast<wxRadioButton*>( child ) )
1145 {
1146 radio->Bind( wxEVT_RADIOBUTTON, &DIALOG_SHIM::onCommandEvent, this );
1147 m_currentValues[ radio ] = radio->GetValue();
1148 }
1149 else if( wxRadioBox* radioBox = dynamic_cast<wxRadioBox*>( child ) )
1150 {
1151 radioBox->Bind( wxEVT_RADIOBOX, &DIALOG_SHIM::onCommandEvent, this );
1152 m_currentValues[ radioBox ] = static_cast<long>( radioBox->GetSelection() );
1153 }
1154 else if( wxGrid* grid = dynamic_cast<wxGrid*>( child ) )
1155 {
1156 grid->Bind( wxEVT_GRID_CELL_CHANGED, &DIALOG_SHIM::onGridCellChanged, this );
1158 }
1159 else if( wxPropertyGrid* propGrid = dynamic_cast<wxPropertyGrid*>( child ) )
1160 {
1161 propGrid->Bind( wxEVT_PG_CHANGED, &DIALOG_SHIM::onPropertyGridChanged, this );
1162 m_currentValues[ propGrid ] = getControlValue( propGrid );
1163 }
1164 else if( wxCheckListBox* checkList = dynamic_cast<wxCheckListBox*>( child ) )
1165 {
1166 checkList->Bind( wxEVT_CHECKLISTBOX, &DIALOG_SHIM::onCommandEvent, this );
1167 m_currentValues[ checkList ] = getControlValue( checkList );
1168 }
1169 else if( wxDataViewListCtrl* dataList = dynamic_cast<wxDataViewListCtrl*>( child ) )
1170 {
1171 dataList->Bind( wxEVT_DATAVIEW_ITEM_VALUE_CHANGED, &DIALOG_SHIM::onDataViewListChanged, this );
1172 m_currentValues[ dataList ] = getControlValue( dataList );
1173 }
1174 else
1175 {
1176 registerUndoRedoHandlers( child->GetChildren() );
1177 }
1178 }
1179}
1180
1181
1183{
1184 // If we are in an event handler that generates lots of events (e.g. cutting
1185 // a range of cells in a grid), we want to coalesce all of those events into a
1186 // single undo commit.
1187 //
1188 // So what we'll do is collect all of the controls that have changes,
1189 // and enqueue a single call to flushPendingControlChanges() to be called after the
1190 // current event handler (which is calling this function) has finished.
1191
1192 const auto [it, inserted] = m_controlsWithPendingChanges.insert( aCtrl );
1193
1194 if( !inserted )
1195 return;
1196
1197 if( m_controlsWithPendingChanges.size() == 1 )
1199}
1200
1201
1203{
1204 for( wxWindow* const ctrl : m_controlsWithPendingChanges )
1205 {
1206 wxVariant before = m_currentValues[ctrl];
1207 wxVariant after = getControlValue( ctrl );
1208
1209 if( before != after )
1210 {
1211 // Note this still produces an undo/redo entry per control,
1212 // even if changed within a control are combined.
1213 // If an event causes a multi-control change, the user will
1214 // still have to hit undo multiple times to get back to the
1215 // original state.`
1216 m_undoStack.push_back( { ctrl, before, after } );
1217 m_redoStack.clear();
1218 m_currentValues[ctrl] = after;
1219 }
1220 }
1221
1223}
1224
1225
1226void DIALOG_SHIM::onCommandEvent( wxCommandEvent& aEvent )
1227{
1228 if( !m_handlingUndoRedo )
1229 recordControlChange( static_cast<wxWindow*>( aEvent.GetEventObject() ) );
1230
1231 aEvent.Skip();
1232}
1233
1234
1235void DIALOG_SHIM::onSpinEvent( wxSpinEvent& aEvent )
1236{
1237 if( !m_handlingUndoRedo )
1238 recordControlChange( static_cast<wxWindow*>( aEvent.GetEventObject() ) );
1239
1240 aEvent.Skip();
1241}
1242
1243
1244void DIALOG_SHIM::onSpinDoubleEvent( wxSpinDoubleEvent& aEvent )
1245{
1246 if( !m_handlingUndoRedo )
1247 recordControlChange( static_cast<wxWindow*>( aEvent.GetEventObject() ) );
1248
1249 aEvent.Skip();
1250}
1251
1252
1253void DIALOG_SHIM::onStyledTextChanged( wxStyledTextEvent& aEvent )
1254{
1255 if( !m_handlingUndoRedo )
1256 recordControlChange( static_cast<wxWindow*>( aEvent.GetEventObject() ) );
1257
1258 aEvent.Skip();
1259}
1260
1261
1262void DIALOG_SHIM::onGridCellChanged( wxGridEvent& aEvent )
1263{
1264 if( !m_handlingUndoRedo )
1265 recordControlChange( static_cast<wxWindow*>( aEvent.GetEventObject() ) );
1266
1267 aEvent.Skip();
1268}
1269
1270void DIALOG_SHIM::onPropertyGridChanged( wxPropertyGridEvent& aEvent )
1271{
1272 if( !m_handlingUndoRedo )
1273 recordControlChange( static_cast<wxWindow*>( aEvent.GetEventObject() ) );
1274
1275 aEvent.Skip();
1276}
1277
1278void DIALOG_SHIM::onDataViewListChanged( wxDataViewEvent& aEvent )
1279{
1280 if( !m_handlingUndoRedo )
1281 recordControlChange( static_cast<wxWindow*>( aEvent.GetEventObject() ) );
1282
1283 aEvent.Skip();
1284}
1285
1286wxVariant DIALOG_SHIM::getControlValue( wxWindow* aCtrl )
1287{
1288 if( wxTextCtrl* textCtrl = dynamic_cast<wxTextCtrl*>( aCtrl ) )
1289 return wxVariant( textCtrl->GetValue() );
1290 else if( wxStyledTextCtrl* scintilla = dynamic_cast<wxStyledTextCtrl*>( aCtrl ) )
1291 return wxVariant( scintilla->GetText() );
1292 else if( wxComboBox* combo = dynamic_cast<wxComboBox*>( aCtrl ) )
1293 return wxVariant( combo->GetValue() );
1294 else if( wxChoice* choice = dynamic_cast<wxChoice*>( aCtrl ) )
1295 return wxVariant( (long) choice->GetSelection() );
1296 else if( wxCheckBox* check = dynamic_cast<wxCheckBox*>( aCtrl ) )
1297 return wxVariant( check->GetValue() );
1298 else if( wxSpinCtrl* spin = dynamic_cast<wxSpinCtrl*>( aCtrl ) )
1299 return wxVariant( (long) spin->GetValue() );
1300 else if( wxSpinCtrlDouble* spinD = dynamic_cast<wxSpinCtrlDouble*>( aCtrl ) )
1301 return wxVariant( spinD->GetValue() );
1302 else if( wxRadioButton* radio = dynamic_cast<wxRadioButton*>( aCtrl ) )
1303 return wxVariant( radio->GetValue() );
1304 else if( wxRadioBox* radioBox = dynamic_cast<wxRadioBox*>( aCtrl ) )
1305 return wxVariant( (long) radioBox->GetSelection() );
1306 else if( wxGrid* grid = dynamic_cast<wxGrid*>( aCtrl ) )
1307 {
1308 // Tables with regroupable/sortable rows serialize by identity instead of row position.
1309 if( auto* table = dynamic_cast<WX_GRID_TABLE_BASE*>( grid->GetTable() );
1310 table && table->HasUndoStateSerialization() )
1311 {
1312 return wxVariant( table->SerializeUndoState() );
1313 }
1314
1315 nlohmann::json j = nlohmann::json::array();
1316 int rows = grid->GetNumberRows();
1317 int cols = grid->GetNumberCols();
1318
1319 for( int r = 0; r < rows; ++r )
1320 {
1321 nlohmann::json row = nlohmann::json::array();
1322
1323 for( int c = 0; c < cols; ++c )
1324 row.push_back( std::string( grid->GetCellValue( r, c ).ToUTF8() ) );
1325
1326 j.push_back( row );
1327 }
1328
1329 return wxVariant( wxString( j.dump() ) );
1330 }
1331 else if( wxPropertyGrid* propGrid = dynamic_cast<wxPropertyGrid*>( aCtrl ) )
1332 {
1333 nlohmann::json j;
1334
1335 for( wxPropertyGridIterator it = propGrid->GetIterator(); !it.AtEnd(); ++it )
1336 {
1337 wxPGProperty* prop = *it;
1338 j[ prop->GetName().ToStdString() ] = prop->GetValueAsString().ToStdString();
1339 }
1340
1341 return wxVariant( wxString( j.dump() ) );
1342 }
1343 else if( wxCheckListBox* checkList = dynamic_cast<wxCheckListBox*>( aCtrl ) )
1344 {
1345 nlohmann::json j = nlohmann::json::array();
1346 unsigned int count = checkList->GetCount();
1347
1348 for( unsigned int i = 0; i < count; ++i )
1349 {
1350 if( checkList->IsChecked( i ) )
1351 j.push_back( i );
1352 }
1353
1354 return wxVariant( wxString( j.dump() ) );
1355 }
1356 else if( wxDataViewListCtrl* dataList = dynamic_cast<wxDataViewListCtrl*>( aCtrl ) )
1357 {
1358 nlohmann::json j = nlohmann::json::array();
1359 unsigned int rows = dataList->GetItemCount();
1360 unsigned int cols = dataList->GetColumnCount();
1361
1362 for( unsigned int r = 0; r < rows; ++r )
1363 {
1364 nlohmann::json row = nlohmann::json::array();
1365
1366 for( unsigned int c = 0; c < cols; ++c )
1367 {
1368 wxVariant val;
1369 dataList->GetValue( val, r, c );
1370 row.push_back( std::string( val.GetString().ToUTF8() ) );
1371 }
1372
1373 j.push_back( row );
1374 }
1375
1376 return wxVariant( wxString( j.dump() ) );
1377 }
1378 else
1379 return wxVariant();
1380}
1381
1382
1383void DIALOG_SHIM::setControlValue( wxWindow* aCtrl, const wxVariant& aValue )
1384{
1385 if( wxTextCtrl* textCtrl = dynamic_cast<wxTextCtrl*>( aCtrl ) )
1386 textCtrl->SetValue( aValue.GetString() );
1387 else if( wxStyledTextCtrl* scintilla = dynamic_cast<wxStyledTextCtrl*>( aCtrl ) )
1388 scintilla->SetText( aValue.GetString() );
1389 else if( wxComboBox* combo = dynamic_cast<wxComboBox*>( aCtrl ) )
1390 combo->SetValue( aValue.GetString() );
1391 else if( wxChoice* choice = dynamic_cast<wxChoice*>( aCtrl ) )
1392 choice->SetSelection( (int) aValue.GetLong() );
1393 else if( wxCheckBox* check = dynamic_cast<wxCheckBox*>( aCtrl ) )
1394 check->SetValue( aValue.GetBool() );
1395 else if( wxSpinCtrl* spin = dynamic_cast<wxSpinCtrl*>( aCtrl ) )
1396 spin->SetValue( (int) aValue.GetLong() );
1397 else if( wxSpinCtrlDouble* spinD = dynamic_cast<wxSpinCtrlDouble*>( aCtrl ) )
1398 spinD->SetValue( aValue.GetDouble() );
1399 else if( wxRadioButton* radio = dynamic_cast<wxRadioButton*>( aCtrl ) )
1400 radio->SetValue( aValue.GetBool() );
1401 else if( wxRadioBox* radioBox = dynamic_cast<wxRadioBox*>( aCtrl ) )
1402 radioBox->SetSelection( (int) aValue.GetLong() );
1403 else if( wxGrid* grid = dynamic_cast<wxGrid*>( aCtrl ) )
1404 {
1405 if( auto* table = dynamic_cast<WX_GRID_TABLE_BASE*>( grid->GetTable() );
1406 table && table->HasUndoStateSerialization() )
1407 {
1408 table->RestoreUndoState( aValue.GetString() );
1409 return;
1410 }
1411
1412 nlohmann::json j = nlohmann::json::parse( aValue.GetString().ToStdString(), nullptr, false );
1413
1414 if( j.is_array() )
1415 {
1416 int rows = std::min( (int) j.size(), grid->GetNumberRows() );
1417
1418 std::vector<std::pair<int, int>> changedCells;
1419
1420 for( int r = 0; r < rows; ++r )
1421 {
1422 nlohmann::json row = j[r];
1423 int cols = std::min( (int) row.size(), grid->GetNumberCols() );
1424
1425 for( int c = 0; c < cols; ++c )
1426 {
1427 wxString value = wxString( row[c].get<std::string>() );
1428
1429 if( grid->GetCellValue( r, c ) != value )
1430 {
1431 grid->SetCellValue( r, c, value );
1432 changedCells.emplace_back( r, c );
1433 }
1434 }
1435 }
1436
1437 for( const auto& [row, col] : changedCells )
1438 {
1439 wxGridEvent evt( grid->GetId(), wxEVT_GRID_CELL_CHANGED, grid, row, col );
1440 evt.SetString( grid->GetCellValue( row, col ) );
1441 grid->GetEventHandler()->ProcessEvent( evt );
1442 }
1443 }
1444 }
1445 else if( wxPropertyGrid* propGrid = dynamic_cast<wxPropertyGrid*>( aCtrl ) )
1446 {
1447 nlohmann::json j = nlohmann::json::parse( aValue.GetString().ToStdString(), nullptr, false );
1448
1449 if( j.is_object() )
1450 {
1451 for( auto it = j.begin(); it != j.end(); ++it )
1452 propGrid->SetPropertyValue( wxString( it.key() ), wxString( it.value().get<std::string>() ) );
1453 }
1454 }
1455 else if( wxCheckListBox* checkList = dynamic_cast<wxCheckListBox*>( aCtrl ) )
1456 {
1457 nlohmann::json j = nlohmann::json::parse( aValue.GetString().ToStdString(), nullptr, false );
1458
1459 if( j.is_array() )
1460 {
1461 unsigned int count = checkList->GetCount();
1462
1463 for( unsigned int i = 0; i < count; ++i )
1464 checkList->Check( i, false );
1465
1466 for( auto& idx : j )
1467 {
1468 unsigned int i = idx.get<unsigned int>();
1469
1470 if( i < count )
1471 checkList->Check( i, true );
1472 }
1473 }
1474 }
1475 else if( wxDataViewListCtrl* dataList = dynamic_cast<wxDataViewListCtrl*>( aCtrl ) )
1476 {
1477 nlohmann::json j = nlohmann::json::parse( aValue.GetString().ToStdString(), nullptr, false );
1478
1479 if( j.is_array() )
1480 {
1481 unsigned int rows = std::min( static_cast<unsigned int>( j.size() ),
1482 static_cast<unsigned int>( dataList->GetItemCount() ) );
1483
1484 for( unsigned int r = 0; r < rows; ++r )
1485 {
1486 nlohmann::json row = j[r];
1487 unsigned int cols = std::min( (unsigned int) row.size(), dataList->GetColumnCount() );
1488
1489 for( unsigned int c = 0; c < cols; ++c )
1490 {
1491 wxVariant val( wxString( row[c].get<std::string>() ) );
1492 dataList->SetValue( val, r, c );
1493 }
1494 }
1495 }
1496 }
1497}
1498
1499
1501{
1503
1504 if( m_undoStack.empty() )
1505 return;
1506
1507 m_handlingUndoRedo = true;
1508 UNDO_STEP step = m_undoStack.back();
1509 m_undoStack.pop_back();
1510 setControlValue( step.ctrl, step.before );
1511 m_currentValues[ step.ctrl ] = step.before;
1512 m_redoStack.push_back( step );
1513 m_handlingUndoRedo = false;
1514}
1515
1516
1518{
1520
1521 if( m_redoStack.empty() )
1522 return;
1523
1524 m_handlingUndoRedo = true;
1525 UNDO_STEP step = m_redoStack.back();
1526 m_redoStack.pop_back();
1527 setControlValue( step.ctrl, step.after );
1528 m_currentValues[ step.ctrl ] = step.after;
1529 m_undoStack.push_back( step );
1530 m_handlingUndoRedo = false;
1531}
1532
1533
1534void DIALOG_SHIM::OnPaint( wxPaintEvent &event )
1535{
1536 if( m_firstPaintEvent )
1537 {
1539
1540 SelectAllInTextCtrls( GetChildren() );
1541 registerUndoRedoHandlers( GetChildren() );
1542
1544
1545 m_firstPaintEvent = false;
1546 }
1547
1548 event.Skip();
1549}
1550
1551
1553{
1554 // Skip targets that can't take focus (e.g. hidden on a notebook page) so ESC still works
1555 if( m_initialFocusTarget && m_initialFocusTarget->IsShownOnScreen()
1556 && m_initialFocusTarget->CanAcceptFocus() )
1557 {
1559 }
1560 else
1561 {
1563 }
1564}
1565
1566
1567void DIALOG_SHIM::OnActivate( wxActivateEvent& aEvent )
1568{
1569 // Null FindFocus() means focus landed on a non-wx element (WM title bar, GTK tab strip)
1570 // where ESC never reaches OnCharHook; defer via CallAfter since GTK reports null transiently
1571 if( aEvent.GetActive() && !m_firstPaintEvent )
1572 {
1573 wxWeakRef<DIALOG_SHIM> self( this );
1574
1575 CallAfter(
1576 [self]()
1577 {
1578 DIALOG_SHIM* dlg = self;
1579
1580 if( dlg && KIPLATFORM::UI::IsWindowActive( dlg )
1581 && wxWindow::FindFocus() == nullptr )
1582 {
1583 dlg->forceInitialFocus();
1584 }
1585 } );
1586 }
1587
1588 aEvent.Skip();
1589}
1590
1591
1593{
1594 if( !GetTitle().StartsWith( wxS( "*" ) ) )
1595 SetTitle( wxS( "*" ) + GetTitle() );
1596}
1597
1598
1600{
1601 if( GetTitle().StartsWith( wxS( "*" ) ) )
1602 SetTitle( GetTitle().AfterFirst( '*' ) );
1603}
1604
1606{
1608
1609 // Apple in its infinite wisdom will raise a disabled window before even passing
1610 // us the event, so we have no way to stop it. Instead, we must set an order on
1611 // the windows so that the modal will be pushed in front of the disabled
1612 // window when it is raised.
1614
1615 // Call the base class ShowModal() method
1616 return wxDialog::ShowModal();
1617}
1618
1619/*
1620 QuasiModal Mode Explained:
1621
1622 The gtk calls in wxDialog::ShowModal() cause event routing problems if that
1623 modal dialog then tries to use KIWAY_PLAYER::ShowModal(). The latter shows up
1624 and mostly works but does not respond to the window decoration close button.
1625 There is no way to get around this without reversing the gtk calls temporarily.
1626
1627 There are also issues with the Scintilla text editor putting up autocomplete
1628 popups, which appear behind the dialog window if QuasiModal is not used.
1629
1630 QuasiModal mode is our own almost modal mode which disables only the parent
1631 of the DIALOG_SHIM, leaving other frames operable and while staying captured in the
1632 nested event loop. This avoids the gtk calls and leaves event routing pure
1633 and sufficient to operate the KIWAY_PLAYER::ShowModal() properly. When using
1634 ShowQuasiModal() you have to use EndQuasiModal() in your dialogs and not
1635 EndModal(). There is also IsQuasiModal() but its value can only be true
1636 when the nested event loop is active. Do not mix the modal and quasi-modal
1637 functions. Use one set or the other.
1638
1639 You might find this behavior preferable over a pure modal mode, and it was said
1640 that only the Mac has this natively, but now other platforms have something
1641 similar. You CAN use it anywhere for any dialog. But you MUST use it when
1642 you want to use KIWAY_PLAYER::ShowModal() from a dialog event.
1643*/
1644
1646{
1647 NULLER raii_nuller( (void*&) m_qmodal_loop );
1648
1649 // release the mouse if it's currently captured as the window having it
1650 // will be disabled when this dialog is shown -- but will still keep the
1651 // capture making it impossible to do anything in the modal dialog itself
1652 if( wxWindow* win = wxWindow::GetCapture() )
1653 win->ReleaseMouse();
1654
1655 // Get the optimal parent
1656 wxWindow* parent = GetParentForModalDialog( GetParent(), GetWindowStyle() );
1657
1658 wxASSERT_MSG( !m_qmodal_parent_disabler, wxT( "Caller using ShowQuasiModal() twice on same window?" ) );
1659
1660 // quasi-modal: disable only my "optimal" parent
1662
1663 // Apple in its infinite wisdom will raise a disabled window before even passing
1664 // us the event, so we have no way to stop it. Instead, we must set an order on
1665 // the windows so that the quasi-modal will be pushed in front of the disabled
1666 // window when it is raised.
1668
1669 Show( true );
1670
1671 m_qmodal_showing = true;
1672
1673 wxGUIEventLoop event_loop;
1674
1675 m_qmodal_loop = &event_loop;
1676
1677 event_loop.Run();
1678
1679 m_qmodal_showing = false;
1680 focusParentCanvas( true );
1681
1682 return GetReturnCode();
1683}
1684
1685
1687{
1689 m_qmodal_parent_disabler->SuspendForTrueModal();
1690}
1691
1692
1694{
1696 m_qmodal_parent_disabler->ResumeAfterTrueModal();
1697}
1698
1699
1701{
1702 // Hook up validator and transfer data from controls handling so quasi-modal dialogs
1703 // handle validation in the same way as other dialogs.
1704 if( ( retCode == wxID_OK ) && ( !Validate() || !TransferDataFromWindow() ) )
1705 return;
1706
1707 SetReturnCode( retCode );
1708
1709 if( !IsQuasiModal() )
1710 {
1711 wxFAIL_MSG( wxT( "Either DIALOG_SHIM::EndQuasiModal was called twice, or ShowQuasiModal wasn't called" ) );
1712 return;
1713 }
1714
1716
1717 if( m_qmodal_loop )
1718 {
1719 if( m_qmodal_loop->IsRunning() )
1720 m_qmodal_loop->Exit( 0 );
1721 else
1722 m_qmodal_loop->ScheduleExit( 0 );
1723 }
1724
1726 m_qmodal_parent_disabler = nullptr;
1727
1728 Show( false );
1729}
1730
1731
1732void DIALOG_SHIM::resetUndoRedoForNewContent( wxWindowList& aChildren )
1733{
1734 m_undoStack.clear();
1735 m_redoStack.clear();
1736 m_currentValues.clear();
1738 registerUndoRedoHandlers( aChildren );
1739}
1740
1741
1742void DIALOG_SHIM::unregisterUnitBinders( wxWindow* aWindow )
1743{
1744 m_unitBinders.erase( aWindow );
1745
1746 for( wxWindow* child : aWindow->GetChildren() )
1747 unregisterUnitBinders( child );
1748}
1749
1750
1751void DIALOG_SHIM::OnCloseWindow( wxCloseEvent& aEvent )
1752{
1753 wxString msg = wxString::Format( "Closing dialog %s", GetTitle() );
1754 APP_MONITOR::AddNavigationBreadcrumb( msg, "dialog.close" );
1755
1757
1758 if( IsQuasiModal() )
1759 {
1760 EndQuasiModal( wxID_CANCEL );
1761 return;
1762 }
1763
1764 // This is mandatory to allow wxDialogBase::OnCloseWindow() to be called.
1765 aEvent.Skip();
1766}
1767
1768
1769void DIALOG_SHIM::OnButton( wxCommandEvent& aEvent )
1770{
1771 const int id = aEvent.GetId();
1772
1773 if( IsQuasiModal() )
1774 {
1775 if( id == GetAffirmativeId() )
1776 {
1777 EndQuasiModal( id );
1778 }
1779 else if( id == wxID_APPLY )
1780 {
1781 // Dialogs that provide Apply buttons should make sure data is valid before
1782 // allowing a transfer, as there is no other way to indicate failure
1783 // (i.e. the dialog can't refuse to close as it might with OK, because it
1784 // isn't closing anyway)
1785 if( Validate() )
1786 ignore_unused( TransferDataFromWindow() );
1787 }
1788 else if( id == wxID_CANCEL )
1789 {
1790 EndQuasiModal( wxID_CANCEL );
1791 }
1792 else // not a standard button
1793 {
1794 aEvent.Skip();
1795 }
1796
1797 return;
1798 }
1799
1800 // This is mandatory to allow wxDialogBase::OnButton() to be called.
1801 aEvent.Skip();
1802}
1803
1804
1805void DIALOG_SHIM::onChildSetFocus( wxFocusEvent& aEvent )
1806{
1807 // When setting focus to a text control reset the before-edit value.
1808
1809 if( !m_isClosing )
1810 {
1811 if( wxTextCtrl* textCtrl = dynamic_cast<wxTextCtrl*>( aEvent.GetEventObject() ) )
1812 m_beforeEditValues[ textCtrl ] = textCtrl->GetValue();
1813 else if( wxStyledTextCtrl* scintilla = dynamic_cast<wxStyledTextCtrl*>( aEvent.GetEventObject() ) )
1814 m_beforeEditValues[ scintilla ] = scintilla->GetText();
1815 }
1816
1817 aEvent.Skip();
1818}
1819
1820
1821void DIALOG_SHIM::OnCharHook( wxKeyEvent& aEvt )
1822{
1823 int key = aEvt.GetKeyCode();
1824 int mods = 0;
1825
1826 if( aEvt.ControlDown() )
1827 mods |= MD_CTRL;
1828 if( aEvt.ShiftDown() )
1829 mods |= MD_SHIFT;
1830 if( aEvt.AltDown() )
1831 mods |= MD_ALT;
1832
1833 int hotkey = key | mods;
1834
1835 // Check for standard undo/redo hotkeys
1836 if( hotkey == (MD_CTRL + 'Z') )
1837 {
1838 doUndo();
1839 return;
1840 }
1841 else if( hotkey == (MD_CTRL + MD_SHIFT + 'Z') || hotkey == (MD_CTRL + 'Y') )
1842 {
1843 doRedo();
1844 return;
1845 }
1846
1847 if( aEvt.GetKeyCode() == 'U' && aEvt.GetModifiers() == wxMOD_CONTROL )
1848 {
1849 if( m_parentFrame )
1850 {
1851 m_parentFrame->ToggleUserUnits();
1852 return;
1853 }
1854 }
1855 // shift-return (Mac default) or Ctrl-Return (GTK) for new line input
1856 else if( ( aEvt.GetKeyCode() == WXK_RETURN || aEvt.GetKeyCode() == WXK_NUMPAD_ENTER ) && aEvt.ShiftDown() )
1857 {
1858 wxObject* eventSource = aEvt.GetEventObject();
1859
1860 if( wxTextCtrl* textCtrl = dynamic_cast<wxTextCtrl*>( eventSource ) )
1861 {
1862 // If the text control is not multi-line, we want to close the dialog
1863 if( !textCtrl->IsMultiLine() )
1864 {
1865 wxPostEvent( this, wxCommandEvent( wxEVT_COMMAND_BUTTON_CLICKED, wxID_OK ) );
1866 return;
1867 }
1868
1869#if defined( __WXMAC__ ) || defined( __WXMSW__ )
1870 wxString eol = "\r\n";
1871#else
1872 wxString eol = "\n";
1873#endif
1874
1875 long pos = textCtrl->GetInsertionPoint();
1876 textCtrl->WriteText( eol );
1877 textCtrl->SetInsertionPoint( pos + eol.length() );
1878 return;
1879 }
1880 else if( wxStyledTextCtrl* scintilla = dynamic_cast<wxStyledTextCtrl*>( eventSource ) )
1881 {
1882 wxString eol = "\n";
1883
1884 switch( scintilla->GetEOLMode() )
1885 {
1886 case wxSTC_EOL_CRLF: eol = "\r\n"; break;
1887 case wxSTC_EOL_CR: eol = "\r"; break;
1888 case wxSTC_EOL_LF: eol = "\n"; break;
1889 }
1890
1891 long pos = scintilla->GetCurrentPos();
1892 scintilla->InsertText( pos, eol );
1893 scintilla->GotoPos( pos + eol.length() );
1894 return;
1895 }
1896 return;
1897 }
1898 // command-return (Mac default) or Ctrl-Return (GTK) for OK
1899 else if( ( aEvt.GetKeyCode() == WXK_RETURN || aEvt.GetKeyCode() == WXK_NUMPAD_ENTER ) && aEvt.ControlDown() )
1900 {
1901 wxPostEvent( this, wxCommandEvent( wxEVT_COMMAND_BUTTON_CLICKED, wxID_OK ) );
1902 return;
1903 }
1904 else if( aEvt.GetKeyCode() == WXK_TAB && !aEvt.ControlDown() )
1905 {
1906 wxWindow* currentWindow = wxWindow::FindFocus();
1907 int currentIdx = -1;
1908 int delta = aEvt.ShiftDown() ? -1 : 1;
1909
1910 auto advance =
1911 [&]( int& idx )
1912 {
1913 // Wrap-around modulus
1914 int size = (int) m_tabOrder.size();
1915 idx = ( ( idx + delta ) % size + size ) % size;
1916 };
1917
1918 for( size_t i = 0; i < m_tabOrder.size(); ++i )
1919 {
1920 // Check for exact match or if currentWindow is a child of the control
1921 // (e.g., the text entry inside a wxComboBox)
1922 if( m_tabOrder[i] == currentWindow
1923 || ( currentWindow && m_tabOrder[i]->IsDescendant( currentWindow ) ) )
1924 {
1925 currentIdx = (int) i;
1926 break;
1927 }
1928 }
1929
1930 if( currentIdx >= 0 )
1931 {
1932 advance( currentIdx );
1933
1934 // Skip hidden or disabled controls
1935 int startIdx = currentIdx;
1936
1937 while( !m_tabOrder[currentIdx]->IsShown() || !m_tabOrder[currentIdx]->IsEnabled() )
1938 {
1939 advance( currentIdx );
1940
1941 if( currentIdx == startIdx )
1942 break; // Avoid infinite loop if all controls are hidden
1943 }
1944
1945 //todo: We don't currently have non-textentry dialog boxes but this will break if
1946 // we add them.
1947#ifdef __APPLE__
1948 while( dynamic_cast<wxTextEntry*>( m_tabOrder[ currentIdx ] ) == nullptr )
1949 advance( currentIdx );
1950#endif
1951
1952 m_tabOrder[ currentIdx ]->SetFocus();
1953 return;
1954 }
1955 }
1956 else if( aEvt.GetKeyCode() == WXK_ESCAPE )
1957 {
1958 wxObject* eventSource = aEvt.GetEventObject();
1959
1960 if( wxTextCtrl* textCtrl = dynamic_cast<wxTextCtrl*>( eventSource ) )
1961 {
1962 // First escape after an edit cancels edit
1963 if( textCtrl->GetValue() != m_beforeEditValues[ textCtrl ] )
1964 {
1965 textCtrl->SetValue( m_beforeEditValues[ textCtrl ] );
1966 textCtrl->SelectAll();
1967 return;
1968 }
1969 }
1970 else if( wxStyledTextCtrl* scintilla = dynamic_cast<wxStyledTextCtrl*>( eventSource ) )
1971 {
1972 // First escape after an edit cancels edit
1973 if( scintilla->GetText() != m_beforeEditValues[ scintilla ] )
1974 {
1975 scintilla->SetText( m_beforeEditValues[ scintilla ] );
1976 scintilla->SelectAll();
1977 return;
1978 }
1979 }
1980 }
1981
1982 aEvt.Skip();
1983}
1984
1985
1986static void recursiveDescent( wxSizer* aSizer, std::map<int, wxString>& aLabels )
1987{
1988 wxStdDialogButtonSizer* sdbSizer = dynamic_cast<wxStdDialogButtonSizer*>( aSizer );
1989
1990 auto setupButton =
1991 [&]( wxButton* aButton )
1992 {
1993 if( aLabels.count( aButton->GetId() ) > 0 )
1994 {
1995 aButton->SetLabel( aLabels[ aButton->GetId() ] );
1996 }
1997 else
1998 {
1999 // wxWidgets has an uneven track record when the language is changed on
2000 // the fly so we set them even when they don't appear in the label map
2001 switch( aButton->GetId() )
2002 {
2003 case wxID_OK: aButton->SetLabel( _( "&OK" ) ); break;
2004 case wxID_CANCEL: aButton->SetLabel( _( "&Cancel" ) ); break;
2005 case wxID_YES: aButton->SetLabel( _( "&Yes" ) ); break;
2006 case wxID_NO: aButton->SetLabel( _( "&No" ) ); break;
2007 case wxID_APPLY: aButton->SetLabel( _( "&Apply" ) ); break;
2008 case wxID_SAVE: aButton->SetLabel( _( "&Save" ) ); break;
2009 case wxID_HELP: aButton->SetLabel( _( "&Help" ) ); break;
2010 case wxID_CONTEXT_HELP: aButton->SetLabel( _( "&Help" ) ); break;
2011 }
2012 }
2013 };
2014
2015 if( sdbSizer )
2016 {
2017 if( sdbSizer->GetAffirmativeButton() )
2018 setupButton( sdbSizer->GetAffirmativeButton() );
2019
2020 if( sdbSizer->GetApplyButton() )
2021 setupButton( sdbSizer->GetApplyButton() );
2022
2023 if( sdbSizer->GetNegativeButton() )
2024 setupButton( sdbSizer->GetNegativeButton() );
2025
2026 if( sdbSizer->GetCancelButton() )
2027 setupButton( sdbSizer->GetCancelButton() );
2028
2029 if( sdbSizer->GetHelpButton() )
2030 setupButton( sdbSizer->GetHelpButton() );
2031
2032 sdbSizer->Layout();
2033
2034 if( sdbSizer->GetAffirmativeButton() )
2035 sdbSizer->GetAffirmativeButton()->SetDefault();
2036 }
2037
2038 for( wxSizerItem* item : aSizer->GetChildren() )
2039 {
2040 if( item->GetSizer() )
2041 recursiveDescent( item->GetSizer(), aLabels );
2042 }
2043}
2044
2045
2046void DIALOG_SHIM::SetupStandardButtons( std::map<int, wxString> aLabels )
2047{
2048 recursiveDescent( GetSizer(), aLabels );
2049}
2050
2051
2052void DIALOG_SHIM::EndDialogShim( int aReturnCode )
2053{
2054 if( IsQuasiModal() )
2055 EndQuasiModal( aReturnCode );
2056 else
2057 EndDialog( aReturnCode ); // Call the default handler for modal and mode-less dialogs.
2058}
int index
const char * name
COMMON_SETTINGS_INTERNALS & CsInternals()
Dialog helper object to sit in the inheritance tree between wxDialog and any class written by wxFormB...
Definition dialog_shim.h:80
void SelectAllInTextCtrls(wxWindowList &children)
wxVariant getControlValue(wxWindow *aCtrl)
bool m_handlingUndoRedo
void onPropertyGridChanged(wxPropertyGridEvent &aEvent)
std::set< wxWindow * > m_noControlUndoRedo
std::vector< wxWindow * > m_tabOrder
void OnPaint(wxPaintEvent &event)
bool m_qmodal_showing
virtual void TearDownQuasiModal()
Override this method to perform dialog tear down actions not suitable for object dtor.
std::set< wxWindow * > m_controlsWithPendingChanges
void recordControlChange(wxWindow *aCtrl)
Tell the undo mechanism to snapshot the current value of a control and record it as an undo step when...
int vertPixelsFromDU(int y) const
Convert an integer number of dialog units to pixels, vertically.
bool Show(bool show) override
std::vector< UNDO_STEP > m_redoStack
void setControlValue(wxWindow *aCtrl, const wxVariant &aValue)
wxGUIEventLoop * m_qmodal_loop
void onChildSetFocus(wxFocusEvent &aEvent)
EDA_UNITS m_units
void LoadControlState()
Load persisted control values from the current project's local settings.
void ExcludeFromControlUndoRedo(wxWindow *aWindow)
Opt a control out of the dialog's generic Ctrl+Z/Ctrl+Y undo/redo.
void UnregisterUnitBinder(UNIT_BINDER *aUnitBinder)
Remove a UNIT_BINDER from the control-state save/restore map.
void OptOut(wxWindow *aWindow)
Opt out of control state saving.
void SaveControlState()
Save control values and geometry to the current project's local settings.
void SetupStandardButtons(std::map< int, wxString > aLabels={})
WINDOW_DISABLER * m_qmodal_parent_disabler
void clampToWorkArea()
Constrain the dialog's minimum size, size and position to the work area of the display it occupies,...
void onInitDialog(wxInitDialogEvent &aEvent)
std::string m_hash_key
bool m_firstPaintEvent
void OnActivate(wxActivateEvent &aEvent)
bool m_userResized
void onSpinDoubleEvent(wxSpinDoubleEvent &aEvent)
void resetUndoRedoForNewContent(wxWindowList &aChildren)
Reset undo/redo tracking after dynamically replacing child panels.
int horizPixelsFromDU(int x) const
Convert an integer number of dialog units to pixels, horizontally.
void resetSize()
Clear the existing dialog size and position.
std::map< wxWindow *, wxString > m_beforeEditValues
void setSizeInDU(int x, int y)
Set the dialog to the given dimensions in "dialog units".
void onDataViewListChanged(wxDataViewEvent &aEvent)
void unregisterUnitBinders(wxWindow *aWindow)
Remove UNIT_BINDER registrations for a window and all its descendants.
bool IsQuasiModal() const
bool m_useCalculatedSize
std::map< wxWindow *, UNIT_BINDER * > m_unitBinders
bool m_childReleased
void EndQuasiModal(int retCode)
void RegisterUnitBinder(UNIT_BINDER *aUnitBinder, wxWindow *aWindow)
Register a UNIT_BINDER so that it can handle units in control-state save/restore.
void OnMove(wxMoveEvent &aEvent)
void EndDialogShim(int aReturnCode)
A mode agnostic way to close a dialog.
void onCommandEvent(wxCommandEvent &aEvent)
void CleanupAfterModalSubDialog()
std::string generateKey(const wxWindow *aWin) const
void PrepareForModalSubDialog()
void OnButton(wxCommandEvent &aEvent)
Properly handle the default button events when in the quasimodal mode when not calling EndQuasiModal ...
void forceInitialFocus()
Focus the requested initial target if it is visible, otherwise focus the dialog itself so keyboard ev...
void onGridCellChanged(wxGridEvent &aEvent)
void finishDialogSettings()
In all dialogs, we must call the same functions to fix minimal dlg size, the default position and per...
void registerUndoRedoHandlers(wxWindowList &aChildren)
wxWindow * m_initialFocusTarget
void OnSize(wxSizeEvent &aEvent)
bool Enable(bool enable) override
DIALOG_SHIM(wxWindow *aParent, wxWindowID id, const wxString &title, const wxPoint &pos=wxDefaultPosition, const wxSize &size=wxDefaultSize, long style=wxDEFAULT_FRAME_STYLE|wxRESIZE_BORDER, const wxString &name=wxDialogNameStr)
void focusParentCanvas(bool aDeferUntilFrameActive=false)
Set focus back to the parent frame's tool canvas if available, otherwise to the parent window.
void SetPosition(const wxPoint &aNewPosition)
Force the position of the dialog to a new position.
void onSpinEvent(wxSpinEvent &aEvent)
bool m_userPositioned
void OnCloseWindow(wxCloseEvent &aEvent)
Properly handle the wxCloseEvent when in the quasimodal mode when not calling EndQuasiModal which is ...
std::map< wxWindow *, wxVariant > m_currentValues
wxSize m_initialSize
EDA_BASE_FRAME * m_parentFrame
virtual void OnCharHook(wxKeyEvent &aEvt)
std::vector< UNDO_STEP > m_undoStack
void onStyledTextChanged(wxStyledTextEvent &aEvent)
void flushPendingControlChanges()
Apply outstanding control changes to the undo stack, and clear the pending list.
int ShowModal() override
The base frame for deriving all KiCad main window classes.
KIWAY_HOLDER(KIWAY *aKiway, HOLDER_TYPE aType)
KIWAY & Kiway() const
Return a reference to the KIWAY that this object has an opportunity to participate in.
void SetKiway(wxWindow *aDest, KIWAY *aKiway)
It is only used for debugging, since "this" is not a wxWindow*.
bool HasKiway() const
Safety check before asking for the Kiway reference.
HOLDER_TYPE GetType() const
void SetBlockingDialog(wxWindow *aWin)
Definition kiway.cpp:686
Definition raii.h:34
virtual COMMON_SETTINGS * GetCommonSettings() const
Definition pgm_base.cpp:546
virtual wxApp & App()
Return a bare naked wxApp which may come from wxPython, SINGLE_TOP, or kicad.exe.
Definition pgm_base.cpp:202
bool SetProperty(const std::string &aKey, T &&aValue)
Set a property with the given key and value.
static PROPERTY_HOLDER * SafeCast(void *aPtr) noexcept
Safely cast a void pointer to PROPERTY_HOLDER*.
virtual wxWindow * GetToolCanvas() const =0
Canvas access.
EDA_UNITS GetUserUnits() const
Temporarily disable a window, and then re-enable on destruction.
Definition raii.h:83
static std::string getDialogKeyFromTitle(const wxString &aTitle)
Strip parenthetical suffixes from dialog titles to create stable persistence keys.
static void recursiveDescent(wxSizer *aSizer, std::map< int, wxString > &aLabels)
static bool isCompoundDateTimePicker(const wxWindow *aWin)
Return true when the given window is a compound date/time picker whose internal children should be op...
wxRect ClampRectToDisplay(const wxRect &aRect, const wxRect &aClientArea)
Constrain a window rectangle so it fits entirely within a display's client area.
KICOMMON_API wxRect ClampRectToDisplay(const wxRect &aRect, const wxRect &aClientArea)
Constrain a window rectangle so it fits entirely within a display's client area.
const int minSize
Push and Shove router track width and via size dialog.
#define _(s)
EDA_UNITS
Definition eda_units.h:44
void ignore_unused(const T &)
Definition ignore.h:20
void AddNavigationBreadcrumb(const wxString &aMsg, const wxString &aCategory)
Add a navigation breadcrumb.
void ReleaseChildWindow(wxNonOwnedWindow *aWindow)
Release a modal window's parent-child relationship with its parent window.
Definition wxgtk/ui.cpp:515
void FixupCancelButtonCmdKeyCollision(wxWindow *aWindow)
Definition wxgtk/ui.cpp:266
void StabilizeWindowPosition(wxWindow *aWindow)
Prepare a top-level window for reliable position round-tripping.
Definition wxgtk/ui.cpp:170
void EnsureVisible(wxWindow *aWindow)
Ensure that a window is visible on the screen.
Definition wxgtk/ui.cpp:164
bool IsWindowActive(wxWindow *aWindow)
Check to see if the given window is the currently active window (e.g.
Definition wxgtk/ui.cpp:149
void ForceFocus(wxWindow *aWindow)
Pass the current focus to the window.
Definition wxgtk/ui.cpp:127
void ReparentModal(wxNonOwnedWindow *aWindow)
Move a window's parent to be the top-level window and force the window to be on top.
Definition wxgtk/ui.cpp:254
STL namespace.
static wxString makeKey(const wxString &aFirst, const wxString &aSecond)
Assemble a two part key as a simple concatenation of aFirst and aSecond parts, using a separator.
PGM_BASE & Pgm()
The global program "get" accessor.
see class PGM_BASE
std::map< std::string, std::map< std::string, nlohmann::json > > m_dialogControlValues
static const long long MM
VECTOR2I end
int delta
@ MD_ALT
Definition tool_event.h:141
@ MD_CTRL
Definition tool_event.h:140
@ MD_SHIFT
Definition tool_event.h:139