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 <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 END_EVENT_TABLE()
65 
66 
67 DIALOG_SHIM::DIALOG_SHIM( wxWindow* aParent, wxWindowID id, const wxString& title,
68  const wxPoint& pos, const wxSize& size, long style, const wxString& name ) :
69  wxDialog( aParent, id, title, pos, size, style, name ),
70  KIWAY_HOLDER( nullptr ),
71  m_units( MILLIMETRES ),
72  m_firstPaintEvent( true ),
73  m_initialFocusTarget( nullptr ),
74  m_qmodal_loop( nullptr ),
75  m_qmodal_showing( false ),
76  m_qmodal_parent_disabler( nullptr )
77 {
78  KIWAY_HOLDER* kiwayHolder = nullptr;
79 
80  if( aParent )
81  {
82  kiwayHolder = dynamic_cast<KIWAY_HOLDER*>( aParent );
83 
84  while( !kiwayHolder && aParent->GetParent() )
85  {
86  aParent = aParent->GetParent();
87  kiwayHolder = dynamic_cast<KIWAY_HOLDER*>( aParent );
88  }
89  }
90 
91  if( kiwayHolder )
92  {
93  // Inherit units from parent
94  m_units = kiwayHolder->GetUserUnits();
95 
96  // Don't mouse-warp after a dialog run from the context menu
97  TOOL_MANAGER* toolMgr = kiwayHolder->GetToolManager();
98 
99  if( toolMgr )
100  toolMgr->VetoContextMenuMouseWarp();
101 
102  // Set up the message bus
103  SetKiway( this, &kiwayHolder->Kiway() );
104  }
105 
106  Bind( wxEVT_CLOSE_WINDOW, &DIALOG_SHIM::OnCloseWindow, this );
107  Bind( wxEVT_BUTTON, &DIALOG_SHIM::OnButton, this );
108 
109 #ifdef __WINDOWS__
110  // On Windows, the app top windows can be brought to the foreground
111  // (at least temporary) in certain circumstances,
112  // for instance when calling an external tool in Eeschema BOM generation.
113  // So set the parent KIWAY_PLAYER kicad frame (if exists) to top window
114  // to avoid this annoying behavior
115  KIWAY_PLAYER* parent_kiwayplayer = dynamic_cast<KIWAY_PLAYER*>( aParent );
116 
117  if( parent_kiwayplayer )
118  Pgm().App().SetTopWindow( parent_kiwayplayer );
119 #endif
120 
121  Connect( wxEVT_PAINT, wxPaintEventHandler( DIALOG_SHIM::OnPaint ) );
122 }
123 
124 
126 {
127  // if the dialog is quasi-modal, this will end its event loop
128  if( IsQuasiModal() )
129  EndQuasiModal( wxID_CANCEL );
130 
132  delete m_qmodal_parent_disabler; // usually NULL by now
133 }
134 
135 
137 {
138  // must be called from the constructor of derived classes,
139  // when all widgets are initialized, and therefore their size fixed
140 
141  // SetSizeHints fixes the minimal size of sizers in the dialog
142  // (SetSizeHints calls Fit(), so no need to call it)
143  GetSizer()->SetSizeHints( this );
144 
145  // the default position, when calling the first time the dlg
146  Center();
147 }
148 
149 
150 void DIALOG_SHIM::SetSizeInDU( int x, int y )
151 {
152  wxSize sz( x, y );
153  SetSize( ConvertDialogToPixels( sz ) );
154 }
155 
156 
158 {
159  wxSize sz( x, 0 );
160  return ConvertDialogToPixels( sz ).x;
161 }
162 
163 
165 {
166  wxSize sz( 0, y );
167  return ConvertDialogToPixels( sz ).y;
168 }
169 
170 
171 // our hashtable is an implementation secret, don't need or want it in a header file
172 #include <hashtables.h>
173 #include <base_struct.h> // EDA_RECT
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 
237 bool DIALOG_SHIM::Enable( bool enable )
238 {
239  // so we can do logging of this state change:
240 
241 #if 0 && defined(DEBUG)
242  const char* type_id = typeid( *this ).name();
243  printf( "DIALOG_SHIM %s: %s\n", type_id, enable ? "enabled" : "disabled" );
244  fflush(0); //Needed on msys2 to immediately print the message
245 #endif
246 
247  return wxDialog::Enable( enable );
248 }
249 
250 
251 // Recursive descent doing a SelectAll() in wxTextCtrls.
252 // MacOS User Interface Guidelines state that when tabbing to a text control all its
253 // text should be selected. Since wxWidgets fails to implement this, we do it here.
254 static void selectAllInTextCtrls( wxWindowList& children )
255 {
256  for( wxWindow* child : children )
257  {
258  wxTextCtrl* childTextCtrl = dynamic_cast<wxTextCtrl*>( child );
259  if( childTextCtrl )
260  {
261  wxTextEntry* asTextEntry = dynamic_cast<wxTextEntry*>( childTextCtrl );
262 
263  // Respect an existing selection
264  if( asTextEntry->GetStringSelection().IsEmpty() )
265  asTextEntry->SelectAll();
266  }
267  else
268  selectAllInTextCtrls( child->GetChildren() );
269  }
270 }
271 
272 
273 #ifdef __WXMAC__
274 static void fixOSXCancelButtonIssue( wxWindow *aWindow )
275 {
276  // A ugly hack to fix an issue on OSX: cmd+c closes the dialog instead of
277  // copying the text if a button with wxID_CANCEL is used in a
278  // wxStdDialogButtonSizer created by wxFormBuilder: the label is &Cancel, and
279  // this accelerator key has priority over the standard copy accelerator.
280  // Note: problem also exists in other languages; for instance cmd+a closes
281  // dialogs in German because the button is &Abbrechen.
282  wxButton* button = dynamic_cast<wxButton*>( wxWindow::FindWindowById( wxID_CANCEL, aWindow ) );
283 
284  if( button )
285  {
286  static const wxString placeholder = wxT( "{amp}" );
287 
288  wxString buttonLabel = button->GetLabel();
289  buttonLabel.Replace( wxT( "&&" ), placeholder );
290  buttonLabel.Replace( wxT( "&" ), wxEmptyString );
291  buttonLabel.Replace( placeholder, wxT( "&" ) );
292  button->SetLabel( buttonLabel );
293  }
294 }
295 #endif
296 
297 
298 void DIALOG_SHIM::OnPaint( wxPaintEvent &event )
299 {
300  if( m_firstPaintEvent )
301  {
302 #ifdef __WXMAC__
303  fixOSXCancelButtonIssue( this );
304 #endif
305 
306  selectAllInTextCtrls( GetChildren() );
307 
309  m_initialFocusTarget->SetFocus();
310  else
311  SetFocus(); // Focus the dialog itself
312 
313  m_firstPaintEvent = false;
314  }
315 
316  event.Skip();
317 }
318 
319 
320 /*
321  Quasi-Modal Mode Explained:
322 
323  The gtk calls in wxDialog::ShowModal() cause event routing problems if that
324  modal dialog then tries to use KIWAY_PLAYER::ShowModal(). The latter shows up
325  and mostly works but does not respond to the window decoration close button.
326  There is no way to get around this without reversing the gtk calls temporarily.
327 
328  Quasi-Modal mode is our own almost modal mode which disables only the parent
329  of the DIALOG_SHIM, leaving other frames operable and while staying captured in the
330  nested event loop. This avoids the gtk calls and leaves event routing pure
331  and sufficient to operate the KIWAY_PLAYER::ShowModal() properly. When using
332  ShowQuasiModal() you have to use EndQuasiModal() in your dialogs and not
333  EndModal(). There is also IsQuasiModal() but its value can only be true
334  when the nested event loop is active. Do not mix the modal and quasi-modal
335  functions. Use one set or the other.
336 
337  You might find this behavior preferable over a pure modal mode, and it was said
338  that only the Mac has this natively, but now other platforms have something
339  similar. You CAN use it anywhere for any dialog. But you MUST use it when
340  you want to use KIWAY_PLAYER::ShowModal() from a dialog event.
341 */
342 
344 {
345  // This is an exception safe way to zero a pointer before returning.
346  // Yes, even though DismissModal() clears this first normally, this is
347  // here in case there's an exception before the dialog is dismissed.
348  struct NULLER
349  {
350  void*& m_what;
351  NULLER( void*& aPtr ) : m_what( aPtr ) {}
352  ~NULLER() { m_what = 0; } // indeed, set it to NULL on destruction
353  } clear_this( (void*&) m_qmodal_loop );
354 
355  // release the mouse if it's currently captured as the window having it
356  // will be disabled when this dialog is shown -- but will still keep the
357  // capture making it impossible to do anything in the modal dialog itself
358  wxWindow* win = wxWindow::GetCapture();
359  if( win )
360  win->ReleaseMouse();
361 
362  // Get the optimal parent
363  wxWindow* parent = GetParentForModalDialog( GetParent(), GetWindowStyle() );
364 
365  // Show the optimal parent
366  DBG( if( parent ) printf( "%s: optimal parent: %s\n", __func__, typeid(*parent).name() );)
367 
368  wxASSERT_MSG( !m_qmodal_parent_disabler,
369  wxT( "Caller using ShowQuasiModal() twice on same window?" ) );
370 
371  // quasi-modal: disable only my "optimal" parent
373 
374 #ifdef __WXMAC__
375  // Apple in its infinite wisdom will raise a disabled window before even passing
376  // us the event, so we have no way to stop it. Instead, we must set an order on
377  // the windows so that the quasi-modal will be pushed in front of the disabled
378  // window when it is raised.
379  ReparentQuasiModal();
380 #endif
381  Show( true );
382 
383  m_qmodal_showing = true;
384 
385  WX_EVENT_LOOP event_loop;
386 
387  m_qmodal_loop = &event_loop;
388 
389  event_loop.Run();
390 
391  m_qmodal_showing = false;
392 
393  return GetReturnCode();
394 }
395 
396 
397 void DIALOG_SHIM::EndQuasiModal( int retCode )
398 {
399  // Hook up validator and transfer data from controls handling so quasi-modal dialogs
400  // handle validation in the same way as other dialogs.
401  if( ( retCode == wxID_OK ) && ( !Validate() || !TransferDataFromWindow() ) )
402  return;
403 
404  SetReturnCode( retCode );
405 
406  if( !IsQuasiModal() )
407  {
408  wxFAIL_MSG( wxT( "either DIALOG_SHIM::EndQuasiModal called twice or ShowQuasiModal wasn't called" ) );
409  return;
410  }
411 
412  if( m_qmodal_loop )
413  {
414  if( m_qmodal_loop->IsRunning() )
415  m_qmodal_loop->Exit( 0 );
416  else
417  m_qmodal_loop->ScheduleExit( 0 );
418 
419  m_qmodal_loop = NULL;
420  }
421 
424 
425  Show( false );
426 }
427 
428 
429 void DIALOG_SHIM::OnCloseWindow( wxCloseEvent& aEvent )
430 {
431  if( IsQuasiModal() )
432  {
433  EndQuasiModal( wxID_CANCEL );
434  return;
435  }
436 
437  // This is mandatory to allow wxDialogBase::OnCloseWindow() to be called.
438  aEvent.Skip();
439 }
440 
441 
442 void DIALOG_SHIM::OnButton( wxCommandEvent& aEvent )
443 {
444  const int id = aEvent.GetId();
445 
446  // If we are pressing a button to exit, we need to enable the escapeID
447  // otherwise the dialog does not process cancel
448  if( id == wxID_CANCEL )
449  SetEscapeId( wxID_ANY );
450 
451  if( IsQuasiModal() )
452  {
453  if( id == GetAffirmativeId() )
454  {
455  EndQuasiModal( id );
456  }
457  else if( id == wxID_APPLY )
458  {
459  // Dialogs that provide Apply buttons should make sure data is valid before
460  // allowing a transfer, as there is no other way to indicate failure
461  // (i.e. the dialog can't refuse to close as it might with OK, because it
462  // isn't closing anyway)
463  if( Validate() )
464  {
465  bool success = TransferDataFromWindow();
466  (void) success;
467  }
468  }
469  else if( id == GetEscapeId() ||
470  (id == wxID_CANCEL && GetEscapeId() == wxID_ANY) )
471  {
472  EndQuasiModal( wxID_CANCEL );
473  }
474  else // not a standard button
475  {
476  aEvent.Skip();
477  }
478 
479  return;
480  }
481 
482  // This is mandatory to allow wxDialogBase::OnButton() to be called.
483  aEvent.Skip();
484 }
485 
486 
487 void DIALOG_SHIM::OnGridEditorShown( wxGridEvent& event )
488 {
489  SetEscapeId( wxID_NONE );
490  event.Skip();
491 }
492 
493 
494 void DIALOG_SHIM::OnGridEditorHidden( wxGridEvent& event )
495 {
496  SetEscapeId( wxID_ANY );
497  event.Skip();
498 }
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:49
Class KIWAY_PLAYER is a wxFrame capable of the OpenProjectFiles function, meaning it can load a porti...
Definition: kiway_player.h:127
#define WX_EVENT_LOOP
Definition: kiway_player.h:107
KIWAY & Kiway() const
Function Kiway returns a reference to the KIWAY that this object has an opportunity to participate in...
Definition: kiway_player.h:61
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
Class TOOL_MANAGER.
Definition: tool_manager.h:49
VTBL_ENTRY TOOL_MANAGER * GetToolManager() const
Function GetToolManager Return the tool manager instance, if any.
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
void VetoContextMenuMouseWarp()
Disables mouse warping after the current context menu is closed.
Definition: tool_manager.h:385
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:35
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: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:101
static void selectAllInTextCtrls(wxWindowList &children)