KiCad PCB EDA Suite
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 <dick@softplc.com>
5  * Copyright (C) 2012-2020 KiCad Developers, see AUTHORS.txt for contributors.
6  *
7  * This program is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License
9  * as published by the Free Software Foundation; either version 2
10  * of the License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program; if not, you may find one here:
19  * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
20  * or you may search the http://www.gnu.org website for the version 2 license,
21  * or you may write to the Free Software Foundation, Inc.,
22  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
23  */
24 
25 #include <dialog_shim.h>
26 #include <kiway_player.h>
27 #include <pgm_base.h>
28 #include <tool/tool_manager.h>
29 #include <kiplatform/ui.h>
30 
31 #include <wx/display.h>
32 #include <wx/evtloop.h>
33 #include <wx/app.h>
34 #include <wx/event.h>
35 #include <wx/grid.h>
36 #include <wx/bmpbuttn.h>
37 #include <wx/textctrl.h>
38 
39 #include <algorithm>
40 
43 {
44  wxWindow* m_win;
45 
46 public:
47 
48  WDO_ENABLE_DISABLE( wxWindow* aWindow ) :
49  m_win( aWindow )
50  {
51  if( m_win )
52  m_win->Disable();
53  }
54 
56  {
57  if( m_win )
58  {
59  m_win->Enable();
60  m_win->SetFocus(); // let's focus back on the parent window
61  }
62  }
63 };
64 
65 
66 BEGIN_EVENT_TABLE( DIALOG_SHIM, wxDialog )
67  // If dialog has a grid and the grid has an active cell editor
68  // Esc key closes cell editor, otherwise Esc key closes the dialog.
69  EVT_GRID_EDITOR_SHOWN( DIALOG_SHIM::OnGridEditorShown )
70  EVT_GRID_EDITOR_HIDDEN( DIALOG_SHIM::OnGridEditorHidden )
71  EVT_CHAR_HOOK( DIALOG_SHIM::OnCharHook )
72 END_EVENT_TABLE()
73 
74 
75 DIALOG_SHIM::DIALOG_SHIM( wxWindow* aParent, wxWindowID id, const wxString& title,
76  const wxPoint& pos, const wxSize& size, long style,
77  const wxString& name ) :
78  wxDialog( aParent, id, title, pos, size, style, name ),
79  KIWAY_HOLDER( nullptr, KIWAY_HOLDER::DIALOG ),
80  m_units( EDA_UNITS::MILLIMETRES ),
81  m_useCalculatedSize( false ),
82  m_firstPaintEvent( true ),
83  m_initialFocusTarget( nullptr ),
84  m_qmodal_loop( nullptr ),
85  m_qmodal_showing( false ),
86  m_qmodal_parent_disabler( nullptr ),
87  m_parentFrame( nullptr )
88 {
89  KIWAY_HOLDER* kiwayHolder = nullptr;
90 
91  if( aParent )
92  {
93  kiwayHolder = dynamic_cast<KIWAY_HOLDER*>( aParent );
94 
95  while( !kiwayHolder && aParent->GetParent() )
96  {
97  aParent = aParent->GetParent();
98  kiwayHolder = dynamic_cast<KIWAY_HOLDER*>( aParent );
99  }
100  }
101 
102  // Inherit units from parent
103  if( kiwayHolder && kiwayHolder->GetType() == KIWAY_HOLDER::FRAME )
104  m_units = static_cast<EDA_BASE_FRAME*>( kiwayHolder )->GetUserUnits();
105  else if( kiwayHolder && kiwayHolder->GetType() == KIWAY_HOLDER::DIALOG )
106  m_units = static_cast<DIALOG_SHIM*>( kiwayHolder )->GetUserUnits();
107 
108  // Don't mouse-warp after a dialog run from the context menu
109  if( kiwayHolder && kiwayHolder->GetType() == KIWAY_HOLDER::FRAME )
110  {
111  m_parentFrame = static_cast<EDA_BASE_FRAME*>( kiwayHolder );
112  TOOL_MANAGER* toolMgr = m_parentFrame->GetToolManager();
113 
114  if( toolMgr && toolMgr->IsContextMenuActive() )
115  toolMgr->VetoContextMenuMouseWarp();
116  }
117 
118  // Set up the message bus
119  if( kiwayHolder )
120  SetKiway( this, &kiwayHolder->Kiway() );
121 
122  Bind( wxEVT_CLOSE_WINDOW, &DIALOG_SHIM::OnCloseWindow, this );
123  Bind( wxEVT_BUTTON, &DIALOG_SHIM::OnButton, this );
124 
125 #ifdef __WINDOWS__
126  // On Windows, the app top windows can be brought to the foreground (at least temporarily)
127  // in certain circumstances such as when calling an external tool in Eeschema BOM generation.
128  // So set the parent frame (if exists) to top window to avoid this annoying behavior.
129  if( kiwayHolder && kiwayHolder->GetType() == KIWAY_HOLDER::FRAME )
130  Pgm().App().SetTopWindow( (EDA_BASE_FRAME*) kiwayHolder );
131 #endif
132 
133  Connect( wxEVT_PAINT, wxPaintEventHandler( DIALOG_SHIM::OnPaint ) );
134 }
135 
136 
138 {
139  // if the dialog is quasi-modal, this will end its event loop
140  if( IsQuasiModal() )
141  EndQuasiModal( wxID_CANCEL );
142 
144  delete m_qmodal_parent_disabler; // usually NULL by now
145 }
146 
147 
149 {
150  // must be called from the constructor of derived classes,
151  // when all widgets are initialized, and therefore their size fixed
152 
153  // SetSizeHints fixes the minimal size of sizers in the dialog
154  // (SetSizeHints calls Fit(), so no need to call it)
155  GetSizer()->SetSizeHints( this );
156 }
157 
158 
159 void DIALOG_SHIM::setSizeInDU( int x, int y )
160 {
161  wxSize sz( x, y );
162  SetSize( ConvertDialogToPixels( sz ) );
163 }
164 
165 
167 {
168  wxSize sz( x, 0 );
169  return ConvertDialogToPixels( sz ).x;
170 }
171 
172 
174 {
175  wxSize sz( 0, y );
176  return ConvertDialogToPixels( sz ).y;
177 }
178 
179 
180 // our hashtable is an implementation secret, don't need or want it in a header file
181 #include <hashtables.h>
182 #include <typeinfo>
183 
184 static std::unordered_map<std::string, wxRect> class_map;
185 
186 
187 void DIALOG_SHIM::SetPosition( const wxPoint& aNewPosition )
188 {
189  wxDialog::SetPosition( aNewPosition );
190 
191  // Now update the stored position:
192  const char* hash_key;
193 
194  if( m_hash_key.size() )
195  {
196  // a special case like EDA_LIST_DIALOG, which has multiple uses.
197  hash_key = m_hash_key.c_str();
198  }
199  else
200  {
201  hash_key = typeid(*this).name();
202  }
203 
204  std::unordered_map<std::string, wxRect>::iterator it = class_map.find( hash_key );
205 
206  if( it == class_map.end() )
207  return;
208 
209  wxRect rect = it->second;
210  rect.SetPosition( aNewPosition );
211 
212  class_map[ hash_key ] = rect;
213 }
214 
215 
216 bool DIALOG_SHIM::Show( bool show )
217 {
218  bool ret;
219  const char* hash_key;
220 
221  if( m_hash_key.size() )
222  {
223  // a special case like EDA_LIST_DIALOG, which has multiple uses.
224  hash_key = m_hash_key.c_str();
225  }
226  else
227  {
228  hash_key = typeid(*this).name();
229  }
230 
231  // Show or hide the window. If hiding, save current position and size.
232  // If showing, use previous position and size.
233  if( show )
234  {
235 #ifndef __WINDOWS__
236  wxDialog::Raise(); // Needed on OS X and some other window managers (i.e. Unity)
237 #endif
238  ret = wxDialog::Show( show );
239 
240  // classname is key, returns a zeroed out default EDA_RECT if none existed before.
241  wxRect savedDialogRect = class_map[ hash_key ];
242 
243  if( savedDialogRect.GetSize().x != 0 && savedDialogRect.GetSize().y != 0 )
244  {
245  if( m_useCalculatedSize )
246  {
247  SetSize( savedDialogRect.GetPosition().x, savedDialogRect.GetPosition().y,
248  wxDialog::GetSize().x, wxDialog::GetSize().y, 0 );
249  }
250  else
251  {
252  SetSize( savedDialogRect.GetPosition().x, savedDialogRect.GetPosition().y,
253  std::max( wxDialog::GetSize().x, savedDialogRect.GetSize().x ),
254  std::max( wxDialog::GetSize().y, savedDialogRect.GetSize().y ),
255  0 );
256  }
257  }
258 
259  // Be sure that the dialog appears in a visible area
260  // (the dialog position might have been stored at the time when it was
261  // shown on another display)
262  if( wxDisplay::GetFromWindow( this ) == wxNOT_FOUND )
263  Centre();
264  }
265  else
266  {
267  // Save the dialog's position & size before hiding, using classname as key
268  class_map[ hash_key ] = wxRect( wxDialog::GetPosition(), wxDialog::GetSize() );
269 
270 #ifdef __WXMAC__
271  if ( m_eventLoop )
272  m_eventLoop->Exit( GetReturnCode() ); // Needed for APP-MODAL dlgs on OSX
273 #endif
274 
275  ret = wxDialog::Show( show );
276  }
277 
278  return ret;
279 }
280 
281 
283 {
284  const char* hash_key;
285 
286  if( m_hash_key.size() )
287  {
288  // a special case like EDA_LIST_DIALOG, which has multiple uses.
289  hash_key = m_hash_key.c_str();
290  }
291  else
292  {
293  hash_key = typeid(*this).name();
294  }
295 
296  std::unordered_map<std::string, wxRect>::iterator it = class_map.find( hash_key );
297 
298  if( it == class_map.end() )
299  return;
300 
301  wxRect rect = it->second;
302  rect.SetSize( wxSize( 0, 0 ) );
303  class_map[ hash_key ] = rect;
304 }
305 
306 
307 bool DIALOG_SHIM::Enable( bool enable )
308 {
309  // so we can do logging of this state change:
310  return wxDialog::Enable( enable );
311 }
312 
313 
314 // Recursive descent doing a SelectAll() in wxTextCtrls.
315 // MacOS User Interface Guidelines state that when tabbing to a text control all its
316 // text should be selected. Since wxWidgets fails to implement this, we do it here.
317 static void selectAllInTextCtrls( wxWindowList& children )
318 {
319  for( wxWindow* child : children )
320  {
321  if( wxTextCtrl* childTextCtrl = dynamic_cast<wxTextCtrl*>( child ) )
322  {
323 #if defined( __WXMAC__ ) || defined( __WXMSW__ )
324  // Respect an existing selection
325  if( childTextCtrl->GetStringSelection().IsEmpty() )
326  childTextCtrl->SelectAll();
327 #endif
328  }
329 #ifdef __WXMAC__
330  // Temp hack for square (looking) buttons on OSX. Will likely be made redundant
331  // by the image store....
332  else if( dynamic_cast<wxBitmapButton*>( child ) != nullptr )
333  {
334  wxSize minSize( 29, 27 );
335  wxRect rect = child->GetRect();
336 
337  child->ConvertDialogToPixels( minSize );
338 
339  rect.Inflate( std::max( 0, minSize.x - rect.GetWidth() ),
340  std::max( 0, minSize.y - rect.GetHeight() ) );
341 
342  child->SetMinSize( rect.GetSize() );
343  child->SetSize( rect );
344  }
345 #endif
346  else
347  {
348  selectAllInTextCtrls( child->GetChildren() );
349  }
350  }
351 }
352 
353 
354 void DIALOG_SHIM::OnPaint( wxPaintEvent &event )
355 {
356  if( m_firstPaintEvent )
357  {
359 
360  selectAllInTextCtrls( GetChildren() );
361 
364  else
365  KIPLATFORM::UI::ForceFocus( this ); // Focus the dialog itself
366 
367  m_firstPaintEvent = false;
368  }
369 
370  event.Skip();
371 }
372 
373 
374 /*
375  Quasi-Modal Mode Explained:
376 
377  The gtk calls in wxDialog::ShowModal() cause event routing problems if that
378  modal dialog then tries to use KIWAY_PLAYER::ShowModal(). The latter shows up
379  and mostly works but does not respond to the window decoration close button.
380  There is no way to get around this without reversing the gtk calls temporarily.
381 
382  Quasi-Modal mode is our own almost modal mode which disables only the parent
383  of the DIALOG_SHIM, leaving other frames operable and while staying captured in the
384  nested event loop. This avoids the gtk calls and leaves event routing pure
385  and sufficient to operate the KIWAY_PLAYER::ShowModal() properly. When using
386  ShowQuasiModal() you have to use EndQuasiModal() in your dialogs and not
387  EndModal(). There is also IsQuasiModal() but its value can only be true
388  when the nested event loop is active. Do not mix the modal and quasi-modal
389  functions. Use one set or the other.
390 
391  You might find this behavior preferable over a pure modal mode, and it was said
392  that only the Mac has this natively, but now other platforms have something
393  similar. You CAN use it anywhere for any dialog. But you MUST use it when
394  you want to use KIWAY_PLAYER::ShowModal() from a dialog event.
395 */
396 
398 {
399  // This is an exception safe way to zero a pointer before returning.
400  // Yes, even though DismissModal() clears this first normally, this is
401  // here in case there's an exception before the dialog is dismissed.
402  struct NULLER
403  {
404  void*& m_what;
405  NULLER( void*& aPtr ) : m_what( aPtr ) {}
406  ~NULLER() { m_what = nullptr; } // indeed, set it to NULL on destruction
407  } clear_this( (void*&) m_qmodal_loop );
408 
409  // release the mouse if it's currently captured as the window having it
410  // will be disabled when this dialog is shown -- but will still keep the
411  // capture making it impossible to do anything in the modal dialog itself
412  wxWindow* win = wxWindow::GetCapture();
413  if( win )
414  win->ReleaseMouse();
415 
416  // Get the optimal parent
417  wxWindow* parent = GetParentForModalDialog( GetParent(), GetWindowStyle() );
418 
419  wxASSERT_MSG( !m_qmodal_parent_disabler,
420  wxT( "Caller using ShowQuasiModal() twice on same window?" ) );
421 
422  // quasi-modal: disable only my "optimal" parent
424 
425  // Apple in its infinite wisdom will raise a disabled window before even passing
426  // us the event, so we have no way to stop it. Instead, we must set an order on
427  // the windows so that the quasi-modal will be pushed in front of the disabled
428  // window when it is raised.
430 
431  Show( true );
432 
433  m_qmodal_showing = true;
434 
435  WX_EVENT_LOOP event_loop;
436 
437  m_qmodal_loop = &event_loop;
438 
439  event_loop.Run();
440 
441  m_qmodal_showing = false;
442 
443  return GetReturnCode();
444 }
445 
446 
447 void DIALOG_SHIM::EndQuasiModal( int retCode )
448 {
449  // Hook up validator and transfer data from controls handling so quasi-modal dialogs
450  // handle validation in the same way as other dialogs.
451  if( ( retCode == wxID_OK ) && ( !Validate() || !TransferDataFromWindow() ) )
452  return;
453 
454  SetReturnCode( retCode );
455 
456  if( !IsQuasiModal() )
457  {
458  wxFAIL_MSG( "either DIALOG_SHIM::EndQuasiModal called twice or ShowQuasiModal"
459  "wasn't called" );
460  return;
461  }
462 
463  if( m_qmodal_loop )
464  {
465  if( m_qmodal_loop->IsRunning() )
466  m_qmodal_loop->Exit( 0 );
467  else
468  m_qmodal_loop->ScheduleExit( 0 );
469 
470  m_qmodal_loop = nullptr;
471  }
472 
474  m_qmodal_parent_disabler = nullptr;
475 
476  Show( false );
477 }
478 
479 
480 void DIALOG_SHIM::OnCloseWindow( wxCloseEvent& aEvent )
481 {
482  if( IsQuasiModal() )
483  {
484  EndQuasiModal( wxID_CANCEL );
485  return;
486  }
487 
488  // This is mandatory to allow wxDialogBase::OnCloseWindow() to be called.
489  aEvent.Skip();
490 }
491 
492 
493 void DIALOG_SHIM::OnButton( wxCommandEvent& aEvent )
494 {
495  const int id = aEvent.GetId();
496 
497  // If we are pressing a button to exit, we need to enable the escapeID
498  // otherwise the dialog does not process cancel
499  if( id == wxID_CANCEL )
500  SetEscapeId( wxID_ANY );
501 
502  if( IsQuasiModal() )
503  {
504  if( id == GetAffirmativeId() )
505  {
506  EndQuasiModal( id );
507  }
508  else if( id == wxID_APPLY )
509  {
510  // Dialogs that provide Apply buttons should make sure data is valid before
511  // allowing a transfer, as there is no other way to indicate failure
512  // (i.e. the dialog can't refuse to close as it might with OK, because it
513  // isn't closing anyway)
514  if( Validate() )
515  {
516  bool success = TransferDataFromWindow();
517  (void) success;
518  }
519  }
520  else if( id == GetEscapeId() ||
521  (id == wxID_CANCEL && GetEscapeId() == wxID_ANY) )
522  {
523  EndQuasiModal( wxID_CANCEL );
524  }
525  else // not a standard button
526  {
527  aEvent.Skip();
528  }
529 
530  return;
531  }
532 
533  // This is mandatory to allow wxDialogBase::OnButton() to be called.
534  aEvent.Skip();
535 }
536 
537 
538 void DIALOG_SHIM::OnCharHook( wxKeyEvent& aEvt )
539 {
540  if( aEvt.GetKeyCode() == 'U' && aEvt.GetModifiers() == wxMOD_CONTROL )
541  {
542  if( m_parentFrame )
543  {
545  return;
546  }
547  }
548  // shift-return (Mac default) or Ctrl-Return (GTK) for OK
549  else if( aEvt.GetKeyCode() == WXK_RETURN && ( aEvt.ShiftDown() || aEvt.ControlDown() ) )
550  {
551  wxPostEvent( this, wxCommandEvent( wxEVT_COMMAND_BUTTON_CLICKED, wxID_OK ) );
552  return;
553  }
554  else if( aEvt.GetKeyCode() == WXK_TAB && !aEvt.ControlDown() )
555  {
556  wxWindow* currentWindow = wxWindow::FindFocus();
557  int currentIdx = -1;
558  int delta = aEvt.ShiftDown() ? -1 : 1;
559 
560  auto advance = [&]( int& idx )
561  {
562  // Wrap-around modulus
563  int size = m_tabOrder.size();
564  idx = ( ( idx + delta ) % size + size ) % size;
565  };
566 
567  for( size_t i = 0; i < m_tabOrder.size(); ++i )
568  {
569  if( m_tabOrder[i] == currentWindow )
570  {
571  currentIdx = (int) i;
572  break;
573  }
574  }
575 
576  if( currentIdx >= 0 )
577  {
578  advance( currentIdx );
579 
580  //todo: We don't currently have non-textentry dialog boxes but this will break if
581  // we add them.
582 #ifdef __APPLE__
583  while( dynamic_cast<wxTextEntry*>( m_tabOrder[ currentIdx ] ) == nullptr )
584  advance( currentIdx );
585 #endif
586 
587  m_tabOrder[ currentIdx ]->SetFocus();
588  return;
589  }
590  }
591 
592  aEvt.Skip();
593 }
594 
595 
596 void DIALOG_SHIM::OnGridEditorShown( wxGridEvent& event )
597 {
598  SetEscapeId( wxID_NONE );
599  event.Skip();
600 }
601 
602 
603 void DIALOG_SHIM::OnGridEditorHidden( wxGridEvent& event )
604 {
605  SetEscapeId( wxID_ANY );
606  event.Skip();
607 }
bool m_qmodal_showing
Definition: dialog_shim.h:213
A mix in class which holds the location of a wxWindow's KIWAY.
Definition: kiway_holder.h:36
void setSizeInDU(int x, int y)
Set the dialog to the given dimensions in "dialog units".
#define WX_EVENT_LOOP
Definition: kiway_player.h:41
KIWAY & Kiway() const
Return a reference to the KIWAY that this object has an opportunity to participate in.
Definition: kiway_holder.h:53
void OnButton(wxCommandEvent &aEvent)
Properly handle the default button events when in the quasimodal mode when not calling EndQuasiModal ...
void SetPosition(wxString aStr, const wxString &aDefaultMeasurementUnit, int *aX, int *aY, const wxString &aActualConversion)
std::string m_hash_key
Definition: dialog_shim.h:199
std::vector< wxWindow * > m_tabOrder
Definition: dialog_shim.h:218
HOLDER_TYPE GetType() const
Definition: kiway_holder.h:46
bool IsQuasiModal() const
Definition: dialog_shim.h:106
static std::unordered_map< std::string, wxRect > class_map
wxWindow * m_initialFocusTarget
Definition: dialog_shim.h:209
int horizPixelsFromDU(int x) const
Convert an integer number of dialog units to pixels, horizontally.
Dialog helper object to sit in the inheritance tree between wxDialog and any class written by wxFormB...
Definition: dialog_shim.h:82
void OnGridEditorShown(wxGridEvent &event)
const int minSize
Push and Shove router track width and via size dialog.
KIWAY Kiway & Pgm(), KFCTL_STANDALONE
The global Program "get" accessor.
Definition: single_top.cpp:106
void OnGridEditorHidden(wxGridEvent &event)
bool m_firstPaintEvent
Definition: dialog_shim.h:208
WX_EVENT_LOOP * m_qmodal_loop
Definition: dialog_shim.h:211
Master controller class:
Definition: tool_manager.h:54
void OnCloseWindow(wxCloseEvent &aEvent)
Properly handle the wxCloseEvent when in the quasimodal mode when not calling EndQuasiModal which is ...
void SetPosition(const wxPoint &aNewPosition)
Force the position of the dialog to a new position.
EDA_BASE_FRAME * m_parentFrame
Definition: dialog_shim.h:216
int ShowQuasiModal()
bool IsContextMenuActive() const
True while processing a context menu.
Definition: tool_manager.h:411
void OnPaint(wxPaintEvent &event)
bool Show(bool show) override
EDA_UNITS
Definition: eda_units.h:38
WDO_ENABLE_DISABLE * m_qmodal_parent_disabler
Definition: dialog_shim.h:214
void finishDialogSettings()
In all dialogs, we must call the same functions to fix minimal dlg size, the default position and per...
virtual void OnCharHook(wxKeyEvent &aEvt)
void EndQuasiModal(int retCode)
virtual void ToggleUserUnits()
see class PGM_BASE
const char * name
Definition: DXF_plotter.cpp:59
void ForceFocus(wxWindow *aWindow)
Pass the current focus to the window.
Definition: gtk/ui.cpp:44
void FixupCancelButtonCmdKeyCollision(wxWindow *aWindow)
Definition: gtk/ui.cpp:71
The base frame for deriving all KiCad main window classes.
void VetoContextMenuMouseWarp()
Disable mouse warping after the current context menu is closed.
Definition: tool_manager.h:422
Toggle a window's "enable" status to disabled, then enabled on destruction.
Definition: dialog_shim.cpp:42
void resetSize()
Clear the existing dialog size and position.
int vertPixelsFromDU(int y) const
Convert an integer number of dialog units to pixels, vertically.
bool Enable(bool enable) override
void ReparentQuasiModal(wxNonOwnedWindow *aWindow)
Move a window's parent to be the top-level window and force the window to be on top.
Definition: gtk/ui.cpp:65
WDO_ENABLE_DISABLE(wxWindow *aWindow)
Definition: dialog_shim.cpp:48
bool m_useCalculatedSize
Definition: dialog_shim.h:203
static void selectAllInTextCtrls(wxWindowList &children)