diff --git a/include/wx/aui/auibook.h b/include/wx/aui/auibook.h index 62c9af46b4..8051ccd7c3 100644 --- a/include/wx/aui/auibook.h +++ b/include/wx/aui/auibook.h @@ -26,6 +26,9 @@ #include "wx/compositebookctrl.h" +class wxAuiSerializer; +class wxAuiDeserializer; + class wxAuiNotebook; class wxAuiTabFrame; @@ -432,6 +435,11 @@ public: // Internal, don't use: use GetPagePosition() instead. bool FindTab(wxWindow* page, wxAuiTabCtrl** ctrl, int* idx) const; + // Serialization support: this is only used by wxAuiManager, don't use + // directly. + void SaveLayout(const wxString& name, wxAuiSerializer& serializer) const; + void LoadLayout(const wxString& name, wxAuiDeserializer& deserializer); + protected: // Common part of all ctors. void Init(); diff --git a/include/wx/aui/serializer.h b/include/wx/aui/serializer.h index bc510e1f11..94f40606dd 100644 --- a/include/wx/aui/serializer.h +++ b/include/wx/aui/serializer.h @@ -16,6 +16,8 @@ // Classes used to save/load wxAuiManager layout. // ---------------------------------------------------------------------------- +// Fields common to wxAuiPaneLayoutInfo and wxAuiTabLayoutInfo containing +// information about a docked pane or tab layout. struct wxAuiDockLayoutInfo { // Identifies the dock containing the pane. @@ -33,6 +35,15 @@ struct wxAuiDockLayoutInfo int dock_size = 0; }; +// This struct contains information about the layout of a tab control in a +// wxAuiNotebook, including where it is docked and the order of pages in it. +struct wxAuiTabLayoutInfo : wxAuiDockLayoutInfo +{ + // If this vector is empty, it means that the tab control contains all + // notebook pages in natural order. + std::vector pages; +}; + // This struct contains the pane name and information about its layout that can // be manipulated by the user interactively. struct wxAuiPaneLayoutInfo : wxAuiDockLayoutInfo @@ -87,6 +98,25 @@ public: // Called after the last call to SavePane(), does nothing by default. virtual void AfterSavePanes() { } + // Called before starting to save information about the notebooks, does + // nothing by default. + virtual void BeforeSaveNotebooks() { } + + // Called before starting to save information about the tabs in the + // notebook in the AUI pane with the given name. + virtual void BeforeSaveNotebook(const wxString& name) = 0; + + // Called to save information about a single tab control in the given + // notebook. + virtual void SaveNotebookTabControl(const wxAuiTabLayoutInfo& tab) = 0; + + // Called after saving information about all the pages of the notebook in + // the AUI pane with the given name, does nothing by default. + virtual void AfterSaveNotebook() { } + + // Called after the last call to SaveNotebook(), does nothing by default. + virtual void AfterSaveNotebooks() { } + // Called after saving everything, does nothing by default. virtual void AfterSave() { } }; @@ -116,6 +146,11 @@ public: // Load information about all the panes previously saved with SavePane(). virtual std::vector LoadPanes() = 0; + // For a pane containing wxAuiNotebook, load information about all the tab + // controls inside it. + virtual std::vector + LoadNotebookTabs(const wxString& name) = 0; + // Create the window to be managed by the given pane: this is called if any // of the panes returned by LoadPanes() doesn't exist in the existing // layout and allows to create windows on the fly. diff --git a/interface/wx/aui/serializer.h b/interface/wx/aui/serializer.h index ac9880c99d..d0387f6ce4 100644 --- a/interface/wx/aui/serializer.h +++ b/interface/wx/aui/serializer.h @@ -8,25 +8,16 @@ ///////////////////////////////////////////////////////////////////////////// /** - Description of user-modifiable pane layout information. + Description of a docked element layout. - This struct is used with wxAuiSerializer and wxAuiDeserializer to store the - pane layout. Its fields have the same meaning as the corresponding fields - in wxAuiPaneInfo (with the exception of `is_maximized`), but it doesn't - contain the fields that it wouldn't make sense to serialize. + The fields in this struct are shared by wxAuiPaneLayoutInfo and + wxAuiTabLayoutInfo and contain information about the layout of a docked + pane or tab layout. @since 3.3.0 - */ -struct wxAuiPaneLayoutInfo +*/ +struct wxAuiDockLayoutInfo { - /** - Ctor sets the name, which is always required. - */ - explicit wxAuiPaneLayoutInfo(wxString name); - - /// Unique name of the pane. - wxString name; - /// Direction of the dock containing the pane. int dock_direction = wxAUI_DOCK_LEFT; @@ -44,7 +35,47 @@ struct wxAuiPaneLayoutInfo /// Size of the containing dock. int dock_size = 0; +}; +/** + Contains information about the layout of a tab control in a wxAuiNotebook. + + This includes where it is docked, via the fields inherited from + wxAuiDockLayoutInfo, and the order of pages in it. + + @since 3.3.0 +*/ +struct wxAuiTabLayoutInfo : wxAuiDockLayoutInfo +{ + /** + Indices of the pages in this tab control in their order on screen. + + If this vector is empty, it means that the tab control contains all + notebook pages in natural order. + */ + std::vector pages; +}; + +/** + Description of user-modifiable pane layout information. + + This struct is used with wxAuiSerializer and wxAuiDeserializer to store the + pane layout. Its fields, including the inherited ones from + wxAuiDockLayoutInfo, have the same meaning as the corresponding fields in + wxAuiPaneInfo (with the exception of `is_maximized`), but it doesn't + contain the fields that it wouldn't make sense to serialize. + + @since 3.3.0 + */ +struct wxAuiPaneLayoutInfo : wxAuiDockLayoutInfo +{ + /** + Ctor sets the name, which is always required. + */ + explicit wxAuiPaneLayoutInfo(wxString name); + + /// Unique name of the pane. + wxString name; /// Position of the pane when floating, may be invalid. wxPoint floating_pos = wxDefaultPosition; @@ -126,6 +157,57 @@ public: */ virtual void AfterSavePanes(); + /** + Called before starting to save information about the notebooks. + + Does nothing by default. + + Note that this function is called after AfterSavePanes() but may not be + called at all if there are no panes containing wxAuiNotebook. + */ + virtual void BeforeSaveNotebooks(); + + /** + Called before starting to save information about the tabs in the + notebook in the AUI pane with the given name. + + This function needs to be overridden to keep record of the notebook for + which SaveNotebookTabControl() will be called next. Of course, if + saving notebook layout is unnecessary, e.g. because the program doesn't + use wxAuiNotebook at all, the implementation can be trivial and just do + nothing. + + This function is called one or more times after BeforeSaveNotebooks(). + */ + virtual void BeforeSaveNotebook(const wxString& name) = 0; + + /** + Called to save information about a single tab control in the given + notebook. + + This function will be called for all tab controls in the notebook after + BeforeSaveNotebook(). + + As with that function, it has to be implemented, but can simply do + nothing if saving notebook layout is not necessary. + */ + virtual void SaveNotebookTabControl(const wxAuiTabLayoutInfo& tab) = 0; + + /** + Called after saving information about all the pages of the notebook in + the AUI pane with the given name. + + Does nothing by default. + */ + virtual void AfterSaveNotebook(); + + /** + Called after the last call to SaveNotebook(). + + Does nothing by default. + */ + virtual void AfterSaveNotebooks(); + /** Called after saving everything. @@ -190,6 +272,19 @@ public: */ virtual std::vector LoadPanes() = 0; + /** + Load information about the notebook tabs previously saved by + wxAuiSerializer::SaveNotebookTabControl(). + + The pane with the name @a name is guaranteed to exist in the layout and + have wxAuiNotebook as the associated window. + + If restoring the notebook layout is not necessary, this function can + just return an empty vector. + */ + virtual std::vector + LoadNotebookTabs(const wxString& name) = 0; + /** Create the window to be managed by the given pane if necessary. diff --git a/samples/aui/auidemo.cpp b/samples/aui/auidemo.cpp index eab1ad5549..1a61b8d373 100644 --- a/samples/aui/auidemo.cpp +++ b/samples/aui/auidemo.cpp @@ -1544,15 +1544,7 @@ public: auto node = new wxXmlNode(wxXML_ELEMENT_NODE, "pane"); node->AddAttribute("name", pane.name); - AddChild(node, "direction", pane.dock_direction); - AddChild(node, "layer", pane.dock_layer); - AddChild(node, "row", pane.dock_row); - AddChild(node, "position", pane.dock_pos); - AddChild(node, "proportion", pane.dock_proportion); - - // Saving dock size of 0 is harmless but unnecessary, so don't do it. - if ( pane.dock_size ) - AddChild(node, "size", pane.dock_size); + AddDockLayout(node, pane); AddChild(node, "floating-rect", wxRect(pane.floating_pos, pane.floating_size)); @@ -1570,6 +1562,54 @@ public: m_root->AddChild(m_panes.release()); } + + virtual void BeforeSaveNotebooks() override + { + m_books.reset(new wxXmlNode(wxXML_ELEMENT_NODE, "notebooks")); + } + + virtual void BeforeSaveNotebook(const wxString& name) override + { + m_book.reset(new wxXmlNode(wxXML_ELEMENT_NODE, "notebook")); + m_book->AddAttribute("name", name); + } + + virtual void SaveNotebookTabControl(const wxAuiTabLayoutInfo& tab) override + { + auto node = new wxXmlNode(wxXML_ELEMENT_NODE, "tab"); + + AddDockLayout(node, tab); + + // We don't need to save the pages order if the vector is empty, this + // means that this tab contains all the notebook pages in default order. + if ( !tab.pages.empty() ) + { + wxString pagesList; + for ( auto page : tab.pages ) + { + if ( !pagesList.empty() ) + pagesList << ','; + + pagesList << page; + } + + AddChild(node, "pages", pagesList); + } + + m_book->AddChild(node); + } + + virtual void AfterSaveNotebook() override + { + m_books->AddChild(m_book.release()); + } + + virtual void AfterSaveNotebooks() override + { + m_root->AddChild(m_books.release()); + } + + virtual void AfterSave() override {} private: @@ -1595,6 +1635,22 @@ private: } } + // Common helper of SavePane() and SaveNotebookTabControl() which both need + // to save the same layout information. + void AddDockLayout(wxXmlNode* node, const wxAuiDockLayoutInfo& layout) + { + AddChild(node, "direction", layout.dock_direction); + AddChild(node, "layer", layout.dock_layer); + AddChild(node, "row", layout.dock_row); + AddChild(node, "position", layout.dock_pos); + AddChild(node, "proportion", layout.dock_proportion); + + // Saving dock size of 0 is harmless but unnecessary, so don't do it. + if ( layout.dock_size ) + AddChild(node, "size", layout.dock_size); + } + + wxXmlDocument m_doc; // Non-owning pointer to the root node of m_doc. @@ -1604,6 +1660,8 @@ private: // document -- this ensures that we don't leak memory if an exception is // thrown before this happens. std::unique_ptr m_panes; + std::unique_ptr m_books; + std::unique_ptr m_book; }; class MyXmlDeserializer : public wxAuiDeserializer @@ -1627,6 +1685,13 @@ public: if ( layout->GetNext() ) throw std::runtime_error("Unexpected multiple layout nodes"); + // Check that we only have the top level nodes that we expect. + // + // This is nice to detect errors in this sample, but note that this + // might not be the best strategy for a real application, which might + // decide to to gracefully ignore unknown nodes instead of failing, or + // at least save the format version in the XML file to be able to give + // a better error message. for ( wxXmlNode* node = layout->GetChildren(); node; node = node->GetNext() ) { if ( node->GetName() == "panes" ) @@ -1636,6 +1701,13 @@ public: m_panes = node; } + else if ( node->GetName() == "notebooks" ) + { + if ( m_books ) + throw std::runtime_error("Unexpected multiple notebooks nodes"); + + m_books = node; + } else { throw std::runtime_error("Unexpected node name"); @@ -1661,34 +1733,13 @@ public: for ( wxXmlNode* child = node->GetChildren(); child; child = child->GetNext() ) { + if ( LoadDockLayout(child, pane) ) + continue; + const wxString& name = child->GetName(); const wxString& content = child->GetNodeContent(); - if ( name == "direction" ) - { - pane.dock_direction = GetInt(content); - } - else if ( name == "layer" ) - { - pane.dock_layer = GetInt(content); - } - else if ( name == "row" ) - { - pane.dock_row = GetInt(content); - } - else if ( name == "position" ) - { - pane.dock_pos = GetInt(content); - } - else if ( name == "proportion" ) - { - pane.dock_proportion = GetInt(content); - } - else if ( name == "size" ) - { - pane.dock_size = GetInt(content); - } - else if ( name == "floating-rect" ) + if ( name == "floating-rect" ) { auto rect = GetRect(content); @@ -1712,6 +1763,26 @@ public: return panes; } + virtual std::vector + LoadNotebookTabs(const wxString& name) override + { + // Find the notebook with the given name. + for ( wxXmlNode* node = m_books->GetChildren(); node; node = node->GetNext() ) + { + if ( node->GetName() != "notebook" ) + throw std::runtime_error("Unexpected notebook node name"); + + if ( node->GetAttribute("name") == name ) + return LoadNotebookTabs(node); + } + + // As above, this might not be the best thing to do in a real + // application, where, perhaps, the XML file was saved by a newer + // version of the problem, but here we do this for simplicity and to + // make sure we detect any errors. + throw std::runtime_error("Notebook with the given name not found"); + } + virtual wxWindow* CreatePaneWindow(wxAuiPaneInfo& pane) override { wxLogWarning("Unknown pane \"%s\"", pane.name); @@ -1755,11 +1826,83 @@ private: return wxRect(wxPoint(x, y), GetSize(strWH)); } + // Common helper of LoadPanes() and LoadNotebookTabs() which both need to + // load the dock layout information. + // + // Returns true if we processed this node. + bool LoadDockLayout(wxXmlNode* node, wxAuiDockLayoutInfo& info) + { + const wxString& name = node->GetName(); + const wxString& content = node->GetNodeContent(); + + if ( name == "direction" ) + { + info.dock_direction = GetInt(content); + } + else if ( name == "layer" ) + { + info.dock_layer = GetInt(content); + } + else if ( name == "row" ) + { + info.dock_row = GetInt(content); + } + else if ( name == "position" ) + { + info.dock_pos = GetInt(content); + } + else if ( name == "proportion" ) + { + info.dock_proportion = GetInt(content); + } + else if ( name == "size" ) + { + info.dock_size = GetInt(content); + } + else + { + return false; + } + + return true; + } + + std::vector LoadNotebookTabs(wxXmlNode* book) + { + std::vector tabs; + + for ( wxXmlNode* node = book->GetChildren(); node; node = node->GetNext() ) + { + if ( node->GetName() != "tab" ) + throw std::runtime_error("Unexpected tab node name"); + + wxAuiTabLayoutInfo tab; + for ( wxXmlNode* child = node->GetChildren(); child; child = child->GetNext() ) + { + if ( LoadDockLayout(child, tab) ) + continue; + + if ( child->GetName() != "pages" ) + throw std::runtime_error("Unexpected tab child node name"); + + for ( const auto& s : wxSplit(child->GetNodeContent(), ',') ) + { + tab.pages.push_back(GetInt(s)); + } + } + + tabs.push_back(tab); + } + + return tabs; + } + wxXmlDocument m_doc; // Non-owning pointers to the nodes in m_doc. wxXmlNode* m_panes = nullptr; + wxXmlNode* m_books = nullptr; }; void MyFrame::OnCopyLayout(wxCommandEvent& WXUNUSED(evt)) diff --git a/src/aui/auibook.cpp b/src/aui/auibook.cpp index 1c960010cb..dd2ea2950a 100644 --- a/src/aui/auibook.cpp +++ b/src/aui/auibook.cpp @@ -26,6 +26,7 @@ #include "wx/frame.h" #endif +#include "wx/aui/serializer.h" #include "wx/aui/tabmdi.h" #include "wx/wupdlock.h" @@ -35,6 +36,8 @@ #include "wx/osx/private.h" #endif +#include + wxDEFINE_EVENT(wxEVT_AUINOTEBOOK_PAGE_CLOSE, wxAuiNotebookEvent); wxDEFINE_EVENT(wxEVT_AUINOTEBOOK_PAGE_CLOSED, wxAuiNotebookEvent); wxDEFINE_EVENT(wxEVT_AUINOTEBOOK_PAGE_CHANGING, wxAuiNotebookEvent); @@ -3686,5 +3689,197 @@ void wxAuiTabCtrl::SetHoverTab(wxWindow* wnd) } } +// ---------------------------------------------------------------------------- +// Layout serialization +// ---------------------------------------------------------------------------- + +void +wxAuiNotebook::SaveLayout(const wxString& name, + wxAuiSerializer& serializer) const +{ + serializer.BeforeSaveNotebook(name); + + for ( const auto& pane : m_mgr.GetAllPanes() ) + { + if ( IsDummyPane(pane) ) + continue; + + wxAuiTabLayoutInfo tab; + m_mgr.CopyDockLayoutFrom(tab, pane); + + const wxAuiTabCtrl* const + tabCtrl = static_cast(pane.window)->m_tabs; + + // As an optimization, don't bother with saving the pages order for the + // main control if it hasn't been changed from the default. + bool mustSavePages = false; + if ( tab.dock_direction != wxAUI_DOCK_CENTER ) + { + // The non-main tab controls can't possibly have all the pages, so + // we always have to save the indices of the ones they contain. + mustSavePages = true; + } + else if ( tabCtrl->GetPageCount() != GetPageCount() ) + { + // If the main control doesn't have all the pages, we need to save + // them for it as well. + mustSavePages = true; + } + + std::vector pages; + int n = 0; + for ( const auto& page : tabCtrl->GetPages() ) + { + // This is inefficient for many pages, we should consider using a + // hash map indexed by the window pointer if this proves to be a + // problem in practice. + const int idx = m_tabs.GetIdxFromWindow(page.window); + if ( idx != n++ ) + { + // And if the pages are not in order, we must save them too. + mustSavePages = true; + } + + pages.push_back(idx); + } + + // But if none of the conditions above is true, we can avoid saving + // them, which also allows us not to remove and re-add them when + // restoring later, see LoadLayout() below. + if ( mustSavePages ) + tab.pages = std::move(pages); + + serializer.SaveNotebookTabControl(tab); + } + + serializer.AfterSaveNotebook(); +} + +void +wxAuiNotebook::LoadLayout(const wxString& name, wxAuiDeserializer& deserializer) +{ + const auto tabs = deserializer.LoadNotebookTabs(name); + + if ( tabs.empty() ) + return; + + // Remove any existing tabs before adding new ones. + UnsplitAll(); + + // Get the only remaining tab control. + wxAuiTabCtrl* const tabMain = GetMainTabCtrl(); + + // Keep track of pages we've already added to some tab control: even if the + // deserialized data is somehow incorrect and duplicates the page indices, + // we don't want to try to have the same page in more than one tab control. + std::unordered_set addedPages; + const int pageCount = m_tabs.GetPageCount(); + + for ( const auto& tab : tabs ) + { + wxAuiTabCtrl* tabCtrl = nullptr; + + // The pages pointer will be set to point to either tab.pages or + // pageDefault in which case it will also be filled. + std::vector pagesDefault; + const std::vector* pages = nullptr; + + // We don't need to create a new pane for the main, central tab control, + // but we still need to order its tabs correctly. + if ( tab.dock_direction == wxAUI_DOCK_CENTER ) + { + // Special case: if the pages vector is empty check if we're using + // the default order, in which case we can avoid doing anything if + // the pages are already in the same order (which is a common case). + if ( tab.pages.empty() ) + { + bool mustRestore = false; + for ( int i = 0; i < pageCount; ++i ) + { + if ( tabMain->GetWindowFromIdx(i) != m_tabs.GetWindowFromIdx(i) ) + { + mustRestore = true; + break; + } + } + + if ( !mustRestore ) + { + // All pages are in the main tab in the default order + // already, so we don't have anything to do. + break; + } + + // The pages had been reordered, restore the default order when + // re-adding them below. + for ( int i = 0; i < pageCount; ++i ) + pagesDefault.push_back(i); + + pages = &pagesDefault; + } + else + { + // Just use the specified pages. + pages = &tab.pages; + } + + // In any case, we must remove all pages currently in this tab as + // they will be re-added. + tabMain->RemoveAll(); + + tabCtrl = tabMain; + } + else // Non-central tab control. + { + // In this case, we must have some pages, but if we somehow don't, + // just skip adding the tab control entirely. + if ( tab.pages.empty() ) + continue; + + // Re-add using the saved layout. + wxAuiPaneInfo pane; + m_mgr.CopyDockLayoutTo(tab, pane); + + // Our panes never show caption. + pane.CaptionVisible(false); + + wxAuiTabFrame* tabframe = CreateTabFrame(); + m_mgr.AddPane(tabframe, pane); + + tabCtrl = tabframe->m_tabs; + pages = &tab.pages; + } + + // In any case, add the pages that this tab control had before to it. + bool first = true; + for ( auto page : *pages ) + { + // We just silently ignore invalid or duplicate page indices + // here, because it doesn't seem right for them to result in a + // fatal error and any warnings would be just annoying. + if ( page >= pageCount ) + continue; + + if ( !addedPages.insert(page).second ) + continue; + + auto info = m_tabs.GetPage(page); + + // We don't save the last active page currently, so just make the + // first one active. + if ( first ) + { + info.active = true; + first = false; + } + + tabCtrl->AddPage(info); + } + + tabCtrl->DoUpdateActive(); + } + + m_mgr.Update(); +} #endif // wxUSE_AUI diff --git a/src/aui/framemanager.cpp b/src/aui/framemanager.cpp index 0fc7065a35..5d4645d445 100644 --- a/src/aui/framemanager.cpp +++ b/src/aui/framemanager.cpp @@ -25,6 +25,7 @@ #include "wx/aui/floatpane.h" #include "wx/aui/tabmdi.h" #include "wx/aui/auibar.h" +#include "wx/aui/auibook.h" #include "wx/aui/serializer.h" #include "wx/mdi.h" #include "wx/wupdlock.h" @@ -69,6 +70,7 @@ wxDEFINE_EVENT( wxEVT_AUI_FIND_MANAGER, wxAuiManagerEvent ); #include "wx/generic/private/drawresize.h" +#include #include wxIMPLEMENT_DYNAMIC_CLASS(wxAuiManagerEvent, wxEvent); @@ -1451,6 +1453,10 @@ void wxAuiManager::SaveLayout(wxAuiSerializer& serializer) const { serializer.BeforeSavePanes(); + // Collect information about all the notebooks we may have while saving + // the panes layout. + std::map notebooks; + for ( const auto& pane : m_panes ) { wxAuiPaneLayoutInfo layoutInfo{pane.name}; @@ -1460,9 +1466,26 @@ void wxAuiManager::SaveLayout(wxAuiSerializer& serializer) const MakeDIP(m_frame, layoutInfo.floating_size); serializer.SavePane(layoutInfo); + + if ( auto* const nb = wxDynamicCast(pane.window, wxAuiNotebook) ) + { + notebooks[pane.name] = nb; + } } serializer.AfterSavePanes(); + + if ( !notebooks.empty() ) + { + serializer.BeforeSaveNotebooks(); + + for ( const auto& kv : notebooks ) + { + kv.second->SaveLayout(kv.first, serializer); + } + + serializer.AfterSaveNotebooks(); + } } serializer.AfterSave(); @@ -1504,7 +1527,7 @@ void wxAuiManager::LoadLayout(wxAuiDeserializer& deserializer) hasMaximized = true; // Find the pane with the same name in the existing layout. - bool found = false; + wxWindow* window = nullptr; for ( auto& existingPane : panes ) { if ( existingPane.name == layoutInfo.name ) @@ -1512,21 +1535,29 @@ void wxAuiManager::LoadLayout(wxAuiDeserializer& deserializer) // Update the existing pane with the restored layout. CopyLayoutTo(layoutInfo, existingPane); - found = true; + window = existingPane.window; break; } } // This pane couldn't be found in the existing layout, let deserializer // create a new window for it if desired, otherwise just ignore it. - if ( !found ) + if ( !window ) { wxAuiPaneInfo pane; pane.name = layoutInfo.name; CopyLayoutTo(layoutInfo, pane); - if ( const auto w = deserializer.CreatePaneWindow(pane) ) - newPanes.emplace_back(w, pane); + window = deserializer.CreatePaneWindow(pane); + if ( !window ) + continue; + + newPanes.emplace_back(window, pane); + } + + if ( auto* const nb = wxDynamicCast(window, wxAuiNotebook) ) + { + nb->LoadLayout(layoutInfo.name, deserializer); } }