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-2018 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 <eda_rect.h>
30 #include <wx/display.h>
31 #include <wx/grid.h>
32 
35 {
36  wxWindow* m_win;
37 
38 public:
39 
40  WDO_ENABLE_DISABLE( wxWindow* aWindow ) :
41  m_win( aWindow )
42  {
43  if( m_win )
44  m_win->Disable();
45  }
46 
48  {
49  if( m_win )
50  {
51  m_win->Enable();
52  m_win->SetFocus(); // let's focus back on the parent window
53  }
54  }
55 };
56 
57 
58 BEGIN_EVENT_TABLE( DIALOG_SHIM, wxDialog )
59  // If dialog has a grid and the grid has an active cell editor
60  // Esc key closes cell editor, otherwise Esc key closes the dialog.
61  EVT_GRID_EDITOR_SHOWN( DIALOG_SHIM::OnGridEditorShown )
62  EVT_GRID_EDITOR_HIDDEN( DIALOG_SHIM::OnGridEditorHidden )
63 END_EVENT_TABLE()
64 
65 
66 DIALOG_SHIM::DIALOG_SHIM( wxWindow* aParent, wxWindowID id, const wxString& title,
67  const wxPoint& pos, const wxSize& size, long style, const wxString& name ) :
68  wxDialog( aParent, id, title, pos, size, style, name ),
69  KIWAY_HOLDER( nullptr ),
70  m_units( MILLIMETRES ),
71  m_firstPaintEvent( true ),
72  m_initialFocusTarget( nullptr ),
73  m_qmodal_loop( nullptr ),
74  m_qmodal_showing( false ),
75  m_qmodal_parent_disabler( nullptr )
76 {
77  KIWAY_HOLDER* kiwayHolder = nullptr;
78 
79  if( aParent )
80  {
81  kiwayHolder = dynamic_cast<KIWAY_HOLDER*>( aParent );
82 
83  while( !kiwayHolder && aParent->GetParent() )
84  {
85  aParent = aParent->GetParent();
86  kiwayHolder = dynamic_cast<KIWAY_HOLDER*>( aParent );
87  }
88  }
89 
90  if( kiwayHolder )
91  {
92  // Inherit units from parent
93  m_units = kiwayHolder->GetUserUnits();
94 
95  // Set up the message bus
96  SetKiway( this, &kiwayHolder->Kiway() );
97  }
98 
99  Bind( wxEVT_CLOSE_WINDOW, &DIALOG_SHIM::OnCloseWindow, this );
100  Bind( wxEVT_BUTTON, &DIALOG_SHIM::OnButton, this );
101 
102 #ifdef __WINDOWS__
103  // On Windows, the app top windows can be brought to the foreground
104  // (at least temporary) in certain circumstances,
105  // for instance when calling an external tool in Eeschema BOM generation.
106  // So set the parent KIWAY_PLAYER kicad frame (if exists) to top window
107  // to avoid this annoying behavior
108  KIWAY_PLAYER* parent_kiwayplayer = dynamic_cast<KIWAY_PLAYER*>( aParent );
109 
110  if( parent_kiwayplayer )
111  Pgm().App().SetTopWindow( parent_kiwayplayer );
112 #endif
113 
114  Connect( wxEVT_PAINT, wxPaintEventHandler( DIALOG_SHIM::OnPaint ) );
115 }
116 
117 
119 {
120  // if the dialog is quasi-modal, this will end its event loop
121  if( IsQuasiModal() )
122  EndQuasiModal( wxID_CANCEL );
123 
125  delete m_qmodal_parent_disabler; // usually NULL by now
126 }
127 
128 
130 {
131  // must be called from the constructor of derived classes,
132  // when all widgets are initialized, and therefore their size fixed
133 
134  // SetSizeHints fixes the minimal size of sizers in the dialog
135  // (SetSizeHints calls Fit(), so no need to call it)
136  GetSizer()->SetSizeHints( this );
137 
138  // the default position, when calling the first time the dlg
139  Center();
140 }
141 
142 
143 void DIALOG_SHIM::SetSizeInDU( int x, int y )
144 {
145  wxSize sz( x, y );
146  SetSize( ConvertDialogToPixels( sz ) );
147 }
148 
149 
151 {
152  wxSize sz( x, 0 );
153  return ConvertDialogToPixels( sz ).x;
154 }
155 
156 
158 {
159  wxSize sz( 0, y );
160  return ConvertDialogToPixels( sz ).y;
161 }
162 
163 
164 // our hashtable is an implementation secret, don't need or want it in a header file
165 #include <hashtables.h>
166 #include <base_struct.h> // EDA_RECT
167 #include <typeinfo>
168 
170 
171 bool DIALOG_SHIM::Show( bool show )
172 {
173  bool ret;
174  const char* hash_key;
175 
176  if( m_hash_key.size() )
177  {
178  // a special case like EDA_LIST_DIALOG, which has multiple uses.
179  hash_key = m_hash_key.c_str();
180  }
181  else
182  {
183  hash_key = typeid(*this).name();
184  }
185 
186  // Show or hide the window. If hiding, save current position and size.
187  // If showing, use previous position and size.
188  if( show )
189  {
190 #ifndef __WINDOWS__
191  wxDialog::Raise(); // Needed on OS X and some other window managers (i.e. Unity)
192 #endif
193  ret = wxDialog::Show( show );
194 
195  // classname is key, returns a zeroed out default EDA_RECT if none existed before.
196  EDA_RECT savedDialogRect = class_map[ hash_key ];
197 
198  if( savedDialogRect.GetSize().x != 0 && savedDialogRect.GetSize().y != 0 )
199  {
200  SetSize( savedDialogRect.GetPosition().x,
201  savedDialogRect.GetPosition().y,
202  std::max( wxDialog::GetSize().x, savedDialogRect.GetSize().x ),
203  std::max( wxDialog::GetSize().y, savedDialogRect.GetSize().y ),
204  0 );
205  }
206 
207  // Be sure that the dialog appears in a visible area
208  // (the dialog position might have been stored at the time when it was
209  // shown on another display)
210  if( wxDisplay::GetFromWindow( this ) == wxNOT_FOUND )
211  Centre();
212  }
213  else
214  {
215  // Save the dialog's position & size before hiding, using classname as key
216  class_map[ hash_key ] = EDA_RECT( wxDialog::GetPosition(), wxDialog::GetSize() );
217 
218 #ifdef __WXMAC__
219  if ( m_eventLoop )
220  m_eventLoop->Exit( GetReturnCode() ); // Needed for APP-MODAL dlgs on OSX
221 #endif
222 
223  ret = wxDialog::Show( show );
224  }
225 
226  return ret;
227 }
228 
229 
230 bool DIALOG_SHIM::Enable( bool enable )
231 {
232  // so we can do logging of this state change:
233 
234 #if defined(DEBUG)
235  const char* type_id = typeid( *this ).name();
236  printf( "wxDialog %s: %s\n", type_id, enable ? "enabled" : "disabled" );
237  fflush(0); //Needed on msys2 to immediately print the message
238 #endif
239 
240  return wxDialog::Enable( enable );
241 }
242 
243 
244 #ifdef __WXMAC__
245 // Recursive descent doing a SelectAll() in wxTextCtrls.
246 // MacOS User Interface Guidelines state that when tabbing to a text control all its
247 // text should be selected. Since wxWidgets fails to implement this, we do it here.
248 static void selectAllInTextCtrls( wxWindowList& children )
249 {
250  for( wxWindow* child : children )
251  {
252  wxTextCtrl* childTextCtrl = dynamic_cast<wxTextCtrl*>( child );
253  if( childTextCtrl )
254  {
255  wxTextEntry* asTextEntry = dynamic_cast<wxTextEntry*>( childTextCtrl );
256 
257  // Respect an existing selection
258  if( asTextEntry->GetStringSelection().IsEmpty() )
259  asTextEntry->SelectAll();
260  }
261  else
262  selectAllInTextCtrls( child->GetChildren() );
263  }
264 }
265 #endif
266 
267 
268 #ifdef __WXMAC__
269 static void fixOSXCancelButtonIssue( wxWindow *aWindow )
270 {
271  // A ugly hack to fix an issue on OSX: cmd+c closes the dialog instead of
272  // copying the text if a button with wxID_CANCEL is used in a
273  // wxStdDialogButtonSizer created by wxFormBuilder: the label is &Cancel, and
274  // this accelerator key has priority over the standard copy accelerator.
275  // Note: problem also exists in other languages; for instance cmd+a closes
276  // dialogs in German because the button is &Abbrechen.
277  wxButton* button = dynamic_cast<wxButton*>( wxWindow::FindWindowById( wxID_CANCEL, aWindow ) );
278 
279  if( button )
280  {
281  static const wxString placeholder = wxT( "{amp}" );
282 
283  wxString buttonLabel = button->GetLabel();
284  buttonLabel.Replace( wxT( "&&" ), placeholder );
285  buttonLabel.Replace( wxT( "&" ), wxEmptyString );
286  buttonLabel.Replace( placeholder, wxT( "&" ) );
287  button->SetLabel( buttonLabel );
288  }
289 }
290 #endif
291 
292 
293 void DIALOG_SHIM::OnPaint( wxPaintEvent &event )
294 {
295  if( m_firstPaintEvent )
296  {
297 #ifdef __WXMAC__
298  fixOSXCancelButtonIssue( this );
299  selectAllInTextCtrls( GetChildren() );
300 #endif
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  // Show the optimal parent
360  DBG( if( parent ) printf( "%s: optimal parent: %s\n", __func__, typeid(*parent).name() );)
361 
362  wxASSERT_MSG( !m_qmodal_parent_disabler,
363  wxT( "Caller using ShowQuasiModal() twice on same window?" ) );
364 
365  // quasi-modal: disable only my "optimal" parent
367 
368 #ifdef __WXMAC__
369  // Apple in its infinite wisdom will raise a disabled window before even passing
370  // us the event, so we have no way to stop it. Instead, we must set an order on
371  // the windows so that the quasi-modal will be pushed in front of the disabled
372  // window when it is raised.
373  ReparentQuasiModal();
374 #endif
375  Show( true );
376 
377  m_qmodal_showing = true;
378 
379  WX_EVENT_LOOP event_loop;
380 
381  m_qmodal_loop = &event_loop;
382 
383  event_loop.Run();
384 
385  m_qmodal_showing = false;
386 
387  return GetReturnCode();
388 }
389 
390 
391 void DIALOG_SHIM::EndQuasiModal( int retCode )
392 {
393  // Hook up validator and transfer data from controls handling so quasi-modal dialogs
394  // handle validation in the same way as other dialogs.
395  if( ( retCode == wxID_OK ) && ( !Validate() || !TransferDataFromWindow() ) )
396  return;
397 
398  SetReturnCode( retCode );
399 
400  if( !IsQuasiModal() )
401  {
402  wxFAIL_MSG( wxT( "either DIALOG_SHIM::EndQuasiModal called twice or ShowQuasiModal wasn't called" ) );
403  return;
404  }
405 
406  if( m_qmodal_loop )
407  {
408  if( m_qmodal_loop->IsRunning() )
409  m_qmodal_loop->Exit( 0 );
410  else
411  m_qmodal_loop->ScheduleExit( 0 );
412 
413  m_qmodal_loop = NULL;
414  }
415 
418 
419  Show( false );
420 }
421 
422 
423 void DIALOG_SHIM::OnCloseWindow( wxCloseEvent& aEvent )
424 {
425  if( IsQuasiModal() )
426  {
427  EndQuasiModal( wxID_CANCEL );
428  return;
429  }
430 
431  // This is mandatory to allow wxDialogBase::OnCloseWindow() to be called.
432  aEvent.Skip();
433 }
434 
435 
436 void DIALOG_SHIM::OnButton( wxCommandEvent& aEvent )
437 {
438  const int id = aEvent.GetId();
439 
440  // If we are pressing a button to exit, we need to enable the escapeID
441  // otherwise the dialog does not process cancel
442  if( id == wxID_CANCEL )
443  SetEscapeId( wxID_ANY );
444 
445  if( IsQuasiModal() )
446  {
447  if( id == GetAffirmativeId() )
448  {
449  EndQuasiModal( id );
450  }
451  else if( id == wxID_APPLY )
452  {
453  // Dialogs that provide Apply buttons should make sure data is valid before
454  // allowing a transfer, as there is no other way to indicate failure
455  // (i.e. the dialog can't refuse to close as it might with OK, because it
456  // isn't closing anyway)
457  if( Validate() )
458  {
459  bool success = TransferDataFromWindow();
460  (void) success;
461  }
462  }
463  else if( id == GetEscapeId() ||
464  (id == wxID_CANCEL && GetEscapeId() == wxID_ANY) )
465  {
466  EndQuasiModal( wxID_CANCEL );
467  }
468  else // not a standard button
469  {
470  aEvent.Skip();
471  }
472 
473  return;
474  }
475 
476  // This is mandatory to allow wxDialogBase::OnButton() to be called.
477  aEvent.Skip();
478 }
479 
480 
481 void DIALOG_SHIM::OnGridEditorShown( wxGridEvent& event )
482 {
483  SetEscapeId( wxID_NONE );
484  event.Skip();
485 }
486 
487 
488 void DIALOG_SHIM::OnGridEditorHidden( wxGridEvent& event )
489 {
490  SetEscapeId( wxID_ANY );
491  event.Skip();
492 }
bool m_qmodal_showing
Definition: dialog_shim.h:184
Class KIWAY_HOLDER is a mix in class which holds the location of a wxWindow's KIWAY.
Definition: kiway_player.h:48
Class KIWAY_PLAYER is a wxFrame capable of the OpenProjectFiles function, meaning it can load a porti...
Definition: kiway_player.h:120
#define WX_EVENT_LOOP
Definition: kiway_player.h:100
KIWAY & Kiway() const
Function Kiway returns a reference to the KIWAY that this object has an opportunity to participate in...
Definition: kiway_player.h:60
void OnButton(wxCommandEvent &aEvent)
Function OnCloseWindow.
std::string m_hash_key
Definition: dialog_shim.h:174
void FinishDialogSettings()
In all dialogs, we must call the same functions to fix minimal dlg size, the default position and per...
VTBL_ENTRY wxApp & App()
Function App returns a bare naked wxApp, which may come from wxPython, SINGLE_TOP,...
Definition: pgm_base.cpp:185
PGM_BASE & Pgm()
The global Program "get" accessor.
Definition: kicad.cpp:66
wxWindow * m_initialFocusTarget
Definition: dialog_shim.h:180
Class DIALOG_SHIM may sit in the inheritance tree between wxDialog and any class written by wxFormBui...
Definition: dialog_shim.h:83
void OnGridEditorShown(wxGridEvent &event)
void OnGridEditorHidden(wxGridEvent &event)
bool m_firstPaintEvent
Definition: dialog_shim.h:179
WX_EVENT_LOOP * m_qmodal_loop
Definition: dialog_shim.h:183
void OnCloseWindow(wxCloseEvent &aEvent)
Function OnCloseWindow.
int ShowQuasiModal()
static RECT_MAP class_map
const wxPoint GetPosition() const
Definition: eda_rect.h:113
void SetSizeInDU(int x, int y)
Set the dialog to the given dimensions in "dialog units".
bool IsQuasiModal()
Definition: dialog_shim.h:125
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:185
void EndQuasiModal(int retCode)
see class PGM_BASE
const char * name
Definition: DXF_plotter.cpp:61
#define max(a, b)
Definition: auxiliary.h:86
Class 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:34
VTBL_ENTRY EDA_UNITS_T GetUserUnits() const
Function GetUserUnits Allows participation in KEYWAY_PLAYER/DIALOG_SHIM userUnits inheritance.
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
Basic classes for most KiCad items.
WDO_ENABLE_DISABLE(wxWindow *aWindow)
Definition: dialog_shim.cpp:40
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:101