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