Browse Source

ViewArea session (#1762)

Le Tan 4 năm trước cách đây
mục cha
commit
1843ca5bfd

+ 3 - 3
src/core/buffer/buffer.cpp

@@ -25,7 +25,7 @@ Buffer::Buffer(const BufferParameters &p_parameters,
                QObject *p_parent)
     : QObject(p_parent),
       m_provider(p_parameters.m_provider),
-      c_id(generateBufferID()),
+      m_id(generateBufferID()),
       m_readOnly(m_provider->isReadOnly())
 {
     m_autoSaveTimer = new QTimer(this);
@@ -110,9 +110,9 @@ QString Buffer::getResourcePath() const
     return m_provider->getResourcePath();
 }
 
-ID Buffer::getID() const
+ID Buffer::getId() const
 {
-    return c_id;
+    return m_id;
 }
 
 const QString &Buffer::getContent() const

+ 2 - 2
src/core/buffer/buffer.h

@@ -87,7 +87,7 @@ namespace vnotex
         // Return nullptr if not available.
         QSharedPointer<File> getFile() const;
 
-        ID getID() const;
+        ID getId() const;
 
         // Get buffer content.
         // It may differ from the content on disk.
@@ -212,7 +212,7 @@ namespace vnotex
         bool isBackupFileOfBuffer(const QString &p_file) const;
 
         // Will be assigned uniquely once created.
-        const ID c_id = 0;
+        const ID m_id = 0;
 
         // Revision of contents.
         int m_revision = 0;

+ 13 - 0
src/core/coreconfig.cpp

@@ -49,6 +49,8 @@ void CoreConfig::init(const QJsonObject &p_app,
     }
 
     loadNoteManagement(appObj, userObj);
+
+    m_recoverLastSessionOnStartEnabled = READBOOL(QStringLiteral("recover_last_session_on_start"));
 }
 
 QJsonObject CoreConfig::toJson() const
@@ -58,6 +60,7 @@ QJsonObject CoreConfig::toJson() const
     obj[QStringLiteral("locale")] = m_locale;
     obj[QStringLiteral("shortcuts")] = saveShortcuts();
     obj[QStringLiteral("toolbar_icon_size")] = m_toolBarIconSize;
+    obj[QStringLiteral("recover_last_session_on_start")] = m_recoverLastSessionOnStartEnabled;
     return obj;
 }
 
@@ -149,3 +152,13 @@ const QStringList &CoreConfig::getExternalNodeExcludePatterns() const
 {
     return m_externalNodeExcludePatterns;
 }
+
+bool CoreConfig::isRecoverLastSessionOnStartEnabled() const
+{
+    return m_recoverLastSessionOnStartEnabled;
+}
+
+void CoreConfig::setRecoverLastSessionOnStartEnabled(bool p_enabled)
+{
+    updateConfig(m_recoverLastSessionOnStartEnabled, p_enabled, this);
+}

+ 6 - 0
src/core/coreconfig.h

@@ -63,6 +63,9 @@ namespace vnotex
 
         static const QStringList &getAvailableLocales();
 
+        bool isRecoverLastSessionOnStartEnabled() const;
+        void setRecoverLastSessionOnStartEnabled(bool p_enabled);
+
     private:
         void loadShortcuts(const QJsonObject &p_app, const QJsonObject &p_user);
 
@@ -84,6 +87,9 @@ namespace vnotex
 
         QStringList m_externalNodeExcludePatterns;
 
+        // Whether recover last session on start.
+        bool m_recoverLastSessionOnStartEnabled = true;
+
         static QStringList s_availableLocales;
     };
 } // ns vnotex

+ 3 - 0
src/core/fileopenparameters.h

@@ -26,6 +26,9 @@ namespace vnotex
         // If m_lineNumber > -1, it indicates the line to scroll to after opening the file.
         // 0-based.
         int m_lineNumber = -1;
+
+        // Whether always open a new window for file.
+        bool m_alwaysNewWindow = false;
     };
 }
 

+ 3 - 0
src/core/global.h

@@ -98,6 +98,9 @@ namespace vnotex
         FocusPreview,
         Invalid
     };
+
+    enum { InvalidViewSplitId = 0 };
+
 } // ns vnotex
 
 Q_DECLARE_OPERATORS_FOR_FLAGS(vnotex::FindOptions);

+ 9 - 3
src/core/iconfig.h

@@ -154,9 +154,7 @@ namespace vnotex
         }
 
         template <typename T>
-        static void updateConfig(T &p_cur,
-                                 const T &p_new,
-                                 IConfig *p_config)
+        static void updateConfig(T &p_cur, const T &p_new, IConfig *p_config)
         {
             if (p_cur == p_new) {
                 return;
@@ -167,6 +165,14 @@ namespace vnotex
             p_config->writeToSettings();
         }
 
+        template <typename T>
+        static void updateConfigWithoutCheck(T &p_cur, const T &p_new, IConfig *p_config)
+        {
+            ++p_config->m_revision;
+            p_cur = p_new;
+            p_config->writeToSettings();
+        }
+
         IConfig *m_topConfig = nullptr;
 
         QString m_sessionName;

+ 15 - 0
src/core/sessionconfig.cpp

@@ -64,6 +64,8 @@ void SessionConfig::init()
     m_exportOption.fromJson(sessionJobj[QStringLiteral("export_option")].toObject());
 
     m_searchOption.fromJson(sessionJobj[QStringLiteral("search_option")].toObject());
+
+    m_viewAreaSession = readByteArray(sessionJobj, QStringLiteral("viewarea_session"));
 }
 
 void SessionConfig::loadCore(const QJsonObject &p_session)
@@ -180,6 +182,7 @@ QJsonObject SessionConfig::toJson() const
     obj[QStringLiteral("state_geometry")] = saveStateAndGeometry();
     obj[QStringLiteral("export_option")] = m_exportOption.toJson();
     obj[QStringLiteral("search_option")] = m_searchOption.toJson();
+    writeByteArray(obj, QStringLiteral("viewarea_session"), m_viewAreaSession);
     return obj;
 }
 
@@ -308,3 +311,15 @@ void SessionConfig::loadStateAndGeometry(const QJsonObject &p_session)
     m_mainWindowStateGeometry.m_mainState = readByteArray(obj, QStringLiteral("main_window_state"));
     m_mainWindowStateGeometry.m_mainGeometry = readByteArray(obj, QStringLiteral("main_window_geometry"));
 }
+
+QByteArray SessionConfig::getViewAreaSessionAndClear()
+{
+    QByteArray bytes;
+    m_viewAreaSession.swap(bytes);
+    return bytes;
+}
+
+void SessionConfig::setViewAreaSession(const QByteArray &p_bytes)
+{
+    updateConfigWithoutCheck(m_viewAreaSession, p_bytes, this);
+}

+ 5 - 0
src/core/sessionconfig.h

@@ -91,6 +91,9 @@ namespace vnotex
         const SearchOption &getSearchOption() const;
         void setSearchOption(const SearchOption &p_option);
 
+        QByteArray getViewAreaSessionAndClear();
+        void setViewAreaSession(const QByteArray &p_obj);
+
     private:
         void loadCore(const QJsonObject &p_session);
 
@@ -129,6 +132,8 @@ namespace vnotex
         ExportOption m_exportOption;
 
         SearchOption m_searchOption;
+
+        QByteArray m_viewAreaSession;
     };
 } // ns vnotex
 

+ 2 - 1
src/data/core/vnotex.json

@@ -39,7 +39,8 @@
                     ".git"
                 ]
             }
-        }
+        },
+        "recover_last_session_on_start" : true
     },
     "editor" : {
         "core": {

+ 14 - 0
src/widgets/dialogs/settings/generalpage.cpp

@@ -69,6 +69,16 @@ void GeneralPage::setupUI()
                 this, &GeneralPage::pageIsChanged);
     }
 #endif
+
+    {
+        const QString label(tr("Recover last session on start"));
+        m_recoverLastSessionCheckBox = WidgetsFactory::createCheckBox(label, this);
+        m_recoverLastSessionCheckBox->setToolTip(tr("Recover last session (like buffers) on start of VNote"));
+        mainLayout->addRow(m_recoverLastSessionCheckBox);
+        addSearchItem(label, m_recoverLastSessionCheckBox->toolTip(), m_recoverLastSessionCheckBox);
+        connect(m_recoverLastSessionCheckBox, &QCheckBox::stateChanged,
+                this, &GeneralPage::pageIsChanged);
+    }
 }
 
 void GeneralPage::loadInternal()
@@ -92,6 +102,8 @@ void GeneralPage::loadInternal()
         int toTray = sessionConfig.getMinimizeToSystemTray();
         m_systemTrayCheckBox->setChecked(toTray > 0);
     }
+
+    m_recoverLastSessionCheckBox->setChecked(coreConfig.isRecoverLastSessionOnStartEnabled());
 }
 
 void GeneralPage::saveInternal()
@@ -113,6 +125,8 @@ void GeneralPage::saveInternal()
         // This will override the -1 state. That is fine.
         sessionConfig.setMinimizeToSystemTray(m_systemTrayCheckBox->isChecked());
     }
+
+    coreConfig.setRecoverLastSessionOnStartEnabled(m_recoverLastSessionCheckBox->isChecked());
 }
 
 QString GeneralPage::title() const

+ 2 - 0
src/widgets/dialogs/settings/generalpage.h

@@ -29,6 +29,8 @@ namespace vnotex
         QComboBox *m_openGLComboBox = nullptr;
 
         QCheckBox *m_systemTrayCheckBox = nullptr;
+
+        QCheckBox *m_recoverLastSessionCheckBox = nullptr;
     };
 }
 

+ 2 - 2
src/widgets/dialogs/settings/markdowneditorpage.cpp

@@ -145,8 +145,8 @@ QGroupBox *MarkdownEditorPage::setupReadGroup()
         m_zoomFactorSpinBox = WidgetsFactory::createDoubleSpinBox(box);
         m_zoomFactorSpinBox->setToolTip(tr("Zoom factor in read mode"));
 
-        m_zoomFactorSpinBox->setRange(0.25, 10);
-        m_zoomFactorSpinBox->setSingleStep(0.25);
+        m_zoomFactorSpinBox->setRange(0.1, 10);
+        m_zoomFactorSpinBox->setSingleStep(0.1);
 
         const QString label(tr("Zoom factor:"));
         layout->addRow(label, m_zoomFactorSpinBox);

+ 3 - 3
src/widgets/editors/markdownviewer.cpp

@@ -360,8 +360,8 @@ void MarkdownViewer::handleWebKeyPress(int p_key, bool p_ctrl, bool p_shift, boo
 void MarkdownViewer::zoomOut()
 {
     qreal factor = zoomFactor();
-    if (factor > 0.25) {
-        factor -= 0.25;
+    if (factor > 0.1) {
+        factor -= 0.1;
         setZoomFactor(factor);
         emit zoomFactorChanged(factor);
     }
@@ -370,7 +370,7 @@ void MarkdownViewer::zoomOut()
 void MarkdownViewer::zoomIn()
 {
     qreal factor = zoomFactor();
-    factor += 0.25;
+    factor += 0.1;
     setZoomFactor(factor);
     emit zoomFactorChanged(factor);
 }

+ 10 - 0
src/widgets/markdownviewwindow.cpp

@@ -935,3 +935,13 @@ void MarkdownViewWindow::openTwice(const QSharedPointer<FileOpenParameters> &p_p
     Q_ASSERT(!p_paras || !p_paras->m_newFile);
     handleFileOpenParameters(p_paras);
 }
+
+ViewWindowSession MarkdownViewWindow::saveSession() const
+{
+    auto session = ViewWindow::saveSession();
+    if (getBuffer()) {
+        session.m_lineNumber = isReadMode() ? adapter()->getTopLineNumber()
+                                            : m_editor->getCursorPosition().first;
+    }
+    return session;
+}

+ 2 - 0
src/widgets/markdownviewwindow.h

@@ -42,6 +42,8 @@ namespace vnotex
 
         void openTwice(const QSharedPointer<FileOpenParameters> &p_paras) Q_DECL_OVERRIDE;
 
+        ViewWindowSession saveSession() const Q_DECL_OVERRIDE;
+
     public slots:
         void handleEditorConfigChange() Q_DECL_OVERRIDE;
 

+ 9 - 0
src/widgets/textviewwindow.cpp

@@ -244,3 +244,12 @@ void TextViewWindow::handleFileOpenParameters(const QSharedPointer<FileOpenParam
         m_editor->scrollToLine(p_paras->m_lineNumber, true);
     }
 }
+
+ViewWindowSession TextViewWindow::saveSession() const
+{
+    auto session = ViewWindow::saveSession();
+    if (getBuffer()) {
+        session.m_lineNumber = m_editor->getCursorPosition().first;
+    }
+    return session;
+}

+ 2 - 0
src/widgets/textviewwindow.h

@@ -29,6 +29,8 @@ namespace vnotex
 
         void openTwice(const QSharedPointer<FileOpenParameters> &p_paras) Q_DECL_OVERRIDE;
 
+        ViewWindowSession saveSession() const Q_DECL_OVERRIDE;
+
     public slots:
         void handleEditorConfigChange() Q_DECL_OVERRIDE;
 

+ 214 - 8
src/widgets/viewarea.cpp

@@ -11,6 +11,7 @@
 #include <QTimer>
 #include <QApplication>
 #include <QSet>
+#include <QHash>
 
 #include "viewwindow.h"
 #include "mainwindow.h"
@@ -21,6 +22,7 @@
 #include <core/vnotex.h>
 #include <core/configmgr.h>
 #include <core/coreconfig.h>
+#include <core/sessionconfig.h>
 #include <core/fileopenparameters.h>
 #include <notebook/node.h>
 #include <notebook/notebook.h>
@@ -47,7 +49,9 @@ ViewArea::ViewArea(QWidget *p_parent)
                     return;
                 }
 
-                // TODO: save last opened files.
+                if (ConfigMgr::getInst().getCoreConfig().isRecoverLastSessionOnStartEnabled()) {
+                    saveSession();
+                }
 
                 bool ret = close(false);
                 if (!ret) {
@@ -61,6 +65,11 @@ ViewArea::ViewArea(QWidget *p_parent)
                 close(true);
             });
 
+    if (ConfigMgr::getInst().getCoreConfig().isRecoverLastSessionOnStartEnabled()) {
+        connect(mainWindow, &MainWindow::mainWindowStarted,
+                this, &ViewArea::loadSession);
+    }
+
     connect(&VNoteX::getInst(), &VNoteX::nodeAboutToMove,
             this, &ViewArea::handleNodeChange);
 
@@ -127,8 +136,6 @@ void ViewArea::setupUI()
 
     m_mainLayout = new QVBoxLayout(this);
     m_mainLayout->setContentsMargins(0, 0, 0, 0);
-
-    showSceneWidget();
 }
 
 QSize ViewArea::sizeHint() const
@@ -145,7 +152,10 @@ QSize ViewArea::sizeHint() const
 void ViewArea::openBuffer(Buffer *p_buffer, const QSharedPointer<FileOpenParameters> &p_paras)
 {
     // We allow multiple ViewWindows of the same buffer in different workspaces by default.
-    auto wins = findBufferInViewSplits(p_buffer);
+    QVector<ViewWindow *> wins;
+    if (!p_paras->m_alwaysNewWindow) {
+        wins = findBufferInViewSplits(p_buffer);
+    }
     if (wins.isEmpty()) {
         if (!m_currentSplit) {
             addFirstViewSplit();
@@ -193,12 +203,20 @@ QVector<ViewWindow *> ViewArea::findBufferInViewSplits(const Buffer *p_buffer) c
     return wins;
 }
 
-ViewSplit *ViewArea::createViewSplit(QWidget *p_parent)
+ViewSplit *ViewArea::createViewSplit(QWidget *p_parent, ID p_viewSplitId)
 {
     auto workspace = createWorkspace();
     m_workspaces.push_back(workspace);
 
-    auto split = new ViewSplit(m_workspaces, workspace, p_parent);
+    ID id = p_viewSplitId;
+    if (id == InvalidViewSplitId) {
+        id = m_nextViewSplitId++;
+    } else {
+        Q_ASSERT(p_viewSplitId >= m_nextViewSplitId);
+        m_nextViewSplitId = id + 1;
+    }
+
+    auto split = new ViewSplit(m_workspaces, workspace, id, p_parent);
     connect(split, &ViewSplit::viewWindowCloseRequested,
             this, [this](ViewWindow *p_win) {
                 closeViewWindow(p_win, false, true);
@@ -282,7 +300,13 @@ void ViewArea::addFirstViewSplit()
     hideSceneWidget();
     m_mainLayout->addWidget(split);
 
-    setCurrentViewSplit(split, false);
+    postFirstViewSplit();
+}
+
+void ViewArea::postFirstViewSplit()
+{
+    Q_ASSERT(!m_splits.isEmpty());
+    setCurrentViewSplit(m_splits.first(), false);
 
     emit viewSplitsCountChanged();
     checkCurrentViewWindowChange();
@@ -446,7 +470,7 @@ QSharedPointer<ViewWorkspace> ViewArea::createWorkspace()
     ID id = 1;
     QSet<ID> usedIds;
     for (auto ws : m_workspaces) {
-        usedIds.insert(ws->c_id);
+        usedIds.insert(ws->m_id);
     }
 
     while (true) {
@@ -996,3 +1020,185 @@ QList<Buffer *> ViewArea::getAllBuffersInViewSplits() const
 
     return bufferSet.values();
 }
+
+void ViewArea::loadSession()
+{
+    auto &sessionConfig = ConfigMgr::getInst().getSessionConfig();
+    auto sessionData = sessionConfig.getViewAreaSessionAndClear();
+
+    auto session = ViewAreaSession::deserialize(sessionData);
+
+    // Load widgets layout.
+    if (session.m_root.isEmpty()) {
+        showSceneWidget();
+    } else {
+        Q_ASSERT(m_splits.isEmpty());
+        if (session.m_root.m_type == ViewAreaSession::Node::Type::Splitter) {
+            // Splitter.
+            auto splitter = createSplitter(session.m_root.m_orientation, this);
+            m_mainLayout->addWidget(splitter);
+
+            loadSplitterFromSession(session.m_root, splitter);
+        } else {
+            // Just only one ViewSplit.
+            Q_ASSERT(session.m_root.m_type == ViewAreaSession::Node::Type::ViewSplit);
+            auto split = createViewSplit(this, session.m_root.m_viewSplitId);
+            m_splits.push_back(split);
+            m_mainLayout->addWidget(split);
+        }
+
+        QHash<ID, int> viewSplitToWorkspace;
+
+        setCurrentViewSplit(m_splits.first(), false);
+
+        // Load invisible workspace.
+        for (int i = 0; i < session.m_workspaces.size(); ++i) {
+            const auto &ws = session.m_workspaces[i];
+            if (ws.m_viewSplitId != InvalidViewSplitId) {
+                viewSplitToWorkspace.insert(ws.m_viewSplitId, i);
+                continue;
+            }
+
+            for (const auto &winSession : ws.m_viewWindows) {
+                openViewWindowFromSession(winSession);
+            }
+
+            // Check if there is any window.
+            if (m_currentSplit->getViewWindowCount() > 0) {
+                m_currentSplit->setCurrentViewWindow(ws.m_currentViewWindowIndex);
+
+                // New another workspace.
+                auto newWs = createWorkspace();
+                m_workspaces.push_back(newWs);
+                m_currentSplit->setWorkspace(newWs);
+            }
+        }
+
+        // Load visible workspace.
+        for (auto split : m_splits) {
+            setCurrentViewSplit(split, false);
+
+            auto it = viewSplitToWorkspace.find(split->getId());
+            Q_ASSERT(it != viewSplitToWorkspace.end());
+
+            const auto &ws = session.m_workspaces[it.value()];
+
+            for (const auto &winSession : ws.m_viewWindows) {
+                openViewWindowFromSession(winSession);
+            }
+
+            if (m_currentSplit->getViewWindowCount() > 0) {
+                m_currentSplit->setCurrentViewWindow(ws.m_currentViewWindowIndex);
+            }
+        }
+
+        postFirstViewSplit();
+
+        distributeViewSplits();
+    }
+}
+
+void ViewArea::saveSession() const
+{
+    ViewAreaSession session;
+    takeSnapshot(session);
+
+    auto &sessionConfig = ConfigMgr::getInst().getSessionConfig();
+    sessionConfig.setViewAreaSession(session.serialize());
+}
+
+static void takeSnapshotOfWidgetNodes(ViewAreaSession::Node &p_node, const QWidget *p_widget, QHash<ID, ID> &p_workspaceToViewSplit)
+{
+    p_node.clear();
+
+    // Splitter.
+    auto splitter = dynamic_cast<const QSplitter *>(p_widget);
+    if (splitter) {
+        p_node.m_type = ViewAreaSession::Node::Type::Splitter;
+        p_node.m_orientation = splitter->orientation();
+        p_node.m_children.resize(splitter->count());
+
+        for (int i = 0; i < p_node.m_children.size(); ++i) {
+            takeSnapshotOfWidgetNodes(p_node.m_children[i], splitter->widget(i), p_workspaceToViewSplit);
+        }
+
+        return;
+    }
+
+    // ViewSplit.
+    auto viewSplit = dynamic_cast<const ViewSplit *>(p_widget);
+    Q_ASSERT(viewSplit);
+    p_node.m_type = ViewAreaSession::Node::Type::ViewSplit;
+    p_node.m_viewSplitId = viewSplit->getId();
+
+    auto ws = viewSplit->getWorkspace();
+    if (ws) {
+        viewSplit->updateStateToWorkspace();
+        p_workspaceToViewSplit.insert(ws->m_id, viewSplit->getId());
+    }
+}
+
+
+void ViewArea::takeSnapshot(ViewAreaSession &p_session) const
+{
+    QHash<ID, ID> workspaceToViewSplit;
+
+    // Widget hirarchy.
+    p_session.m_root.clear();
+    if (!m_splits.isEmpty()) {
+        auto topWidget = m_mainLayout->itemAt(0)->widget();
+        takeSnapshotOfWidgetNodes(p_session.m_root, topWidget, workspaceToViewSplit);
+    }
+
+    // Workspaces.
+    p_session.m_workspaces.clear();
+    p_session.m_workspaces.reserve(m_workspaces.size());
+    for (const auto &ws : m_workspaces) {
+        p_session.m_workspaces.push_back(ViewAreaSession::Workspace());
+        auto &wsSnap = p_session.m_workspaces.last();
+        if (ws->m_visible) {
+            auto it = workspaceToViewSplit.find(ws->m_id);
+            Q_ASSERT(it != workspaceToViewSplit.end());
+            wsSnap.m_viewSplitId = it.value();
+        }
+        wsSnap.m_currentViewWindowIndex = ws->m_currentViewWindowIndex;
+        for (auto win : ws->m_viewWindows) {
+            wsSnap.m_viewWindows.push_back(win->saveSession());
+        }
+    }
+}
+
+void ViewArea::loadSplitterFromSession(const ViewAreaSession::Node &p_node, QSplitter *p_splitter)
+{
+    // @p_splitter is the splitter corresponding to @p_node.
+    Q_ASSERT(p_node.m_type == ViewAreaSession::Node::Type::Splitter);
+
+    for (const auto &child : p_node.m_children) {
+        if (child.m_type == ViewAreaSession::Node::Type::Splitter) {
+            auto childSplitter = createSplitter(child.m_orientation, p_splitter);
+            p_splitter->addWidget(childSplitter);
+
+            loadSplitterFromSession(child, childSplitter);
+        } else {
+            Q_ASSERT(child.m_type == ViewAreaSession::Node::Type::ViewSplit);
+            auto childSplit = createViewSplit(this, child.m_viewSplitId);
+            m_splits.push_back(childSplit);
+            p_splitter->addWidget(childSplit);
+        }
+    }
+}
+
+void ViewArea::openViewWindowFromSession(const ViewWindowSession &p_session)
+{
+    if (p_session.m_bufferPath.isEmpty()) {
+        return;
+    }
+
+    auto paras = QSharedPointer<FileOpenParameters>::create();
+    paras->m_mode = p_session.m_viewWindowMode;
+    paras->m_readOnly = p_session.m_readOnly;
+    paras->m_lineNumber = p_session.m_lineNumber;
+    paras->m_alwaysNewWindow = true;
+
+    emit VNoteX::getInst().openFileRequested(p_session.m_bufferPath, paras);
+}

+ 18 - 3
src/widgets/viewarea.h

@@ -10,6 +10,7 @@
 #include "global.h"
 #include "navigationmode.h"
 #include "viewsplit.h"
+#include "viewareasession.h"
 
 class QLayout;
 class QSplitter;
@@ -29,7 +30,7 @@ namespace vnotex
     struct ViewWorkspace
     {
         explicit ViewWorkspace(ID p_id)
-            : c_id(p_id)
+            : m_id(p_id)
         {
         }
 
@@ -45,7 +46,7 @@ namespace vnotex
             m_currentViewWindowIndex = 0;
         }
 
-        const ID c_id = 0;
+        const ID m_id = 0;
 
         // Whether it is displayed by a ViewSplit now.
         bool m_visible = false;
@@ -132,6 +133,10 @@ namespace vnotex
 
         void handleNodeChange(Node *p_node, const QSharedPointer<Event> &p_event);
 
+        void loadSession();
+
+        void saveSession() const;
+
     private:
         enum class SplitType
         {
@@ -145,7 +150,7 @@ namespace vnotex
         // Does not search invisible work spaces.
         QVector<ViewWindow *> findBufferInViewSplits(const Buffer *p_buffer) const;
 
-        ViewSplit *createViewSplit(QWidget *p_parent);
+        ViewSplit *createViewSplit(QWidget *p_parent, ID p_viewSplitId = InvalidViewSplitId);
 
         // A Scene widget will be used when there is no split.
         // Usually it is used to show some help message.
@@ -205,6 +210,14 @@ namespace vnotex
 
         QVector<ViewWindow *> getAllViewWindows(ViewSplit *p_split) const;
 
+        void takeSnapshot(ViewAreaSession &p_session) const;
+
+        void postFirstViewSplit();
+
+        void loadSplitterFromSession(const ViewAreaSession::Node &p_node, QSplitter *p_splitter);
+
+        void openViewWindowFromSession(const ViewWindowSession &p_session);
+
         QLayout *m_mainLayout = nullptr;
 
         QWidget *m_sceneWidget = nullptr;
@@ -226,6 +239,8 @@ namespace vnotex
 
         // Timer to check file change outside periodically.
         QTimer *m_fileCheckTimer = nullptr;
+
+        ID m_nextViewSplitId = InvalidViewSplitId + 1;
     };
 } // ns vnotex
 

+ 101 - 0
src/widgets/viewareasession.cpp

@@ -0,0 +1,101 @@
+#include "viewareasession.h"
+
+#include <QIODevice>
+#include <QDebug>
+#include <QSplitter>
+#include <QLayout>
+
+using namespace vnotex;
+
+QDataStream &::vnotex::operator<<(QDataStream &p_ds, const ViewAreaSession::Node &p_node)
+{
+    p_ds << static_cast<qint8>(p_node.m_type);
+    p_ds << static_cast<qint8>(p_node.m_orientation);
+    p_ds << p_node.m_viewSplitId;
+    p_ds << p_node.m_children;
+    return p_ds;
+}
+
+QDataStream &::vnotex::operator>>(QDataStream &p_ds, ViewAreaSession::Node &p_node)
+{
+    qint8 tmp = 0;
+
+    p_ds >> tmp;
+    p_node.m_type = static_cast<ViewAreaSession::Node::Type>(tmp);
+
+    p_ds >> tmp;
+    p_node.m_orientation = static_cast<Qt::Orientation>(tmp);
+
+    p_ds >> p_node.m_viewSplitId;
+    p_ds >> p_node.m_children;
+    return p_ds;
+}
+
+QDataStream &::vnotex::operator<<(QDataStream &p_ds, const ViewAreaSession::Workspace &p_workspace)
+{
+    p_ds << p_workspace.m_viewSplitId;
+
+    p_ds << p_workspace.m_viewWindows;
+
+    p_ds << static_cast<qint32>(p_workspace.m_currentViewWindowIndex);
+
+    return p_ds;
+}
+
+QDataStream &::vnotex::operator>>(QDataStream &p_ds, ViewAreaSession::Workspace &p_workspace)
+{
+    p_ds >> p_workspace.m_viewSplitId;
+
+    p_ds >> p_workspace.m_viewWindows;
+
+    {
+        qint32 tmp = 0;
+        p_ds >> tmp;
+        p_workspace.m_currentViewWindowIndex = tmp;
+    }
+
+    return p_ds;
+}
+
+void ViewAreaSession::Node::clear()
+{
+    m_type = Type::Empty;
+    m_orientation = Qt::Horizontal;
+    m_viewSplitId = InvalidViewSplitId;
+    m_children.clear();
+}
+
+bool ViewAreaSession::Node::isEmpty() const
+{
+    return m_type == Type::Empty;
+}
+
+QByteArray ViewAreaSession::serialize() const
+{
+    QByteArray data;
+    QDataStream outs(&data, QIODevice::WriteOnly);
+    outs.setVersion(QDataStream::Qt_5_12);
+
+    outs << m_root;
+
+    outs << m_workspaces;
+
+    return data;
+}
+
+ViewAreaSession ViewAreaSession::deserialize(const QByteArray &p_data)
+{
+    ViewAreaSession session;
+    if (p_data.isEmpty()) {
+        return session;
+    }
+
+    QDataStream ins(p_data);
+    ins.setVersion(QDataStream::Qt_5_12);
+
+    ins >> session.m_root;
+
+    ins >> session.m_workspaces;
+
+    return session;
+}

+ 66 - 0
src/widgets/viewareasession.h

@@ -0,0 +1,66 @@
+#ifndef VIEWAREASESSION_H
+#define VIEWAREASESSION_H
+
+#include <QVector>
+#include <QDataStream>
+#include <QHash>
+
+#include <core/global.h>
+
+#include "viewwindowsession.h"
+
+
+namespace vnotex
+{
+    struct ViewAreaSession
+    {
+        // A node for splitter and ViewSplit hirarchy.
+        struct Node
+        {
+            enum Type
+            {
+                Splitter,
+                ViewSplit,
+                Empty
+            };
+
+            void clear();
+
+            bool isEmpty() const;
+
+            Type m_type = Type::Empty;
+
+            Qt::Orientation m_orientation = Qt::Horizontal;
+
+            ID m_viewSplitId = InvalidViewSplitId;
+
+            QVector<Node> m_children;
+        };
+
+        struct Workspace
+        {
+            ID m_viewSplitId = InvalidViewSplitId;
+
+            QVector<ViewWindowSession> m_viewWindows;
+
+            int m_currentViewWindowIndex = 0;
+        };
+
+        QByteArray serialize() const;
+
+        static ViewAreaSession deserialize(const QByteArray &p_data);
+
+        Node m_root;
+
+        QVector<Workspace> m_workspaces;
+    };
+
+
+    extern QDataStream &operator<<(QDataStream &p_ds, const ViewAreaSession::Node &p_node);
+    extern QDataStream &operator>>(QDataStream &p_ds, ViewAreaSession::Node &p_node);
+
+    extern QDataStream &operator<<(QDataStream &p_ds, const ViewAreaSession::Workspace &p_workspace);
+    extern QDataStream &operator>>(QDataStream &p_ds, ViewAreaSession::Workspace &p_workspace);
+}
+
+#endif // VIEWAREASESSION_H

+ 33 - 8
src/widgets/viewsplit.cpp

@@ -42,8 +42,10 @@ const QString ViewSplit::c_actionButtonForegroundName = "widgets#viewsplit#actio
 
 ViewSplit::ViewSplit(const QVector<QSharedPointer<ViewWorkspace>> &p_allWorkspaces,
                      const QSharedPointer<ViewWorkspace> &p_workspace,
+                     ID p_id,
                      QWidget *p_parent)
     : QTabWidget(p_parent),
+      m_id(p_id),
       m_allWorkspaces(p_allWorkspaces)
 {
     setAcceptDrops(true);
@@ -258,17 +260,12 @@ void ViewSplit::setWorkspace(const QSharedPointer<ViewWorkspace> &p_workspace)
 void ViewSplit::updateAndTakeCurrentWorkspace()
 {
     if (m_workspace) {
-        // Store current workspace.
-        m_workspace->m_currentViewWindowIndex = currentIndex();
+        updateStateToWorkspace();
 
         // Take all the view windows out.
         int cnt = getViewWindowCount();
-        m_workspace->m_viewWindows.resize(cnt);
         for (int i = cnt - 1; i >= 0; --i) {
-            auto window = getViewWindow(i);
-            takeViewWindow(window);
-
-            m_workspace->m_viewWindows[i] = window;
+            takeViewWindow(getViewWindow(i));
         }
 
         m_workspace->m_visible = false;
@@ -279,6 +276,23 @@ void ViewSplit::updateAndTakeCurrentWorkspace()
     }
 }
 
+void ViewSplit::updateStateToWorkspace() const
+{
+    if (!m_workspace) {
+        return;
+    }
+
+    Q_ASSERT(m_workspace->m_visible);
+
+    m_workspace->m_currentViewWindowIndex = currentIndex();
+
+    int cnt = getViewWindowCount();
+    m_workspace->m_viewWindows.resize(cnt);
+    for (int i = cnt - 1; i >= 0; --i) {
+        m_workspace->m_viewWindows[i] = getViewWindow(i);
+    }
+}
+
 QVector<ViewWindow *> ViewSplit::findBuffer(const Buffer *p_buffer) const
 {
     QVector<ViewWindow *> wins;
@@ -441,7 +455,7 @@ void ViewSplit::updateMenu(QMenu *p_menu)
         }
 
         for (int i = 0; i < m_allWorkspaces.size(); ++i) {
-            auto act = new QAction(tr("Workspace %1").arg(m_allWorkspaces[i]->c_id),
+            auto act = new QAction(tr("Workspace %1").arg(m_allWorkspaces[i]->m_id),
                                    m_workspaceActionGroup);
             act->setData(i);
             act->setCheckable(true);
@@ -735,3 +749,14 @@ void ViewSplit::focus()
 {
     focusCurrentViewWindow();
 }
+
+ID ViewSplit::getId() const
+{
+    return m_id;
+}
+
+void ViewSplit::setCurrentViewWindow(int p_idx)
+{
+    auto win = getViewWindow(p_idx);
+    setCurrentViewWindow(win);
+}

+ 12 - 3
src/widgets/viewsplit.h

@@ -30,9 +30,10 @@ namespace vnotex
             ViewWindow *m_viewWindow = nullptr;
         };
 
-        explicit ViewSplit(const QVector<QSharedPointer<ViewWorkspace>> &p_allWorkspaces,
-                           const QSharedPointer<ViewWorkspace> &p_workspace,
-                           QWidget *p_parent = nullptr);
+        ViewSplit(const QVector<QSharedPointer<ViewWorkspace>> &p_allWorkspaces,
+                  const QSharedPointer<ViewWorkspace> &p_workspace,
+                  ID p_id,
+                  QWidget *p_parent = nullptr);
 
         ~ViewSplit();
 
@@ -45,6 +46,8 @@ namespace vnotex
         ViewWindow *getCurrentViewWindow() const;
         void setCurrentViewWindow(ViewWindow *p_win);
 
+        void setCurrentViewWindow(int p_idx);
+
         // @p_win is not deleted.
         void takeViewWindow(ViewWindow *p_win);
 
@@ -62,6 +65,10 @@ namespace vnotex
 
         void focus();
 
+        ID getId() const;
+
+        void updateStateToWorkspace() const;
+
     signals:
         void viewWindowCloseRequested(ViewWindow *p_win);
 
@@ -122,6 +129,8 @@ namespace vnotex
 
         void focusCurrentViewWindow();
 
+        ID m_id = 0;
+
         const QVector<QSharedPointer<ViewWorkspace>> &m_allWorkspaces;
 
         QSharedPointer<ViewWorkspace> m_workspace;

+ 11 - 0
src/widgets/viewwindow.cpp

@@ -1079,3 +1079,14 @@ QToolBar *ViewWindow::createToolBar(QWidget *p_parent)
     toolBar->setProperty(PropertyDefs::c_viewWindowToolBar, true);
     return toolBar;
 }
+
+ViewWindowSession ViewWindow::saveSession() const
+{
+    ViewWindowSession session;
+    if (m_buffer) {
+        session.m_bufferPath = m_buffer->getPath();
+        session.m_readOnly = m_buffer->isReadOnly();
+    }
+    session.m_viewWindowMode = getMode();
+    return session;
+}

+ 3 - 0
src/widgets/viewwindow.h

@@ -9,6 +9,7 @@
 #include <core/global.h>
 
 #include "viewwindowtoolbarhelper.h"
+#include "viewwindowsession.h"
 
 class QVBoxLayout;
 class QTimer;
@@ -71,6 +72,8 @@ namespace vnotex
         // Called by upside.
         void checkFileMissingOrChangedOutsidePeriodically();
 
+        virtual ViewWindowSession saveSession() const;
+
     public slots:
         virtual void handleEditorConfigChange() = 0;
 

+ 33 - 0
src/widgets/viewwindowsession.cpp

@@ -0,0 +1,33 @@
+#include "viewwindowsession.h"
+
+using namespace vnotex;
+
+QDataStream &::vnotex::operator<<(QDataStream &p_ds, const ViewWindowSession &p_session)
+{
+    p_ds << p_session.m_bufferPath;
+    p_ds << static_cast<qint8>(p_session.m_readOnly);
+    p_ds << static_cast<qint8>(p_session.m_viewWindowMode);
+    p_ds << static_cast<qint32>(p_session.m_lineNumber);
+    return p_ds;
+}
+
+QDataStream &::vnotex::operator>>(QDataStream &p_ds, ViewWindowSession &p_session)
+{
+    p_ds >> p_session.m_bufferPath;
+
+    qint8 tmp8 = 0;
+
+    p_ds >> tmp8;
+    p_session.m_readOnly = tmp8 > 0;
+
+    p_ds >> tmp8;
+    p_session.m_viewWindowMode = static_cast<ViewWindowMode>(tmp8);
+
+    {
+        qint32 tmp = 0;
+        p_ds >> tmp;
+        p_session.m_lineNumber = static_cast<int>(tmp);
+    }
+
+    return p_ds;
+}

+ 26 - 0
src/widgets/viewwindowsession.h

@@ -0,0 +1,26 @@
+#ifndef VIEWWINDOWSESSION_H
+#define VIEWWINDOWSESSION_H
+
+#include <QDataStream>
+
+#include <core/global.h>
+
+namespace vnotex
+{
+    struct ViewWindowSession
+    {
+        QString m_bufferPath;
+
+        bool m_readOnly = false;
+
+        ViewWindowMode m_viewWindowMode = ViewWindowMode::Read;
+
+        // 0-based.
+        int m_lineNumber = -1;
+    };
+
+    extern QDataStream &operator<<(QDataStream &p_ds, const ViewWindowSession &p_session);
+    extern QDataStream &operator>>(QDataStream &p_ds, ViewWindowSession &p_session);
+}
+
+#endif // VIEWWINDOWSESSION_H

+ 4 - 0
src/widgets/widgets.pri

@@ -57,8 +57,10 @@ SOURCES += \
     $$PWD/textviewwindow.cpp \
     $$PWD/toolbarhelper.cpp \
     $$PWD/treeview.cpp \
+    $$PWD/viewareasession.cpp \
     $$PWD/viewsplit.cpp \
     $$PWD/viewwindow.cpp \
+    $$PWD/viewwindowsession.cpp \
     $$PWD/viewwindowtoolbarhelper.cpp \
     $$PWD/webpage.cpp \
     $$PWD/webviewer.cpp \
@@ -149,8 +151,10 @@ HEADERS += \
     $$PWD/textviewwindowhelper.h \
     $$PWD/toolbarhelper.h \
     $$PWD/treeview.h \
+    $$PWD/viewareasession.h \
     $$PWD/viewsplit.h \
     $$PWD/viewwindow.h \
+    $$PWD/viewwindowsession.h \
     $$PWD/viewwindowtoolbarhelper.h \
     $$PWD/webpage.h \
     $$PWD/webviewer.h \