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