KiCad PCB EDA Suite
Loading...
Searching...
No Matches
editor_tabs_panel.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 The KiCad Developers, see AUTHORS.txt for contributors.
5 *
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 2
9 * of the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18 */
19
21
22#include <algorithm>
23
24#include <wx/aui/auibook.h>
25#include <wx/aui/framemanager.h>
26#include <wx/event.h>
27#include <wx/intl.h>
28#include <wx/menu.h>
29#include <wx/sizer.h>
30
32
33
34// Offset into a private range so these ids never collide with the host frame.
35enum
36{
37 ID_TAB_CLOSE = wxID_HIGHEST + 1,
41};
42
43
44int EDITOR_TABS_MODEL::FindIndex( const wxString& aKey ) const
45{
46 for( size_t i = 0; i < m_entries.size(); ++i )
47 {
48 if( m_entries[i].key == aKey )
49 return static_cast<int>( i );
50 }
51
52 return -1;
53}
54
55
57{
58 for( size_t i = 0; i < m_entries.size(); ++i )
59 {
60 const ENTRY& e = m_entries[i];
61
62 if( e.preview && !e.modified )
63 return static_cast<int>( i );
64 }
65
66 return -1;
67}
68
69
70int EDITOR_TABS_MODEL::OpenDocument( const wxString& aKey, bool aAsPreview )
71{
72 const int existing = FindIndex( aKey );
73
74 if( existing >= 0 )
75 {
76 if( !aAsPreview )
77 m_entries[existing].preview = false;
78
79 return existing;
80 }
81
82 if( aAsPreview )
83 {
84 const int reuse = PreviewIndex();
85
86 if( reuse >= 0 )
87 {
88 m_entries[reuse].key = aKey;
89 m_entries[reuse].preview = true;
90 m_entries[reuse].modified = false;
91 return reuse;
92 }
93 }
94
95 m_entries.push_back( ENTRY{ aKey, aAsPreview, false } );
96
97 return static_cast<int>( m_entries.size() ) - 1;
98}
99
100
101void EDITOR_TABS_MODEL::CloseDocument( const wxString& aKey )
102{
103 const int idx = FindIndex( aKey );
104
105 if( idx >= 0 )
106 m_entries.erase( m_entries.begin() + idx );
107}
108
109
110void EDITOR_TABS_MODEL::MarkModified( const wxString& aKey, bool aModified )
111{
112 const int idx = FindIndex( aKey );
113
114 if( idx < 0 )
115 return;
116
117 m_entries[idx].modified = aModified;
118
119 // Promotion sticks even after the dirty flag later clears.
120 if( aModified )
121 m_entries[idx].preview = false;
122}
123
124
125void EDITOR_TABS_MODEL::Promote( const wxString& aKey )
126{
127 const int idx = FindIndex( aKey );
128
129 if( idx >= 0 )
130 m_entries[idx].preview = false;
131}
132
133
134bool EDITOR_TABS_MODEL::CanCloseWithoutPrompt( const wxString& aKey ) const
135{
136 const int idx = FindIndex( aKey );
137
138 if( idx < 0 )
139 return true;
140
141 return !m_entries[idx].modified;
142}
143
144
145EDITOR_TABS_PANEL::EDITOR_TABS_PANEL( wxWindow* aParent, EDA_DRAW_PANEL_GAL* aSharedCanvas ) :
146 wxPanel( aParent ),
147 m_sharedCanvas( aSharedCanvas )
148{
149 // wxWidgets 3.3 made wxAuiTabCtrl an internal class that only works as a child of a notebook, so
150 // the strip is a real wxAuiNotebook. Its (empty) page area is collapsed by updateTabStripHeight and
151 // the host-owned canvas sits below it as a sibling; documents are tracked by hidden key windows.
152 //
153 // No wxAUI_NB_TAB_MOVE: the model, m_pageWindows and the host context vectors are index-aligned
154 // with the notebook page order, and there is no reorder handler, so user drag-reordering would
155 // desync them and target the wrong document.
156 m_tabs = new wxAuiNotebook( this, wxID_ANY, wxDefaultPosition, wxDefaultSize,
157 wxAUI_NB_CLOSE_ON_ALL_TABS | wxAUI_NB_SCROLL_BUTTONS );
158
159 m_tabs->SetArtProvider( new KICAD_TAB_ART( [this]( wxWindow* aPageWindow ) -> TAB_VISUAL_STATE
160 {
161 return visualStateForIndex(
162 indexOfWindow( aPageWindow ) );
163 } ) );
164
165 // Floor the height before first layout so a zero-height paint never trips GTK's invalid bitmap
166 // size assert. updateTabStripHeight refines it to the measured tab height once pages exist.
167 m_tabs->SetMinSize( wxSize( -1, FromDIP( 28 ) ) );
168
169 m_sizer = new wxBoxSizer( wxVERTICAL );
170 m_sizer->Add( m_tabs, 0, wxEXPAND );
171
172 if( m_sharedCanvas )
173 {
174 // Canvas is host-owned so remember its parent to hand it back and avoid ~wxPanel freeing it.
175 // It is reparented in once and never again so a tab switch never disturbs the GL context.
177 m_sharedCanvas->Reparent( this );
178 m_sizer->Add( m_sharedCanvas, 1, wxEXPAND );
179 }
180
181 SetSizer( m_sizer );
182
183 m_tabs->Bind( wxEVT_AUINOTEBOOK_PAGE_CHANGED, &EDITOR_TABS_PANEL::onPageChanged, this );
184 m_tabs->Bind( wxEVT_AUINOTEBOOK_PAGE_CLOSE, &EDITOR_TABS_PANEL::onPageClose, this );
185 m_tabs->Bind( wxEVT_AUINOTEBOOK_TAB_RIGHT_DOWN, &EDITOR_TABS_PANEL::onTabRightDown, this );
186
187 // The notebook has no per-tab double-click event, so read the raw double-click on the tab strip
188 // itself and hit-test it back to a page for preview promotion. updateTabStripHeight() keeps this
189 // binding attached to the live tab-strip control as pages come and go.
191}
192
193
195{
196 // Hand the host-owned canvas back so the frame frees it exactly once. Idempotent once null.
197 if( !m_sharedCanvas )
198 return;
199
200 // Drop the sizer's reference first so no stale wxSizerItem lingers after the reparent.
201 if( m_sizer )
202 m_sizer->Detach( m_sharedCanvas );
203
205 m_sharedCanvas = nullptr;
206}
207
208
210{
211 // Hand the canvas back before child teardown so wx never frees it. Fallback for when no host did.
213
214 // The notebook owns the per-tab key windows and frees them with itself, so just drop our handles.
215 m_pageWindows.clear();
216}
217
218
219int EDITOR_TABS_PANEL::indexOfWindow( wxWindow* aWindow ) const
220{
221 for( size_t i = 0; i < m_pageWindows.size(); ++i )
222 {
223 if( m_pageWindows[i] == aWindow )
224 return static_cast<int>( i );
225 }
226
227 return -1;
228}
229
230
232{
233 // wxAuiNotebook owns the tab-strip control and can destroy and recreate it (wx 3.2 frees the tab
234 // frame when the last page closes and rebuilds it on the next add), so bind to whatever control is
235 // live now. Unbind first so re-binding the same control never stacks a second handler; a control
236 // that was never bound makes the Unbind a harmless no-op, and a destroyed control already dropped
237 // its binding with itself, so no stale pointer is ever dereferenced.
238 wxAuiTabCtrl* tabCtrl = m_tabs->GetActiveTabCtrl();
239
240 if( !tabCtrl )
241 return;
242
243 tabCtrl->Unbind( wxEVT_LEFT_DCLICK, &EDITOR_TABS_PANEL::onTabDClick, this );
244 tabCtrl->Bind( wxEVT_LEFT_DCLICK, &EDITOR_TABS_PANEL::onTabDClick, this );
245}
246
247
249{
250 // The notebook can rebuild its tab-strip control as pages come and go, so keep the double-click
251 // handler attached to the live control.
253
254 // Clamp the notebook to its tab-strip height so its unused page area collapses below the visible
255 // region and the shared canvas owns the rest of the pane.
256 const bool hasPages = !m_pageWindows.empty();
257
258 int height = hasPages ? m_tabs->GetTabCtrlHeight() : 0;
259
260 // GetTabCtrlHeight reports 0 before the strip has a valid measuring font, so fall back to a floor.
261 if( hasPages && height <= 0 )
262 height = FromDIP( 28 );
263
264 // Collapse the row when there are no tabs, but keep the control's own min height above zero so a
265 // stray paint never sees a zero-height client area and trips the GTK assert.
266 if( m_sizer )
267 m_sizer->SetItemMinSize( m_tabs, -1, height );
268
269 m_tabs->SetMinSize( wxSize( -1, hasPages ? height : FromDIP( 28 ) ) );
270 m_tabs->Show( hasPages );
271
272 if( m_sharedCanvas )
273 m_sharedCanvas->Show( hasPages );
274
275 Layout();
276}
277
278
280{
281 if( aIdx < 0 || aIdx >= static_cast<int>( m_model.Entries().size() ) )
282 return TAB_VISUAL_STATE{};
283
285 return onQueryVisualState( aIdx );
286
287 const EDITOR_TABS_MODEL::ENTRY& e = m_model.Entries()[aIdx];
288
290}
291
292
293int EDITOR_TABS_PANEL::AddTab( const wxString& aKey, const wxString& aLabel, bool aAsPreview )
294{
295 const int existing = m_model.FindIndex( aKey );
296
297 if( existing >= 0 )
298 {
299 m_model.OpenDocument( aKey, aAsPreview );
300 SelectTab( existing );
301 return existing;
302 }
303
304 // Capture the preview slot's key before OpenDocument overwrites it, else a reused slot leaves the
305 // old key as a dead step in the MRU order.
306 wxString reusedOldKey;
307
308 if( aAsPreview )
309 {
310 const int previewSlot = m_model.PreviewIndex();
311
312 if( previewSlot >= 0 )
313 reusedOldKey = m_model.Entries()[previewSlot].key;
314 }
315
316 const int reuse = m_model.OpenDocument( aKey, aAsPreview );
317
318 if( reuse < static_cast<int>( m_pageWindows.size() ) )
319 {
320 m_tabs->SetPageText( reuse, aLabel );
321
322 if( !reusedOldKey.empty() && reusedOldKey != aKey )
323 forgetMru( reusedOldKey );
324 }
325 else
326 {
327 wxWindow* key = new wxWindow( m_tabs, wxID_ANY );
328 key->Hide();
329
330 // The notebook force-selects its first page, so swallow that change event and drive activation
331 // explicitly through SelectTab below.
332 m_activating = true;
333 m_tabs->AddPage( key, aLabel, false );
334 m_activating = false;
335
336 m_pageWindows.push_back( key );
337 }
338
340
341 touchMru( aKey );
342 SelectTab( reuse );
343 m_tabs->Refresh();
344
345 return reuse;
346}
347
348
350{
351 closeTabInternal( aIdx );
352}
353
354
356{
357 if( aIdx < 0 || aIdx >= static_cast<int>( m_pageWindows.size() ) )
358 return;
359
360 // Remember the active tab as a key, not an index, since indices shift on removal.
361 const int activeBefore = GetActiveTab();
362 const wxString activeBeforeKey =
363 ( activeBefore >= 0 && activeBefore < static_cast<int>( m_model.Entries().size() ) )
364 ? m_model.Entries()[activeBefore].key
365 : wxString();
366
367 // Every close entry point funnels through here so the user is prompted exactly once.
369 return;
370
371 const wxString key = m_model.Entries()[aIdx].key;
372
373 // DeletePage frees the key window and reselects a neighbour; swallow that change event since the
374 // successor selection is chosen explicitly below.
375 m_activating = true;
376 m_tabs->DeletePage( static_cast<size_t>( aIdx ) );
377 m_activating = false;
378
379 m_pageWindows.erase( m_pageWindows.begin() + aIdx );
380 m_model.CloseDocument( key );
381 forgetMru( key );
382
384
385 const int newCount = static_cast<int>( m_pageWindows.size() );
386
387 if( newCount <= 0 )
388 {
389 m_tabs->Refresh();
390 return;
391 }
392
394 {
395 // The host already installed the successor document, so select the page visually only and
396 // keep the MRU consistent. Re-entering activation would double-install or hit a freed index.
397 int visualIdx;
398
399 if( activeBeforeKey.empty() || activeBeforeKey == key )
400 visualIdx = std::min( aIdx, newCount - 1 );
401 else
402 visualIdx = m_model.FindIndex( activeBeforeKey );
403
404 if( visualIdx >= 0 && visualIdx < newCount )
405 {
406 // ChangeSelection updates the strip without firing a change event, so the host is not
407 // re-notified to install a document it already installed.
408 m_tabs->ChangeSelection( static_cast<size_t>( visualIdx ) );
409 touchMru( m_model.Entries()[visualIdx].key );
410 }
411 }
412 else
413 {
414 SelectTab( std::min( aIdx, newCount - 1 ) );
415 }
416
417 m_tabs->Refresh();
418}
419
420
422{
423 if( aKeepIdx < 0 || aKeepIdx >= static_cast<int>( m_pageWindows.size() ) )
424 return;
425
426 const wxString keepKey = m_model.Entries()[aKeepIdx].key;
427
428 // Close from the back so earlier indices stay valid.
429 for( int i = static_cast<int>( m_pageWindows.size() ) - 1; i >= 0; --i )
430 {
431 if( m_model.Entries()[i].key != keepKey )
432 CloseTab( i );
433 }
434}
435
436
438{
439 // A negative anchor would otherwise close every tab.
440 if( aIdx < 0 || aIdx >= static_cast<int>( m_pageWindows.size() ) )
441 return;
442
443 for( int i = static_cast<int>( m_pageWindows.size() ) - 1; i > aIdx; --i )
444 CloseTab( i );
445}
446
447
449{
450 for( int i = static_cast<int>( m_pageWindows.size() ) - 1; i >= 0; --i )
451 CloseTab( i );
452}
453
454
455void EDITOR_TABS_PANEL::MarkModified( int aIdx, bool aModified )
456{
457 if( aIdx < 0 || aIdx >= static_cast<int>( m_model.Entries().size() ) )
458 return;
459
460 // Clearing the preview flag stops the next library-open from replacing this edited document.
461 m_model.MarkModified( m_model.Entries()[aIdx].key, aModified );
463}
464
465
467{
468 if( aIdx < 0 || aIdx >= static_cast<int>( m_model.Entries().size() ) )
469 return;
470
471 if( !m_model.Entries()[aIdx].preview )
472 return;
473
474 m_model.Promote( m_model.Entries()[aIdx].key );
476}
477
478
480{
481 if( aIdx < 0 || aIdx >= static_cast<int>( m_pageWindows.size() ) )
482 return;
483
484 // ChangeSelection moves the strip without firing a change event, so activation is driven once,
485 // here, rather than also arriving through onPageChanged.
486 m_tabs->ChangeSelection( static_cast<size_t>( aIdx ) );
487 m_tabs->Refresh();
488 activateTab( aIdx );
489}
490
491
493{
494 if( aIdx < 0 || aIdx >= static_cast<int>( m_model.Entries().size() ) )
495 return;
496
497 // Guard against re-entrancy from a programmatic SetActivePage so the host is notified once.
498 if( m_activating )
499 return;
500
501 m_activating = true;
502 touchMru( m_model.Entries()[aIdx].key );
503
504 if( onActivateTab )
505 onActivateTab( aIdx );
506
507 m_activating = false;
508}
509
510
511void EDITOR_TABS_PANEL::AdvanceTab( bool aForward )
512{
513 if( m_pageWindows.size() < 2 )
514 return;
515
516 // Cycle the MRU order so repeated Ctrl+Tab walks recently visited tabs first.
517 const int active = GetActiveTab();
518
519 if( active < 0 )
520 return;
521
522 const wxString activeKey = m_model.Entries()[active].key;
523 int pos = -1;
524
525 for( size_t i = 0; i < m_mru.size(); ++i )
526 {
527 if( m_mru[i] == activeKey )
528 {
529 pos = static_cast<int>( i );
530 break;
531 }
532 }
533
534 if( pos < 0 )
535 return;
536
537 const int count = static_cast<int>( m_mru.size() );
538 const int next = aForward ? ( pos + 1 ) % count : ( pos - 1 + count ) % count;
539 const int idx = m_model.FindIndex( m_mru[next] );
540
541 if( idx >= 0 )
542 SelectTab( idx );
543}
544
545
547{
548 return m_tabs->GetSelection();
549}
550
551
552int EDITOR_TABS_PANEL::FindTab( const wxString& aKey ) const
553{
554 return m_model.FindIndex( aKey );
555}
556
557
559{
560 m_tabs->Refresh();
561}
562
563
564void EDITOR_TABS_PANEL::touchMru( const wxString& aKey )
565{
566 forgetMru( aKey );
567 m_mru.insert( m_mru.begin(), aKey );
568}
569
570
571void EDITOR_TABS_PANEL::forgetMru( const wxString& aKey )
572{
573 m_mru.erase( std::remove( m_mru.begin(), m_mru.end(), aKey ), m_mru.end() );
574}
575
576
577void EDITOR_TABS_PANEL::onPageChanged( wxAuiNotebookEvent& aEvent )
578{
579 // The notebook drives its own active page, so a user tab switch surfaces here as the activation
580 // path. Programmatic selection uses ChangeSelection and is fired explicitly, not through here.
581 const int idx = aEvent.GetSelection();
582
583 if( idx >= 0 && idx < static_cast<int>( m_model.Entries().size() ) )
584 activateTab( idx );
585
586 aEvent.Skip();
587}
588
589
590void EDITOR_TABS_PANEL::onPageClose( wxAuiNotebookEvent& aEvent )
591{
592 const int idx = aEvent.GetSelection();
593
594 // Veto the control's own removal and route through CloseTab, which owns the single close prompt.
595 aEvent.Veto();
596 CloseTab( idx );
597}
598
599
600void EDITOR_TABS_PANEL::onTabRightDown( wxAuiNotebookEvent& aEvent )
601{
602 m_contextMenuIdx = aEvent.GetSelection();
603
604 if( m_contextMenuIdx < 0 )
605 return;
606
607 wxMenu menu;
608 menu.Append( ID_TAB_CLOSE, _( "Close Tab" ) );
609 menu.Append( ID_TAB_CLOSE_OTHERS, _( "Close Other Tabs" ) );
610 menu.Append( ID_TAB_CLOSE_TO_RIGHT, _( "Close Tabs to the Right" ) );
611 menu.Append( ID_TAB_CLOSE_ALL, _( "Close All Tabs" ) );
612
613 menu.Bind( wxEVT_COMMAND_MENU_SELECTED, &EDITOR_TABS_PANEL::onContextMenu, this );
614
615 PopupMenu( &menu );
616}
617
618
619void EDITOR_TABS_PANEL::onTabDClick( wxMouseEvent& aEvent )
620{
621 aEvent.Skip();
622
623 // The handler is bound on the tab strip control, so the click is in its coordinates and hit-tests
624 // against the same control.
625 wxAuiTabCtrl* tabCtrl = m_tabs->GetActiveTabCtrl();
626
627 if( !tabCtrl )
628 return;
629
630 wxWindow* page = nullptr;
631
632 if( tabCtrl->TabHitTest( aEvent.GetX(), aEvent.GetY(), &page ) )
633 PromoteTab( indexOfWindow( page ) );
634}
635
636
637void EDITOR_TABS_PANEL::onContextMenu( wxCommandEvent& aEvent )
638{
639 const int idx = m_contextMenuIdx;
640
641 if( idx < 0 )
642 return;
643
644 switch( aEvent.GetId() )
645 {
646 case ID_TAB_CLOSE: CloseTab( idx ); break;
647 case ID_TAB_CLOSE_OTHERS: CloseOthers( idx ); break;
648 case ID_TAB_CLOSE_TO_RIGHT: CloseToRight( idx ); break;
649 case ID_TAB_CLOSE_ALL: CloseAll(); break;
650 default: break;
651 }
652}
int PreviewIndex() const
Index of the current reusable preview tab, or -1 if none.
bool CanCloseWithoutPrompt(const wxString &aKey) const
True when the document has no unsaved edits and can be closed silently.
void Promote(const wxString &aKey)
Clear the preview flag so the tab becomes permanent and is no longer reused.
void MarkModified(const wxString &aKey, bool aModified)
Update the modified flag.
int FindIndex(const wxString &aKey) const
int OpenDocument(const wxString &aKey, bool aAsPreview)
Return the index to display the document at, reusing a preview slot or appending a new tab.
std::vector< ENTRY > m_entries
void CloseDocument(const wxString &aKey)
Drop the document with aKey, freeing the preview slot if it held it.
void closeTabInternal(int aIdx)
Close the tab at aIdx, prompting the host exactly once.
std::function< TAB_VISUAL_STATE(int)> onQueryVisualState
Host reports the visual state (modified/preview) for the tab at the given index.
void bindTabDClick()
(Re)bind the double-click handler to the notebook's current tab-strip control.
TAB_VISUAL_STATE visualStateForIndex(int aIdx) const
wxAuiNotebook * m_tabs
int indexOfWindow(wxWindow *aWindow) const
Map a notebook page window pointer to its current tab index, or -1.
void onPageChanged(wxAuiNotebookEvent &aEvent)
void CloseToRight(int aIdx)
void onContextMenu(wxCommandEvent &aEvent)
bool m_suppressActivateOnClose
When set, closeTabInternal selects the fallback page without firing onActivateTab.
void AdvanceTab(bool aForward)
std::vector< wxString > m_mru
Most-recently-used key order for Ctrl+Tab cycling; front is most recent.
void touchMru(const wxString &aKey)
Bump aKey to the front of the MRU order.
void onTabDClick(wxMouseEvent &aEvent)
void PromoteTab(int aIdx)
Convert a preview tab into a permanent one, dropping its italic styling.
std::function< void(int)> onActivateTab
Host swaps the active document context to the tab at the given index.
std::function< bool(int)> onCloseTabRequested
Host prompts as needed; return false to veto the close.
void MarkModified(int aIdx, bool aModified)
Mark the tab modified.
void onTabRightDown(wxAuiNotebookEvent &aEvent)
void updateTabStripHeight()
Clamp the notebook to its tab-strip height and re-Layout so its (unused) page area collapses and the ...
int AddTab(const wxString &aKey, const wxString &aLabel, bool aAsPreview)
void forgetMru(const wxString &aKey)
Remove aKey from the MRU order.
EDITOR_TABS_PANEL(wxWindow *aParent, EDA_DRAW_PANEL_GAL *aSharedCanvas)
EDA_DRAW_PANEL_GAL * m_sharedCanvas
void CloseOthers(int aKeepIdx)
void onPageClose(wxAuiNotebookEvent &aEvent)
std::vector< wxWindow * > m_pageWindows
Hidden per-tab key windows; index-aligned with the model entries.
void activateTab(int aIdx)
Activate the tab at aIdx without re-entering the change handler.
EDITOR_TABS_MODEL m_model
void ReleaseSharedCanvas()
Hand the host-owned canvas back to its original parent.
wxWindow * m_originalCanvasParent
The canvas's parent before it was borrowed, reparented back on destruction so it is not freed as a ch...
bool m_activating
Guards activateTab() against re-entrancy from programmatic SetActivePage().
int FindTab(const wxString &aKey) const
A wxAuiTabArt that renders editor tabs with preview/modified decorations.
#define _(s)
@ ID_TAB_CLOSE_TO_RIGHT
@ ID_TAB_CLOSE_OTHERS
@ ID_TAB_CLOSE_ALL
@ ID_TAB_CLOSE
TAB_VISUAL_STATE ResolveTabVisualState(bool aPreview, bool aModified)
Resolve a tab's decorations from its document state flags.
CITER next(CITER it)
Definition ptree.cpp:120
Visual decorations derived from document state: preview is italic, modified is bold with a leading as...