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 <eda_rect.h>
27 #include <kiway_player.h>
28 #include <pgm_base.h>
29 #include <tool/tool_manager.h>
30 
31 #include <kiplatform/ui.h>
32 
33 #include <wx/display.h>
34 #include <wx/evtloop.h>
35 #include <wx/app.h>
36 #include <wx/event.h>
37 #include <wx/grid.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, const wxString& name )
77  : wxDialog( aParent, id, title, pos, size, style, name ),
78  KIWAY_HOLDER( nullptr, KIWAY_HOLDER::DIALOG ),
79  m_units( EDA_UNITS::MILLIMETRES ),
80  m_firstPaintEvent( true ),
81  m_initialFocusTarget( nullptr ),
82  m_qmodal_loop( nullptr ),
83  m_qmodal_showing( false ),
84  m_qmodal_parent_disabler( nullptr ),
85  m_parentFrame( nullptr )
86 {
87  KIWAY_HOLDER* kiwayHolder = nullptr;
88 
89  if( aParent )
90  {
91  kiwayHolder = dynamic_cast<KIWAY_HOLDER*>( aParent );
92 
93  while( !kiwayHolder && aParent->GetParent() )
94  {
95  aParent = aParent->GetParent();
96  kiwayHolder = dynamic_cast<KIWAY_HOLDER*>( aParent );
97  }
98  }
99 
100  // Inherit units from parent
101  if( kiwayHolder && kiwayHolder->GetType() == KIWAY_HOLDER::FRAME )
102  m_units = static_cast<EDA_BASE_FRAME*>( kiwayHolder )->GetUserUnits();
103  else if( kiwayHolder && kiwayHolder->GetType() == KIWAY_HOLDER::DIALOG )
104  m_units = static_cast<DIALOG_SHIM*>( kiwayHolder )->GetUserUnits();
105 
106  // Don't mouse-warp after a dialog run from the context menu
107  if( kiwayHolder && kiwayHolder->GetType() == KIWAY_HOLDER::FRAME )
108  {
109  m_parentFrame = static_cast<EDA_BASE_FRAME*>( kiwayHolder );
110  TOOL_MANAGER* toolMgr = m_parentFrame->GetToolManager();
111 
112  if( toolMgr && toolMgr->IsContextMenuActive() )
113  toolMgr->VetoContextMenuMouseWarp();
114  }
115 
116  // Set up the message bus
117  if( kiwayHolder )
118  SetKiway( this, &kiwayHolder->Kiway() );
119 
120  Bind( wxEVT_CLOSE_WINDOW, &DIALOG_SHIM::OnCloseWindow, this );
121  Bind( wxEVT_BUTTON, &DIALOG_SHIM::OnButton, this );
122 
123 #ifdef __WINDOWS__
124  // On Windows, the app top windows can be brought to the foreground (at least temporarily)
125  // in certain circumstances such as when calling an external tool in Eeschema BOM generation.
126  // So set the parent frame (if exists) to top window to avoid this annoying behavior.
127  if( kiwayHolder && kiwayHolder->GetType() == KIWAY_HOLDER::FRAME )
128  Pgm().App().SetTopWindow( (EDA_BASE_FRAME*) kiwayHolder );
129 #endif
130 
131  Connect( wxEVT_PAINT, wxPaintEventHandler( DIALOG_SHIM::OnPaint ) );
132 }
133 
134 
136 {
137  // if the dialog is quasi-modal, this will end its event loop
138  if( IsQuasiModal() )
139  EndQuasiModal( wxID_CANCEL );
140 
142  delete m_qmodal_parent_disabler; // usually NULL by now
143 }
144 
145 
147 {
148  // must be called from the constructor of derived classes,
149  // when all widgets are initialized, and therefore their size fixed
150 
151  // SetSizeHints fixes the minimal size of sizers in the dialog
152  // (SetSizeHints calls Fit(), so no need to call it)
153  GetSizer()->SetSizeHints( this );
154 }
155 
156 
157 void DIALOG_SHIM::SetSizeInDU( int x, int y )
158 {
159  wxSize sz( x, y );
160  SetSize( ConvertDialogToPixels( sz ) );
161 }
162 
163 
165 {
166  wxSize sz( x, 0 );
167  return ConvertDialogToPixels( sz ).x;
168 }
169 
170 
172 {
173  wxSize sz( 0, y );
174  return ConvertDialogToPixels( sz ).y;
175 }
176 
177 
178 // our hashtable is an implementation secret, don't need or want it in a header file
179 #include <hashtables.h>
180 #include <typeinfo>
181 
183 
184 bool DIALOG_SHIM::Show( bool show )
185 {
186  bool ret;
187  const char* hash_key;
188 
189  if( m_hash_key.size() )
190  {
191  // a special case like EDA_LIST_DIALOG, which has multiple uses.
192  hash_key = m_hash_key.c_str();
193  }
194  else
195  {
196  hash_key = typeid(*this).name();
197  }
198 
199  // Show or hide the window. If hiding, save current position and size.
200  // If showing, use previous position and size.
201  if( show )
202  {
203 #ifndef __WINDOWS__
204  wxDialog::Raise(); // Needed on OS X and some other window managers (i.e. Unity)
205 #endif
206  ret = wxDialog::Show( show );
207 
208  // classname is key, returns a zeroed out default EDA_RECT if none existed before.
209  EDA_RECT savedDialogRect = class_map[ hash_key ];
210 
211  if( savedDialogRect.GetSize().x != 0 && savedDialogRect.GetSize().y != 0 )
212  {
213  SetSize( savedDialogRect.GetPosition().x,
214  savedDialogRect.GetPosition().y,
215  std::max( wxDialog::GetSize().x, savedDialogRect.GetSize().x ),
216  std::max( wxDialog::GetSize().y, savedDialogRect.GetSize().y ),
217  0 );
218  }
219 
220  // Be sure that the dialog appears in a visible area
221  // (the dialog position might have been stored at the time when it was
222  // shown on another display)
223  if( wxDisplay::GetFromWindow( this ) == wxNOT_FOUND )
224  Centre();
225  }
226  else
227  {
228  // Save the dialog's position & size before hiding, using classname as key
229  class_map[ hash_key ] = EDA_RECT( wxDialog::GetPosition(), wxDialog::GetSize() );
230 
231 #ifdef __WXMAC__
232  if ( m_eventLoop )
233  m_eventLoop->Exit( GetReturnCode() ); // Needed for APP-MODAL dlgs on OSX
234 #endif
235 
236  ret = wxDialog::Show( show );
237  }
238 
239  return ret;
240 }
241 
242 
244 {
245  const char* hash_key;
246 
247  if( m_hash_key.size() )
248  {
249  // a special case like EDA_LIST_DIALOG, which has multiple uses.
250  hash_key = m_hash_key.c_str();
251  }
252  else
253  {
254  hash_key = typeid(*this).name();
255  }
256 
257  RECT_MAP::iterator it = class_map.find( hash_key );
258 
259  if( it == class_map.end() )
260  return;
261 
262  EDA_RECT rect = it->second;
263  rect.SetSize( 0, 0 );
264  class_map[ hash_key ] = rect;
265 }
266 
267 
268 bool DIALOG_SHIM::Enable( bool enable )
269 {
270  // so we can do logging of this state change:
271  return wxDialog::Enable( enable );
272 }
273 
274 
275 // Recursive descent doing a SelectAll() in wxTextCtrls.
276 // MacOS User Interface Guidelines state that when tabbing to a text control all its
277 // text should be selected. Since wxWidgets fails to implement this, we do it here.
278 static void selectAllInTextCtrls( wxWindowList& children )
279 {
280  for( wxWindow* child : children )
281  {
282  if( auto childTextCtrl = dynamic_cast<wxTextCtrl*>( child ) )
283  {
284  // Respect an existing selection
285  if( childTextCtrl->GetStringSelection().IsEmpty() )
286  childTextCtrl->SelectAll();
287  }
288  else
289  selectAllInTextCtrls( child->GetChildren() );
290  }
291 }
292 
293 
294 void DIALOG_SHIM::OnPaint( wxPaintEvent &event )
295 {
296  if( m_firstPaintEvent )
297  {
299 
300  selectAllInTextCtrls( GetChildren() );
301 
303  m_initialFocusTarget->SetFocus();
304  else
305  SetFocus(); // Focus the dialog itself
306 
307  m_firstPaintEvent = false;
308  }
309 
310  event.Skip();
311 }
312 
313 
314 /*
315  Quasi-Modal Mode Explained:
316 
317  The gtk calls in wxDialog::ShowModal() cause event routing problems if that
318  modal dialog then tries to use KIWAY_PLAYER::ShowModal(). The latter shows up
319  and mostly works but does not respond to the window decoration close button.
320  There is no way to get around this without reversing the gtk calls temporarily.
321 
322  Quasi-Modal mode is our own almost modal mode which disables only the parent
323  of the DIALOG_SHIM, leaving other frames operable and while staying captured in the
324  nested event loop. This avoids the gtk calls and leaves event routing pure
325  and sufficient to operate the KIWAY_PLAYER::ShowModal() properly. When using
326  ShowQuasiModal() you have to use EndQuasiModal() in your dialogs and not
327  EndModal(). There is also IsQuasiModal() but its value can only be true
328  when the nested event loop is active. Do not mix the modal and quasi-modal
329  functions. Use one set or the other.
330 
331  You might find this behavior preferable over a pure modal mode, and it was said
332  that only the Mac has this natively, but now other platforms have something
333  similar. You CAN use it anywhere for any dialog. But you MUST use it when
334  you want to use KIWAY_PLAYER::ShowModal() from a dialog event.
335 */
336 
338 {
339  // This is an exception safe way to zero a pointer before returning.
340  // Yes, even though DismissModal() clears this first normally, this is
341  // here in case there's an exception before the dialog is dismissed.
342  struct NULLER
343  {
344  void*& m_what;
345  NULLER( void*& aPtr ) : m_what( aPtr ) {}
346  ~NULLER() { m_what = 0; } // indeed, set it to NULL on destruction
347  } clear_this( (void*&) m_qmodal_loop );
348 
349  // release the mouse if it's currently captured as the window having it
350  // will be disabled when this dialog is shown -- but will still keep the
351  // capture making it impossible to do anything in the modal dialog itself
352  wxWindow* win = wxWindow::GetCapture();
353  if( win )
354  win->ReleaseMouse();
355 
356  // Get the optimal parent
357  wxWindow* parent = GetParentForModalDialog( GetParent(), GetWindowStyle() );
358 
359  wxASSERT_MSG( !m_qmodal_parent_disabler,
360  wxT( "Caller using ShowQuasiModal() twice on same window?" ) );
361 
362  // quasi-modal: disable only my "optimal" parent
364 
365  // Apple in its infinite wisdom will raise a disabled window before even passing
366  // us the event, so we have no way to stop it. Instead, we must set an order on
367  // the windows so that the quasi-modal will be pushed in front of the disabled
368  // window when it is raised.
370 
371  Show( true );
372 
373  m_qmodal_showing = true;
374 
375  WX_EVENT_LOOP event_loop;
376 
377  m_qmodal_loop = &event_loop;
378 
379  event_loop.Run();
380 
381  m_qmodal_showing = false;
382 
383  return GetReturnCode();
384 }
385 
386 
387 void DIALOG_SHIM::EndQuasiModal( int retCode )
388 {
389  // Hook up validator and transfer data from controls handling so quasi-modal dialogs
390  // handle validation in the same way as other dialogs.
391  if( ( retCode == wxID_OK ) && ( !Validate() || !TransferDataFromWindow() ) )
392  return;
393 
394  SetReturnCode( retCode );
395 
396  if( !IsQuasiModal() )
397  {
398  wxFAIL_MSG( "either DIALOG_SHIM::EndQuasiModal called twice or ShowQuasiModal"
399  "wasn't called" );
400  return;
401  }
402 
403  if( m_qmodal_loop )
404  {
405  if( m_qmodal_loop->IsRunning() )
406  m_qmodal_loop->Exit( 0 );
407  else
408  m_qmodal_loop->ScheduleExit( 0 );
409 
411  }
412 
415 
416  Show( false );
417 }
418 
419 
420 void DIALOG_SHIM::OnCloseWindow( wxCloseEvent& aEvent )
421 {
422  if( IsQuasiModal() )
423  {
424  EndQuasiModal( wxID_CANCEL );
425  return;
426  }
427 
428  // This is mandatory to allow wxDialogBase::OnCloseWindow() to be called.
429  aEvent.Skip();
430 }
431 
432 
433 void DIALOG_SHIM::OnButton( wxCommandEvent& aEvent )
434 {
435  const int id = aEvent.GetId();
436 
437  // If we are pressing a button to exit, we need to enable the escapeID
438  // otherwise the dialog does not process cancel
439  if( id == wxID_CANCEL )
440  SetEscapeId( wxID_ANY );
441 
442  if( IsQuasiModal() )
443  {
444  if( id == GetAffirmativeId() )
445  {
446  EndQuasiModal( id );
447  }
448  else if( id == wxID_APPLY )
449  {
450  // Dialogs that provide Apply buttons should make sure data is valid before
451  // allowing a transfer, as there is no other way to indicate failure
452  // (i.e. the dialog can't refuse to close as it might with OK, because it
453  // isn't closing anyway)
454  if( Validate() )
455  {
456  bool success = TransferDataFromWindow();
457  (void) success;
458  }
459  }
460  else if( id == GetEscapeId() ||
461  (id == wxID_CANCEL && GetEscapeId() == wxID_ANY) )
462  {
463  EndQuasiModal( wxID_CANCEL );
464  }
465  else // not a standard button
466  {
467  aEvent.Skip();
468  }
469 
470  return;
471  }
472 
473  // This is mandatory to allow wxDialogBase::OnButton() to be called.
474  aEvent.Skip();
475 }
476 
477 
478 void DIALOG_SHIM::OnCharHook( wxKeyEvent& aEvt )
479 {
480  if( aEvt.GetKeyCode() == 'U' && aEvt.GetModifiers() == wxMOD_CONTROL )
481  {
482  if( m_parentFrame )
483  {
485  return;
486  }
487  }
488  // shift-return (Mac default) or Ctrl-Return (GTK) for OK
489  else if( aEvt.GetKeyCode() == WXK_RETURN && ( aEvt.ShiftDown() || aEvt.ControlDown() ) )
490  {
491  wxPostEvent( this, wxCommandEvent( wxEVT_COMMAND_BUTTON_CLICKED, wxID_OK ) );
492  return;
493  }
494  else if( aEvt.GetKeyCode() == WXK_TAB && !aEvt.ControlDown() )
495  {
496  wxWindow* currentWindow = wxWindow::FindFocus();
497  int currentIdx = -1;
498  int delta = aEvt.ShiftDown() ? -1 : 1;
499 
500  auto advance = [&]( int& idx )
501  {
502  // Wrap-around modulus
503  int size = m_tabOrder.size();
504  idx = ( ( idx + delta ) % size + size ) % size;
505  };
506 
507  for( size_t i = 0; i < m_tabOrder.size(); ++i )
508  {
509  if( m_tabOrder[i] == currentWindow )
510  {
511  currentIdx = (int) i;
512  break;
513  }
514  }
515 
516  if( currentIdx >= 0 )
517  {
518  advance( currentIdx );
519 
520  //todo: We don't currently have non-textentry dialog boxes but this will break if
521  // we add them.
522 #ifdef __APPLE__
523  while( dynamic_cast<wxTextEntry*>( m_tabOrder[ currentIdx ] ) == nullptr )
524  advance( currentIdx );
525 #endif
526 
527  m_tabOrder[ currentIdx ]->SetFocus();
528  return;
529  }
530  }
531 
532  aEvt.Skip();
533 }
534 
535 
536 void DIALOG_SHIM::OnGridEditorShown( wxGridEvent& event )
537 {
538  SetEscapeId( wxID_NONE );
539  event.Skip();
540 }
541 
542 
543 void DIALOG_SHIM::OnGridEditorHidden( wxGridEvent& event )
544 {
545  SetEscapeId( wxID_ANY );
546  event.Skip();
547 }
bool m_qmodal_showing
Definition: dialog_shim.h:209
KIWAY_HOLDER is a mix in class which holds the location of a wxWindow's KIWAY.
Definition: kiway_holder.h:39
#define WX_EVENT_LOOP
Definition: kiway_player.h:43
KIWAY & Kiway() const
Function Kiway returns a reference to the KIWAY that this object has an opportunity to participate in...
Definition: kiway_holder.h:56
void OnButton(wxCommandEvent &aEvent)
Properly handle the default button events when in the quasimodal mode when not calling EndQuasiModal ...
std::string m_hash_key
Definition: dialog_shim.h:199
std::vector< wxWindow * > m_tabOrder
Definition: dialog_shim.h:214
void ResetSize()
Clear the existing dialog size and position.
void FinishDialogSettings()
In all dialogs, we must call the same functions to fix minimal dlg size, the default position and per...
wxWindow * m_initialFocusTarget
Definition: dialog_shim.h:205
Dialog helper object to sit in the inheritance tree between wxDialog and any class written by wxFormB...
Definition: dialog_shim.h:83
void OnGridEditorShown(wxGridEvent &event)
bool IsContextMenuActive()
True while processing a context menu.
Definition: tool_manager.h:404
KIWAY Kiway & Pgm(), KFCTL_STANDALONE
The global Program "get" accessor.
Definition: single_top.cpp:102
void OnGridEditorHidden(wxGridEvent &event)
bool m_firstPaintEvent
Definition: dialog_shim.h:204
WX_EVENT_LOOP * m_qmodal_loop
Definition: dialog_shim.h:207
TOOL_MANAGER.
Definition: tool_manager.h:51
#define NULL
void OnCloseWindow(wxCloseEvent &aEvent)
Properly handle the wxCloseEvent when in the quasimodal mode when not calling EndQuasiModal which is ...
EDA_BASE_FRAME * m_parentFrame
Definition: dialog_shim.h:212
int ShowQuasiModal()
static RECT_MAP class_map
const wxPoint GetPosition() const
Definition: eda_rect.h:115
void SetSizeInDU(int x, int y)
Set the dialog to the given dimensions in "dialog units".
bool IsQuasiModal()
Definition: dialog_shim.h:124
HOLDER_TYPE GetType()
Definition: kiway_holder.h:49
void OnPaint(wxPaintEvent &event)
bool Show(bool show) override
int VertPixelsFromDU(int y)
Convert an integer number of dialog units to pixels, vertically.
EDA_UNITS
Definition: eda_units.h:38
WDO_ENABLE_DISABLE * m_qmodal_parent_disabler
Definition: dialog_shim.h:210
virtual void OnCharHook(wxKeyEvent &aEvt)
void EndQuasiModal(int retCode)
void SetSize(const wxSize &size)
Definition: eda_rect.h:144
virtual void ToggleUserUnits()
see class PGM_BASE
const char * name
Definition: DXF_plotter.cpp:59
void FixupCancelButtonCmdKeyCollision(wxWindow *aWindow)
Definition: gtk/ui.cpp:56
The base frame for deriving all KiCad main window classes.
void VetoContextMenuMouseWarp()
Disables mouse warping after the current context menu is closed.
Definition: tool_manager.h:414
EDA_RECT handles the component boundary box.
Definition: eda_rect.h:44
Toggle a window's "enable" status to disabled, then enabled on destruction.
Definition: dialog_shim.cpp:42
int HorizPixelsFromDU(int x)
Convert an integer number of dialog units to pixels, horizontally.
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:50
WDO_ENABLE_DISABLE(wxWindow *aWindow)
Definition: dialog_shim.cpp:48
std::unordered_map< std::string, EDA_RECT > RECT_MAP
Map a C string to an EDA_RECT.
Definition: hashtables.h:136
const wxSize GetSize() const
Definition: eda_rect.h:103
static void selectAllInTextCtrls(wxWindowList &children)