diff --git a/Makefile.in b/Makefile.in index 383cde57e2..56c24bf5d2 100644 --- a/Makefile.in +++ b/Makefile.in @@ -4018,6 +4018,7 @@ COND_USE_GUI_1_ALL_GUI_HEADERS = \ wx/aui/tabart.h \ wx/xrc/xh_aui.h \ wx/xrc/xh_auitoolb.h \ + wx/aui/serializer.h \ $(AUI_GTK_HDR) \ $(AUI_PLATFORM_HDR) \ wx/propgrid/advprops.h \ diff --git a/build/bakefiles/files.bkl b/build/bakefiles/files.bkl index a3959c3df6..5df3d90b4e 100644 --- a/build/bakefiles/files.bkl +++ b/build/bakefiles/files.bkl @@ -3006,6 +3006,7 @@ IMPORTANT: please read docs/tech/tn0016.txt before modifying this file! wx/aui/tabart.h wx/xrc/xh_aui.h wx/xrc/xh_auitoolb.h + wx/aui/serializer.h $(AUI_CMN_HDR) diff --git a/build/cmake/files.cmake b/build/cmake/files.cmake index 035be3cdd8..bdea252b76 100644 --- a/build/cmake/files.cmake +++ b/build/cmake/files.cmake @@ -2793,6 +2793,7 @@ set(AUI_CMN_HDR wx/aui/tabart.h wx/xrc/xh_aui.h wx/xrc/xh_auitoolb.h + wx/aui/serializer.h ) set(AUI_MSW_HDR diff --git a/build/cmake/samples/CMakeLists.txt b/build/cmake/samples/CMakeLists.txt index f2508e244d..77486f1c64 100644 --- a/build/cmake/samples/CMakeLists.txt +++ b/build/cmake/samples/CMakeLists.txt @@ -11,7 +11,7 @@ wx_add_sample(access accesstest.cpp DEPENDS wxUSE_ACCESSIBILITY) wx_add_sample(animate anitest.cpp anitest.h DATA throbber.gif throbber_2x.gif hourglass.ani DEPENDS wxUSE_ANIMATIONCTRL) wx_add_sample(archive CONSOLE) wx_add_sample(artprov arttest.cpp artbrows.cpp artbrows.h) -wx_add_sample(aui auidemo.cpp LIBRARIES wxaui wxhtml NAME auidemo DEPENDS wxUSE_AUI) +wx_add_sample(aui auidemo.cpp LIBRARIES wxaui wxhtml wxxml NAME auidemo DEPENDS wxUSE_AUI) wx_add_sample(calendar DEPENDS wxUSE_CALENDARCTRL) wx_add_sample(caret DEPENDS wxUSE_CARET) wx_add_sample(collpane DEPENDS wxUSE_COLLPANE) diff --git a/build/files b/build/files index 8fc9cede57..b261af69f1 100644 --- a/build/files +++ b/build/files @@ -2722,6 +2722,7 @@ AUI_CMN_HDR = wx/aui/auibar.h wx/aui/tabmdi.h wx/aui/aui.h + wx/aui/serializer.h wx/aui/tabart.h wx/xrc/xh_aui.h wx/xrc/xh_auitoolb.h diff --git a/build/msw/wx_aui.vcxproj b/build/msw/wx_aui.vcxproj index 8f5e4d9288..448783fcab 100644 --- a/build/msw/wx_aui.vcxproj +++ b/build/msw/wx_aui.vcxproj @@ -931,6 +931,7 @@ + diff --git a/build/msw/wx_aui.vcxproj.filters b/build/msw/wx_aui.vcxproj.filters index e582bb6b1c..09323c6043 100644 --- a/build/msw/wx_aui.vcxproj.filters +++ b/build/msw/wx_aui.vcxproj.filters @@ -86,6 +86,9 @@ Common Headers + + Common Headers + Common Headers diff --git a/include/wx/aui/framemanager.h b/include/wx/aui/framemanager.h index fdb412eaba..cab528d13c 100644 --- a/include/wx/aui/framemanager.h +++ b/include/wx/aui/framemanager.h @@ -128,6 +128,8 @@ class wxAuiPaneInfo; class wxAuiDockInfo; class wxAuiDockArt; class wxAuiManagerEvent; +class wxAuiSerializer; +class wxAuiDeserializer; using wxAuiDockUIPartArray = wxBaseArray; using wxAuiDockInfoArray = wxBaseArray; @@ -452,6 +454,12 @@ public: void Update(); + // Serialize or restore the whole layout using the provided serializer. + void SaveLayout(wxAuiSerializer& serializer) const; + void LoadLayout(wxAuiDeserializer& deserializer); + + // Older functions using bespoke text format, prefer using the ones using + // wxAuiSerializer and wxAuiDeserializer above instead in the new code. wxString SavePaneInfo(const wxAuiPaneInfo& pane); void LoadPaneInfo(wxString panePart, wxAuiPaneInfo &pane); wxString SavePerspective(); diff --git a/include/wx/aui/serializer.h b/include/wx/aui/serializer.h new file mode 100644 index 0000000000..1b720d744e --- /dev/null +++ b/include/wx/aui/serializer.h @@ -0,0 +1,109 @@ +/////////////////////////////////////////////////////////////////////////////// +// Name: wx/aui/serializer.h +// Purpose: Declaration of wxAuiSerializer and wxAuiDeserializer classes. +// Author: Vadim Zeitlin +// Created: 2024-01-20 +// Copyright: (c) 2024 Vadim Zeitlin +// Licence: wxWindows licence +/////////////////////////////////////////////////////////////////////////////// + +#ifndef _WX_AUI_SERIALIZER_H_ +#define _WX_AUI_SERIALIZER_H_ + +// ---------------------------------------------------------------------------- +// Classes used to save/load wxAuiManager layout. +// ---------------------------------------------------------------------------- + +// wxAuiSerializer is used with wxAuiManager::SaveLayout(). +// +// This is an abstract base class, you need to inherit from it and override its +// pure virtual functions in your derived class. +// +// If any of the functions of the derived class throw an exception, it is +// propagated out of wxAuiManager::SaveLayout() and it's callers responsibility +// to handle it. +class wxAuiSerializer +{ +public: + // Trivial default ctor. + wxAuiSerializer() = default; + + // Trivial but virtual dtor for a base class. + virtual ~wxAuiSerializer() = default; + + + // Called before doing anything else, does nothing by default. + virtual void BeforeSave() { } + + // Called before starting to save information about the panes, does nothing + // by default. + virtual void BeforeSavePanes() { } + + // Save information about the given pane. + virtual void SavePane(const wxAuiPaneInfo& pane) = 0; + + // Called after the last call to SavePane(), does nothing by default. + virtual void AfterSavePanes() { } + + // Called before starting to save information about the docks, does nothing + // by default. + virtual void BeforeSaveDocks() { } + + // Save information about the given dock. + virtual void SaveDock(const wxAuiDockInfo& dock) = 0; + + // Called after the last call to SaveDock(), does nothing by default. + virtual void AfterSaveDocks() { } + + // Called after saving everything, does nothing by default. + virtual void AfterSave() { } +}; + +// wxAuiDeserializer is used with wxAuiManager::LoadLayout(). +// +// As wxAuiSerializer, this is an abstract base class, you need to inherit from +// it and override its pure virtual functions in your derived class. +// +// Derived class function also may throw and, if any of them other than +// AfterLoad() does, the existing layout is not changed, i.e. +// wxAuiManager::LoadLayout() is exception-safe. +class wxAuiDeserializer +{ +public: + // Ctor takes the manager for which we're restoring the layout, it must + // remain valid for the lifetime of this object. + explicit wxAuiDeserializer(wxAuiManager& manager) : m_manager(manager) { } + + // Trivial but virtual dtor for a base class. + virtual ~wxAuiDeserializer() = default; + + + // Called before doing anything else, does nothing by default. + virtual void BeforeLoad() { } + + // Load information about all the panes previously saved with SavePane(). + virtual std::vector LoadPanes() = 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. + // + // If this function returns nullptr, the pane is not added to the manager. + virtual wxWindow* CreatePaneWindow(const wxAuiPaneInfo& WXUNUSED(pane)) + { + return nullptr; + } + + // Load information about all the docks previously saved with SaveDock(). + virtual std::vector LoadDocks() = 0; + + // Called after restoring everything, calls Update() on the manager by + // default. + virtual void AfterLoad() { m_manager.Update(); } + +protected: + // The manager for which we're restoring the layout. + wxAuiManager& m_manager; +}; + +#endif // _WX_AUI_SERIALIZER_H_ diff --git a/interface/wx/aui/framemanager.h b/interface/wx/aui/framemanager.h index ead1c1c559..1b5d36e539 100644 --- a/interface/wx/aui/framemanager.h +++ b/interface/wx/aui/framemanager.h @@ -371,6 +371,19 @@ public: const wxAuiPaneInfo& insert_location, int insert_level = wxAUI_INSERT_PANE); + /** + Load the layout information saved by SaveLayout(). + + The implementation of wxAuiDeserializer object passed to this function + should be consistent with that of the serializer used to save the + layout. See @ref page_samples_aui for an example of using serializer + saving the layout in XML format and matching deserializer restoring the + layout from it. + + @since 3.3.0 + */ + void LoadLayout(wxAuiDeserializer& deserializer) const; + /** LoadPaneInfo() is similar to LoadPerspective, with the exception that it only loads information about a single pane. @@ -389,6 +402,10 @@ public: /** Loads a saved perspective. + This function is used to load layouts previously saved with + SavePerspective(), use LoadLayout() to load a layout saved with + SaveLayout(). + A perspective is the layout state of an AUI managed window. All currently existing panes that have an object in "perspective" @@ -422,6 +439,19 @@ public: */ void RestoreMaximizedPane(); + /** + Save the layout information using the provided object. + + This function allows to use a custom @a serializer to save the layout + information in any format, e.g. @ref page_samples_aui shows how to save + it in XML format. + + See wxAuiSerializer documentation for more details. + + @since 3.3.0 + */ + void SaveLayout(wxAuiSerializer& serializer) const; + /** SavePaneInfo() is similar to SavePerspective, with the exception that it only saves information about a single pane. @@ -441,6 +471,9 @@ public: Saves the entire user interface layout into an encoded wxString, which can then be stored by the application (probably using wxConfig). + @note You may prefer to use SaveLayout() instead of this function for + more flexibility. + @see LoadPerspective @see LoadPaneInfo @see SavePaneInfo diff --git a/interface/wx/aui/serializer.h b/interface/wx/aui/serializer.h new file mode 100644 index 0000000000..896313fcc5 --- /dev/null +++ b/interface/wx/aui/serializer.h @@ -0,0 +1,202 @@ +///////////////////////////////////////////////////////////////////////////// +// Name: aui/serializer.h +// Purpose: Documentation of wxAuiSerializer and wxAuiDeserializer. +// Author: Vadim Zeitlin +// Created: 2024-02-20 +// Copyright: (c) 2024 Vadim Zeitlin +// Licence: wxWindows licence +///////////////////////////////////////////////////////////////////////////// + +/** + @class wxAuiSerializer + + wxAuiSerializer is used by wxAuiManager::SaveLayout() to store layout + information. + + This is an abstract base class, you need to inherit from it and override its + pure virtual functions in your derived class. + + SavePane() and SaveDock() must be overridden and will be called by + wxAuiManager for each pane and dock present in the layout. The other + functions don't need to be overridden, but it is often convenient to + perform some actions before or after starting to save the objects of the + given type or at the beginning or end of the whole saving process, so this + class provides hooks for doing it. + + If any of the functions of the derived class throw an exception, it is + propagated out of wxAuiManager::SaveLayout() and it's callers responsibility + to handle it. + + @library{wxaui} + @category{aui} + + @since 3.3.0 + */ +class wxAuiSerializer +{ +public: + /// Trivial default ctor. + wxAuiSerializer() = default; + + /// Trivial but virtual destructor. + virtual ~wxAuiSerializer() = default; + + /** + Called before doing anything else. + + Does nothing by default. + */ + virtual void BeforeSave(); + + /** + Called before starting to save information about the panes. + + Does nothing by default. + */ + virtual void BeforeSavePanes(); + + /** + Save information about the given pane. + + This function will be called for all panes and must be implemented to + save their data in a format from which it can be restored later using a + matching wxAuiDeserializer implementation. + + Note that all sizes and positions in @a pane are using DIPs, i.e. + resolution-independent pixels, when it is passed to this function, so + it does _not_ need to perform any scaling itself to ensure that the + stored values are restored correctly if the resolution changes. + */ + virtual void SavePane(const wxAuiPaneInfo& pane) = 0; + + /** + Called after the last call to SavePane(). + + Does nothing by default. + */ + virtual void AfterSavePanes(); + + /** + Called before starting to save information about the docks. + + Does nothing by default. + */ + virtual void BeforeSaveDocks(); + + /** + Save information about the given dock. + + This function will be called for all docks and must be implemented to + save their data in a format from which it can be restored later using a + matching wxAuiDeserializer implementation. + + As with SavePane(), the coordinates in @a dock are always in DIPs and + this function does _not_ need to perform any scaling itself. + */ + virtual void SaveDock(const wxAuiDockInfo& dock) = 0; + + /** + Called after the last call to SaveDock(). + + Does nothing by default. + */ + virtual void AfterSaveDocks(); + + /** + Called after saving everything. + + Does nothing by default. + */ + virtual void AfterSave(); +}; + +/** + @class wxAuiDeserializer + + wxAuiDeserializer is used by wxAuiManager::LoadLayout() to restore layout + information saved by wxAuiManager::SaveLayout(). + + As wxAuiSerializer, this is an abstract base class, you need to inherit from + it and override its pure virtual functions in your derived class. + + Derived class function also may throw and, if any of them other than + AfterLoad() does, the existing layout is not changed, i.e. + wxAuiManager::LoadLayout() is exception-safe. + + @library{wxaui} + @category{aui} + + @since 3.3.0 + */ +class wxAuiDeserializer +{ +public: + /** + Constructor taking the manager for which we're restoring the layout. + + The manager remains valid for the lifetime of this object. + */ + explicit wxAuiDeserializer(wxAuiManager& manager); + + /// Trivial but virtual destructor. + virtual ~wxAuiDeserializer() = default; + + + /** + Called before doing anything else. + + Does nothing by default. + */ + virtual void BeforeLoad(); + + /** + Load information about all the panes previously saved by + wxAuiSerializer::SavePane(). + + Unlike the serializer function, this one is called only once and should + return all the panes in the layout. + + Just as the serializer function, this one doesn't need to perform any + scaling itself as this will be done, if necessary, by wxAuiManager + itself. + + If some pane in the returned vector doesn't already exist, i.e. there + is no pane with the matching name, CreatePaneWindow() is called to + allow creating it on the fly. + */ + virtual std::vector LoadPanes() = 0; + + /** + Create the window to be managed by the given pane if necessary. + + This function 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. + + If this function returns @NULL, as it does by default, the pane is not + added to the manager. + */ + virtual wxWindow* CreatePaneWindow(const wxAuiPaneInfo& pane); + + /** + Load information about all the docks previously saved with SaveDock(). + + As with LoadPanes(), this function doesn't need to perform any scaling + itself. + */ + virtual std::vector LoadDocks() = 0; + + /** + Called after restoring everything. + + Default implementation calls wxAuiManager::Update(). Override this + function and do _not_ call the base class version if you want to + prevent this from happening, e.g. if you need to make further changes + to the restored layout before updating it. + */ + virtual void AfterLoad(); + +protected: + /// The manager for which we're restoring the layout. + wxAuiManager& m_manager; +}; diff --git a/samples/aui/auidemo.cpp b/samples/aui/auidemo.cpp index ee15ce1c62..2db802586f 100644 --- a/samples/aui/auidemo.cpp +++ b/samples/aui/auidemo.cpp @@ -28,6 +28,7 @@ #include "wx/log.h" #include "wx/menu.h" #include "wx/toolbar.h" +#include "wx/sstream.h" #include "wx/statusbr.h" #include "wx/msgdlg.h" #include "wx/textdlg.h" @@ -35,6 +36,10 @@ #include "wx/checkbox.h" #include "wx/aui/aui.h" +#include "wx/aui/serializer.h" + +#include "wx/xml/xml.h" + #include "../sample.xpm" // -- application -- @@ -70,7 +75,8 @@ class MyFrame : public wxFrame ID_NotebookContent, ID_SizeReportContent, ID_CreatePerspective, - ID_CopyPerspectiveCode, + ID_CopyLayout, + ID_PasteLayout, ID_AllowFloating, ID_AllowActivePane, ID_TransparentHint, @@ -149,7 +155,8 @@ private: void OnChangeContentPane(wxCommandEvent& evt); void OnDropDownToolbarItem(wxAuiToolBarEvent& evt); void OnCreatePerspective(wxCommandEvent& evt); - void OnCopyPerspectiveCode(wxCommandEvent& evt); + void OnCopyLayout(wxCommandEvent& evt); + void OnPasteLayout(wxCommandEvent& evt); void OnRestorePerspective(wxCommandEvent& evt); void OnSettings(wxCommandEvent& evt); void OnCustomizeToolbar(wxCommandEvent& evt); @@ -602,7 +609,8 @@ wxBEGIN_EVENT_TABLE(MyFrame, wxFrame) EVT_MENU(MyFrame::ID_CreateSizeReport, MyFrame::OnCreateSizeReport) EVT_MENU(MyFrame::ID_CreateNotebook, MyFrame::OnCreateNotebook) EVT_MENU(MyFrame::ID_CreatePerspective, MyFrame::OnCreatePerspective) - EVT_MENU(MyFrame::ID_CopyPerspectiveCode, MyFrame::OnCopyPerspectiveCode) + EVT_MENU(MyFrame::ID_CopyLayout, MyFrame::OnCopyLayout) + EVT_MENU(MyFrame::ID_PasteLayout, MyFrame::OnPasteLayout) EVT_MENU(ID_AllowFloating, MyFrame::OnManagerFlag) EVT_MENU(ID_TransparentHint, MyFrame::OnManagerFlag) EVT_MENU(ID_VenetianBlindsHint, MyFrame::OnManagerFlag) @@ -764,7 +772,8 @@ MyFrame::MyFrame(wxWindow* parent, m_perspectives_menu = new wxMenu; m_perspectives_menu->Append(ID_CreatePerspective, _("Create Perspective")); - m_perspectives_menu->Append(ID_CopyPerspectiveCode, _("Copy Perspective Data To Clipboard")); + m_perspectives_menu->Append(ID_CopyLayout, _("Copy Layout to Clipboard as XML\tCtrl-C")); + m_perspectives_menu->Append(ID_PasteLayout, _("Paste XML Layout from Clipboard\tCtrl-V")); m_perspectives_menu->AppendSeparator(); m_perspectives_menu->Append(ID_FirstPerspective+0, _("Default Startup")); m_perspectives_menu->Append(ID_FirstPerspective+1, _("All Panes")); @@ -1452,16 +1461,384 @@ void MyFrame::OnCreatePerspective(wxCommandEvent& WXUNUSED(event)) m_perspectives.Add(m_mgr.SavePerspective()); } -void MyFrame::OnCopyPerspectiveCode(wxCommandEvent& WXUNUSED(evt)) +// Sample serializer and deserializer implementations for saving and loading layouts. +class MyXmlSerializer : public wxAuiSerializer { - wxString s = m_mgr.SavePerspective(); +public: + MyXmlSerializer() = default; + + wxString GetXML() const + { + wxStringOutputStream oss; + if ( !m_doc.Save(oss) ) + { + wxLogError("Failed to save XML document."); + } + + return oss.GetString(); + } + + // Implement wxAuiSerializer methods. + virtual void BeforeSave() override + { + m_root = new wxXmlNode(wxXML_ELEMENT_NODE, "aui-layout"); + m_root->AddAttribute("version", "3.3.0"); + + m_doc.SetRoot(m_root); + } + + virtual void BeforeSavePanes() override + { + m_panes = new wxXmlNode(wxXML_ELEMENT_NODE, "panes"); + } + + virtual void SavePane(const wxAuiPaneInfo& pane) override + { + auto node = new wxXmlNode(m_panes, wxXML_ELEMENT_NODE, "pane"); + node->AddAttribute("name", pane.name); + + AddChild(node, "caption", pane.caption); + AddChild(node, "state", pane.state); + 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); + AddChild(node, "best-size", pane.best_size); + AddChild(node, "min-size", pane.min_size); + AddChild(node, "max-size", pane.max_size); + AddChild(node, "floating-rect", + wxRect(pane.floating_pos, pane.floating_size)); + } + + virtual void AfterSavePanes() override + { + m_root->AddChild(m_panes); + } + + virtual void BeforeSaveDocks() override + { + m_docks = new wxXmlNode(wxXML_ELEMENT_NODE, "docks"); + } + + virtual void SaveDock(const wxAuiDockInfo& dock) override + { + auto node = new wxXmlNode(m_docks, wxXML_ELEMENT_NODE, "dock"); + node->AddAttribute("resizable", dock.resizable ? "1" : "0"); + + AddChild(node, "direction", dock.dock_direction); + AddChild(node, "layer", dock.dock_layer); + AddChild(node, "row", dock.dock_row); + AddChild(node, "size", dock.size); + if ( dock.min_size ) + AddChild(node, "min-size", dock.min_size); + } + + virtual void AfterSaveDocks() override + { + m_root->AddChild(m_docks); + } + + virtual void AfterSave() override {} + +private: + void AddChild(wxXmlNode* parent, const wxString& name, const wxString& value) + { + auto node = new wxXmlNode(parent, wxXML_ELEMENT_NODE, name); + node->AddChild(new wxXmlNode(wxXML_TEXT_NODE, {}, value)); + } + + void AddChild(wxXmlNode* parent, const wxString& name, int value) + { + AddChild(parent, name, wxString::Format("%u", value)); + } + + void AddChild(wxXmlNode* parent, const wxString& name, const wxSize& size) + { + if ( size != wxDefaultSize ) + AddChild(parent, name, wxString::Format("%dx%d", size.x, size.y)); + } + + void AddChild(wxXmlNode* parent, const wxString& name, const wxRect& rect) + { + if ( rect.GetPosition() != wxDefaultPosition || + rect.GetSize() != wxDefaultSize ) + { + AddChild(parent, name, + wxString::Format("%d,%d %dx%d", + rect.x, rect.y, rect.width, rect.height)); + } + } + + wxXmlDocument m_doc; + wxXmlNode* m_root = nullptr; + wxXmlNode* m_panes = nullptr; + wxXmlNode* m_docks = nullptr; +}; + +class MyXmlDeserializer : public wxAuiDeserializer +{ +public: + explicit MyXmlDeserializer(wxAuiManager& manager, const wxString& xml) + : wxAuiDeserializer(manager) + { + wxStringInputStream iss(xml); + if ( !m_doc.Load(iss) ) + throw std::runtime_error("Failed to load XML document."); + + const auto root = m_doc.GetDocumentNode(); + const auto layout = root->GetChildren(); + if ( !layout ) + throw std::runtime_error("Missing layout node"); + if ( layout->GetName() != "aui-layout" ) + throw std::runtime_error("Unexpected XML node name"); + if ( layout->GetAttribute("version") != "3.3.0" ) + throw std::runtime_error("Unexpected XML version"); + if ( layout->GetNext() ) + throw std::runtime_error("Unexpected multiple layout nodes"); + + for ( wxXmlNode* node = layout->GetChildren(); node; node = node->GetNext() ) + { + if ( node->GetName() == "panes" ) + { + if ( m_panes ) + throw std::runtime_error("Unexpected multiple panes nodes"); + + m_panes = node; + } + else if ( node->GetName() == "docks" ) + { + if ( m_docks ) + throw std::runtime_error("Unexpected multiple docks nodes"); + + m_docks = node; + } + else + { + throw std::runtime_error("Unexpected node name"); + } + } + + if ( !m_panes ) + throw std::runtime_error("Missing panes node"); + + if ( !m_docks ) + throw std::runtime_error("Missing docks node"); + } + + // Implement wxAuiDeserializer methods. + virtual std::vector LoadPanes() override + { + std::vector panes; + + for ( wxXmlNode* node = m_panes->GetChildren(); node; node = node->GetNext() ) + { + if ( node->GetName() != "pane" ) + throw std::runtime_error("Unexpected pane node name"); + + { + wxAuiPaneInfo pane; + pane.name = node->GetAttribute("name"); + + for ( wxXmlNode* child = node->GetChildren(); child; child = child->GetNext() ) + { + const wxString& name = child->GetName(); + const wxString& content = child->GetNodeContent(); + + if ( name == "caption" ) + { + pane.caption = content; + } + else if ( name == "state" ) + { + pane.state = wxAuiPaneInfo::wxAuiPaneState(GetInt(content)); + } + else 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 == "best-size" ) + { + pane.best_size = GetSize(content); + } + else if ( name == "min-size" ) + { + pane.min_size = GetSize(content); + } + else if ( name == "max-size" ) + { + pane.max_size = GetSize(content); + } + else if ( name == "floating-rect" ) + { + auto rect = GetRect(content); + + pane.floating_pos = rect.GetPosition(); + pane.floating_size = rect.GetSize(); + } + else + { + throw std::runtime_error("Unexpected pane child node name"); + } + } + + panes.push_back(pane); + } + } + + return panes; + } + + virtual wxWindow* CreatePaneWindow(const wxAuiPaneInfo& pane) override + { + wxLogWarning("Unknown pane \"%s\"", pane.name); + return nullptr; + } + + virtual std::vector LoadDocks() override + { + std::vector docks; + + for ( wxXmlNode* node = m_docks->GetChildren(); node; node = node->GetNext() ) + { + if ( node->GetName() != "dock" ) + throw std::runtime_error("Unexpected dock node name"); + + wxAuiDockInfo dock; + if ( node->GetAttribute("resizable") == "1" ) + dock.resizable = true; + + for ( wxXmlNode* child = node->GetChildren(); child; child = child->GetNext() ) + { + const wxString& name = child->GetName(); + const wxString& content = child->GetNodeContent(); + + if ( name == "direction" ) + { + dock.dock_direction = GetInt(content); + } + else if ( name == "layer" ) + { + dock.dock_layer = GetInt(content); + } + else if ( name == "row" ) + { + dock.dock_row = GetInt(content); + } + else if ( name == "size" ) + { + dock.size = GetInt(content); + } + else if ( name == "min-size" ) + { + dock.min_size = GetInt(content); + } + else + { + throw std::runtime_error("Unexpected dock child node name"); + } + } + + docks.push_back(dock); + } + + return docks; + } + +private: + int GetInt(const wxString& str) + { + int value; + if ( !str.ToInt(&value) ) + throw std::runtime_error("Failed to parse integer"); + + return value; + } + + wxSize GetSize(const wxString& str) + { + wxString strH; + const wxString strW = str.BeforeFirst('x', &strH); + + unsigned int w, h; + if ( !strW.ToUInt(&w) || !strH.ToUInt(&h) ) + throw std::runtime_error("Failed to parse size"); + + return wxSize(w, h); + } + + wxRect GetRect(const wxString& str) + { + wxString strWH; + const wxString strXY = str.BeforeFirst(' ', &strWH); + + wxString strY; + const wxString strX = strXY.BeforeFirst(',', &strY); + + unsigned int x, y; + if ( !strX.ToUInt(&x) || !strY.ToUInt(&y) ) + throw std::runtime_error("Failed to parse position"); + + return wxRect(wxPoint(x, y), GetSize(strWH)); + } + + + wxXmlDocument m_doc; + wxXmlNode* m_panes = nullptr; + wxXmlNode* m_docks = nullptr; +}; + +void MyFrame::OnCopyLayout(wxCommandEvent& WXUNUSED(evt)) +{ + MyXmlSerializer ser; + m_mgr.SaveLayout(ser); #if wxUSE_CLIPBOARD - if (wxTheClipboard->Open()) + wxClipboardLocker clipLock; + + wxTheClipboard->SetData(new wxTextDataObject(ser.GetXML())); +#endif // wxUSE_CLIPBOARD +} + +void MyFrame::OnPasteLayout(wxCommandEvent& WXUNUSED(evt)) +{ +#if wxUSE_CLIPBOARD + wxClipboardLocker clipLock; + + wxTextDataObject data; + if ( !wxTheClipboard->GetData(data) ) { - wxTheClipboard->SetData(new wxTextDataObject(s)); - wxTheClipboard->Close(); + wxLogError("Failed to get XML data from clipboard."); + return; } + + try + { + MyXmlDeserializer deser(m_mgr, data.GetText()); + m_mgr.LoadLayout(deser); + } + catch ( const std::exception& e ) + { + wxLogError("Failed to load layout: %s", e.what()); + } +#else + wxLogError("This menu command requires wxUSE_CLIPBOARD."); #endif } diff --git a/src/aui/framemanager.cpp b/src/aui/framemanager.cpp index d972f40adf..aaf5023b30 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/serializer.h" #include "wx/mdi.h" #include "wx/wupdlock.h" @@ -1340,6 +1341,205 @@ bool wxAuiManager::LoadPerspective(const wxString& layout, bool update) return true; } +// These helper functions are used by SaveLayout() and LoadLayout() below, as +// we save the panes and docks geometries using DIPs on all platforms in order +// to ensure that they're restored correctly if the display DPI changes between +// saving and restoring the layout even on the platforms not using DIPs. +namespace +{ + +void MakeDIP(wxWindow* w, wxPoint& pos) +{ + pos = w->ToDIP(pos); +} + +void MakeDIP(wxWindow* w, wxSize& size) +{ + size = w->ToDIP(size); +} + +void MakeDIP(wxWindow* w, wxRect& rect) +{ + rect = wxRect{w->ToDIP(rect.GetPosition()), w->ToDIP(rect.GetSize())}; +} + +void MakeLogical(wxWindow* w, wxPoint& pos) +{ + pos = w->FromDIP(pos); +} + +void MakeLogical(wxWindow* w, wxSize& size) +{ + size = w->FromDIP(size); +} + +void MakeLogical(wxWindow* w, wxRect& rect) +{ + rect = wxRect{w->FromDIP(rect.GetPosition()), w->FromDIP(rect.GetSize())}; +} + +} // anonymous namespace + +void wxAuiManager::SaveLayout(wxAuiSerializer& serializer) const +{ + serializer.BeforeSave(); + + if ( !m_panes.empty() ) + { + serializer.BeforeSavePanes(); + + for ( const auto& pane : m_panes ) + { + auto paneDIP = pane; + + MakeDIP(m_frame, paneDIP.best_size); + MakeDIP(m_frame, paneDIP.min_size); + MakeDIP(m_frame, paneDIP.max_size); + + MakeDIP(m_frame, paneDIP.floating_pos); + MakeDIP(m_frame, paneDIP.floating_size); + + MakeDIP(m_frame, paneDIP.rect); + + serializer.SavePane(paneDIP); + } + + serializer.AfterSavePanes(); + } + + if ( !m_docks.empty() ) + { + serializer.BeforeSaveDocks(); + + for ( const auto& dock : m_docks ) + { + auto dockDIP = dock; + + MakeDIP(m_frame, dockDIP.rect); + + // Update dock sizes to ensure that restoring this layout later + // restores the same geometry as is used now: if we didn't do it, + // panes would have their initial sizes. + switch ( dock.dock_direction ) + { + case wxAUI_DOCK_TOP: + case wxAUI_DOCK_BOTTOM: + dockDIP.size = dock.rect.height; + break; + + case wxAUI_DOCK_LEFT: + case wxAUI_DOCK_RIGHT: + dockDIP.size = dock.rect.width; + break; + + case wxAUI_DOCK_CENTER: + // Not clear what to do for this one, but it shouldn't + // matter as its size is determined by what remains + // available after positioning the rest of the elements, so + // don't do anything. + break; + + case wxAUI_DOCK_NONE: + wxFAIL_MSG("invalid dock direction"); + break; + } + + serializer.SaveDock(dockDIP); + } + + serializer.AfterSaveDocks(); + } + + serializer.AfterSave(); +} + +void wxAuiManager::LoadLayout(wxAuiDeserializer& deserializer) +{ + deserializer.BeforeLoad(); + + // Note that we don't reset m_hasMaximized here but use a local variable so + // as to avoid modifying m_hasMaximized in case one of the deserializer + // functions called below throws an exception. + bool hasMaximized = false; + + // Also keep local variables for the existing (and possibly updated) panes + // and the new ones for the same reason. + wxAuiPaneInfoArray panes = m_panes; + + struct NewPane + { + // In C++11 this ctor is required. + NewPane(wxWindow* window_, const wxAuiPaneInfo& info_) + : window(window_), info(info_) + { + } + + wxWindow* window = nullptr; + wxAuiPaneInfo info; + }; + std::vector newPanes; + + for ( const auto& paneDIP : deserializer.LoadPanes() ) + { + auto pane = paneDIP; + + MakeLogical(m_frame, pane.best_size); + MakeLogical(m_frame, pane.min_size); + MakeLogical(m_frame, pane.max_size); + + MakeLogical(m_frame, pane.floating_pos); + MakeLogical(m_frame, pane.floating_size); + + MakeLogical(m_frame, pane.rect); + + if ( pane.IsMaximized() ) + hasMaximized = true; + + // Find the pane with the same name in the existing layout. + bool found = false; + for ( auto& existingPane : panes ) + { + if ( existingPane.name == pane.name ) + { + // Update the existing pane with the restored layout. + existingPane.SafeSet(pane); + + found = true; + 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 ( const auto w = deserializer.CreatePaneWindow(pane) ) + newPanes.emplace_back(w, pane); + } + } + + wxAuiDockInfoArray docks; + for ( const auto& dockDIP : deserializer.LoadDocks() ) + { + auto dock = dockDIP; + + MakeLogical(m_frame, dock.rect); + + docks.push_back(dock); + } + + // After loading everything successfully, do update the internal variables. + m_hasMaximized = hasMaximized; + m_panes.swap(panes); + + for ( const auto& newPane : newPanes ) + AddPane(newPane.window, newPane.info); + + m_docks.swap(docks); + + deserializer.AfterLoad(); +} + void wxAuiManager::GetPanePositionsAndSizes(wxAuiDockInfo& dock, wxArrayInt& positions, wxArrayInt& sizes)