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