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