Browse Source

support spliting edit windows

Signed-off-by: Le Tan <[email protected]>
Le Tan 9 năm trước cách đây
mục cha
commit
915c25e1a5

+ 11 - 0
src/resources/icons/corner_menu.svg

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="512px" height="512px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
+<g id="Icon_3_">
+	<g>
+		<path d="M64,384h384v-42.666H64V384z M64,277.334h384v-42.667H64V277.334z M64,128v42.665h384V128H64z"/>
+	</g>
+</g>
+</svg>

+ 10 - 0
src/resources/icons/remove_split.svg

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="512px" height="512px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
+<path d="M458,210.409l-145.267-12.476L256,64l-56.743,133.934L54,210.409l110.192,95.524L131.161,448L256,372.686L380.83,448
+	l-33.021-142.066L458,210.409z M272.531,345.286L256,335.312l-16.53,9.973l-59.988,36.191l15.879-68.296l4.369-18.79l-14.577-12.637
+	l-52.994-45.939l69.836-5.998l19.206-1.65l7.521-17.75l27.276-64.381l27.27,64.379l7.52,17.751l19.208,1.65l69.846,5.998
+	l-52.993,45.939l-14.576,12.636l4.367,18.788l15.875,68.299L272.531,345.286z"/>
+</svg>

+ 9 - 0
src/resources/icons/split_window.svg

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="512px" height="512px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
+<path d="M458,210.409l-145.267-12.476L256,64l-56.743,133.934L54,210.409l110.192,95.524L131.161,448L256,372.686L380.83,448
+	l-33.021-142.066L458,210.409z M272.531,345.287L256,335.313l-0.002-189.277l27.27,64.379l7.52,17.751l19.208,1.65l69.846,5.998
+	l-52.993,45.939l-14.576,12.636l4.367,18.788l15.875,68.299L272.531,345.287z"/>
+</svg>

+ 6 - 4
src/src.pro

@@ -21,7 +21,6 @@ SOURCES += main.cpp\
     vconfigmanager.cpp \
     vfilelist.cpp \
     dialog/vnewfiledialog.cpp \
-    vtabwidget.cpp \
     vedit.cpp \
     veditor.cpp \
     vnotefile.cpp \
@@ -40,7 +39,9 @@ SOURCES += main.cpp\
     veditoperations.cpp \
     vmdeditoperations.cpp \
     dialog/vinsertimagedialog.cpp \
-    vdownloader.cpp
+    vdownloader.cpp \
+    veditarea.cpp \
+    veditwindow.cpp
 
 HEADERS  += vmainwindow.h \
     vdirectorytree.h \
@@ -50,7 +51,6 @@ HEADERS  += vmainwindow.h \
     vconfigmanager.h \
     vfilelist.h \
     dialog/vnewfiledialog.h \
-    vtabwidget.h \
     vedit.h \
     veditor.h \
     vconstants.h \
@@ -71,7 +71,9 @@ HEADERS  += vmainwindow.h \
     veditoperations.h \
     vmdeditoperations.h \
     dialog/vinsertimagedialog.h \
-    vdownloader.h
+    vdownloader.h \
+    veditarea.h \
+    veditwindow.h
 
 RESOURCES += \
     vnote.qrc

+ 274 - 0
src/veditarea.cpp

@@ -0,0 +1,274 @@
+#include <QtWidgets>
+#include "veditarea.h"
+#include "veditwindow.h"
+#include "veditor.h"
+#include "vnote.h"
+#include "vconfigmanager.h"
+
+VEditArea::VEditArea(VNote *vnote, QWidget *parent)
+    : QWidget(parent), vnote(vnote), curWindowIndex(0)
+{
+    setupUI();
+}
+
+void VEditArea::setupUI()
+{
+    splitter = new QSplitter(this);
+
+    // Add a window
+    insertSplitWindow(0);
+    setCurrentWindow(0, true);
+
+    QHBoxLayout *mainLayout = new QHBoxLayout();
+    mainLayout->addWidget(splitter);
+
+    setLayout(mainLayout);
+}
+
+void VEditArea::insertSplitWindow(int idx)
+{
+    VEditWindow *win = new VEditWindow(vnote);
+    splitter->insertWidget(idx, win);
+    connect(win, &VEditWindow::tabStatusChanged,
+            this, &VEditArea::curTabStatusChanged);
+    connect(win, &VEditWindow::requestSplitWindow,
+            this, &VEditArea::handleSplitWindowRequest);
+    connect(win, &VEditWindow::requestRemoveSplit,
+            this, &VEditArea::handleRemoveSplitRequest);
+    connect(win, &VEditWindow::getFocused,
+            this, &VEditArea::handleWindowFocused);
+
+    int nrWin = splitter->count();
+    if (nrWin == 1) {
+        // Disable removing split
+        win->setRemoveSplitEnable(false);
+    } else {
+        // Enable removing split
+        for (int i = 0; i < nrWin; ++i) {
+            getWindow(i)->setRemoveSplitEnable(true);
+        }
+    }
+}
+
+void VEditArea::removeSplitWindow(VEditWindow *win)
+{
+    if (!win) {
+        return;
+    }
+    win->hide();
+    // Should be deleted later
+    win->deleteLater();
+
+    int nrWin = splitter->count();
+    if (nrWin == 2) {
+        // Disable removing split
+        getWindow(0)->setRemoveSplitEnable(false);
+        getWindow(1)->setRemoveSplitEnable(false);
+    } else {
+        // Enable removing split
+        for (int i = 0; i < nrWin; ++i) {
+            getWindow(i)->setRemoveSplitEnable(true);
+        }
+    }
+}
+
+// A given file can be opened in multiple split windows. A given file could be
+// opened at most in one tab inside a window.
+void VEditArea::openFile(QJsonObject fileJson)
+{
+    if (fileJson.isEmpty()) {
+        return;
+    }
+
+    QString notebook = fileJson["notebook"].toString();
+    QString relativePath = fileJson["relative_path"].toString();
+    int mode = OpenFileMode::Read;
+    if (fileJson.contains("mode")) {
+        mode = fileJson["mode"].toInt();
+    }
+
+    qDebug() << "open notebook" << notebook << "path" << relativePath << mode;
+
+    // Find if it has been opened already
+    int winIdx, tabIdx;
+    bool setFocus = false;
+    auto tabs = findTabsByFile(notebook, relativePath);
+    if (!tabs.empty()) {
+        // Current window first
+        winIdx = tabs[0].first;
+        tabIdx = tabs[0].second;
+        for (int i = 0; i < tabs.size(); ++i) {
+            if (tabs[i].first == curWindowIndex) {
+                winIdx = tabs[i].first;
+                tabIdx = tabs[i].second;
+                break;
+            }
+        }
+        setFocus = true;
+        goto out;
+    }
+
+    // Open it in current window
+    winIdx = curWindowIndex;
+    tabIdx = openFileInWindow(winIdx, notebook, relativePath, mode);
+
+out:
+    setCurrentTab(winIdx, tabIdx, setFocus);
+}
+
+QVector<QPair<int, int> > VEditArea::findTabsByFile(const QString &notebook, const QString &relativePath)
+{
+    QVector<QPair<int, int> > tabs;
+    int nrWin = splitter->count();
+    for (int winIdx = 0; winIdx < nrWin; ++winIdx) {
+        VEditWindow *win = getWindow(winIdx);
+        int tabIdx = win->findTabByFile(notebook, relativePath);
+        if (tabIdx != -1) {
+            QPair<int, int> match;
+            match.first = winIdx;
+            match.second = tabIdx;
+            tabs.append(match);
+        }
+    }
+    return tabs;
+}
+
+int VEditArea::openFileInWindow(int windowIndex, const QString &notebook, const QString &relativePath,
+                                int mode)
+{
+    Q_ASSERT(windowIndex < splitter->count());
+    VEditWindow *win = getWindow(windowIndex);
+    return win->openFile(notebook, relativePath, mode);
+}
+
+void VEditArea::setCurrentTab(int windowIndex, int tabIndex, bool setFocus)
+{
+    setCurrentWindow(windowIndex, setFocus);
+
+    VEditWindow *win = getWindow(windowIndex);
+    win->setCurrentIndex(tabIndex);
+}
+
+void VEditArea::setCurrentWindow(int windowIndex, bool setFocus)
+{
+    if (curWindowIndex == windowIndex) {
+        return;
+    }
+    qDebug() << "current window" << windowIndex;
+    curWindowIndex = windowIndex;
+    if (setFocus) {
+        getWindow(windowIndex)->focusWindow();
+    }
+
+    // Update tab status
+    QString notebook, relativePath;
+    bool editMode, modifiable;
+    getWindow(curWindowIndex)->getTabStatus(notebook, relativePath, editMode, modifiable);
+    emit curTabStatusChanged(notebook, relativePath, editMode, modifiable);
+}
+
+void VEditArea::closeFile(QJsonObject fileJson)
+{
+    if (fileJson.isEmpty()) {
+        return;
+    }
+    QString notebook = fileJson["notebook"].toString();
+    QString relativePath = fileJson["relative_path"].toString();
+
+    int nrWin = splitter->count();
+    for (int i = 0; i < nrWin; ++i) {
+        VEditWindow *win = getWindow(i);
+        win->closeFile(notebook, relativePath);
+    }
+}
+
+void VEditArea::editFile()
+{
+    VEditWindow *win = getWindow(curWindowIndex);
+    win->editFile();
+}
+
+void VEditArea::saveFile()
+{
+    VEditWindow *win = getWindow(curWindowIndex);
+    win->saveFile();
+}
+
+void VEditArea::readFile()
+{
+    VEditWindow *win = getWindow(curWindowIndex);
+    win->readFile();
+}
+
+void VEditArea::saveAndReadFile()
+{
+    VEditWindow *win = getWindow(curWindowIndex);
+    win->saveAndReadFile();
+}
+
+void VEditArea::handleNotebookRenamed(const QVector<VNotebook> &notebooks,
+                                      const QString &oldName, const QString &newName)
+{
+    int nrWin = splitter->count();
+    for (int i = 0; i < nrWin; ++i) {
+        VEditWindow *win = getWindow(i);
+        win->handleNotebookRenamed(notebooks, oldName, newName);
+    }
+}
+
+void VEditArea::handleSplitWindowRequest(VEditWindow *curWindow)
+{
+    if (!curWindow) {
+        return;
+    }
+    int idx = splitter->indexOf(curWindow);
+    qDebug() << "window" << idx << "requests split itself";
+    insertSplitWindow(++idx);
+    setCurrentWindow(idx, true);
+}
+
+void VEditArea::handleRemoveSplitRequest(VEditWindow *curWindow)
+{
+    if (!curWindow) {
+        return;
+    }
+    int idx = splitter->indexOf(curWindow);
+
+    removeSplitWindow(curWindow);
+
+    if (idx >= splitter->count()) {
+        idx = splitter->count() - 1;
+    }
+
+    // At least one split window left
+    Q_ASSERT(idx >= 0);
+    setCurrentWindow(idx, true);
+}
+
+void VEditArea::mousePressEvent(QMouseEvent *event)
+{
+    return;
+    qDebug() << "VEditArea press event" << event;
+    QPoint pos = event->pos();
+    int nrWin = splitter->count();
+    for (int i = 0; i < nrWin; ++i) {
+        VEditWindow *win = getWindow(i);
+        if (win->geometry().contains(pos, true)) {
+            setCurrentWindow(i, true);
+            break;
+        }
+    }
+    QWidget::mousePressEvent(event);
+}
+
+void VEditArea::handleWindowFocused()
+{
+    QObject *winObject = sender();
+    int nrWin = splitter->count();
+    for (int i = 0; i < nrWin; ++i) {
+        if (splitter->widget(i) == winObject) {
+            setCurrentWindow(i, false);
+            break;
+        }
+    }
+}

+ 70 - 0
src/veditarea.h

@@ -0,0 +1,70 @@
+#ifndef VEDITAREA_H
+#define VEDITAREA_H
+
+#include <QWidget>
+#include <QJsonObject>
+#include <QString>
+#include <QFileInfo>
+#include <QDir>
+#include <QVector>
+#include <QPair>
+#include <QtDebug>
+#include <QSplitter>
+#include "vnotebook.h"
+#include "veditwindow.h"
+
+class VNote;
+
+class VEditArea : public QWidget
+{
+    Q_OBJECT
+public:
+    explicit VEditArea(VNote *vnote, QWidget *parent = 0);
+
+signals:
+    void curTabStatusChanged(const QString &notebook, const QString &relativePath,
+                             bool editMode, bool modifiable);
+
+protected:
+    void mousePressEvent(QMouseEvent *event) Q_DECL_OVERRIDE;
+
+public slots:
+    void openFile(QJsonObject fileJson);
+    // Close the file forcely
+    void closeFile(QJsonObject fileJson);
+    void editFile();
+    void saveFile();
+    void readFile();
+    void saveAndReadFile();
+    void handleNotebookRenamed(const QVector<VNotebook> &notebooks, const QString &oldName,
+                               const QString &newName);
+
+private slots:
+    void handleSplitWindowRequest(VEditWindow *curWindow);
+    void handleRemoveSplitRequest(VEditWindow *curWindow);
+    void handleWindowFocused();
+
+private:
+    void setupUI();
+    QVector<QPair<int, int> > findTabsByFile(const QString &notebook, const QString &relativePath);
+    int openFileInWindow(int windowIndex, const QString &notebook, const QString &relativePath,
+                         int mode);
+    void setCurrentTab(int windowIndex, int tabIndex, bool setFocus);
+    void setCurrentWindow(int windowIndex, bool setFocus);
+    inline VEditWindow *getWindow(int windowIndex) const;
+    void insertSplitWindow(int idx);
+    void removeSplitWindow(VEditWindow *win);
+
+    VNote *vnote;
+    int curWindowIndex;
+
+    // Splitter holding multiple split windows
+    QSplitter *splitter;
+};
+
+inline VEditWindow* VEditArea::getWindow(int windowIndex) const
+{
+    return dynamic_cast<VEditWindow *>(splitter->widget(windowIndex));
+}
+
+#endif // VEDITAREA_H

+ 16 - 1
src/veditor.cpp

@@ -32,6 +32,9 @@ VEditor::VEditor(const QString &path, bool modifiable, QWidget *parent)
     setupUI();
 
     showFileReadMode();
+
+    connect(qApp, &QApplication::focusChanged,
+            this, &VEditor::handleFocusChanged);
 }
 
 VEditor::~VEditor()
@@ -148,7 +151,7 @@ void VEditor::readFile()
     if (textEditor->isModified()) {
         // Need to save the changes
         QMessageBox msgBox(this);
-        msgBox.setText("The note has been modified.");
+        msgBox.setText(QString("The note \"%1\" has been modified.").arg(noteFile->fileName));
         msgBox.setInformativeText("Do you want to save your changes?");
         msgBox.setIcon(QMessageBox::Information);
         msgBox.setStandardButtons(QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel);
@@ -220,3 +223,15 @@ void VEditor::setupMarkdownPreview()
 
     addWidget(webPreviewer);
 }
+
+void VEditor::focusTab()
+{
+    currentWidget()->setFocus();
+}
+
+void VEditor::handleFocusChanged(QWidget *old, QWidget *now)
+{
+    if (isChild(now)) {
+        emit getFocused();
+    }
+}

+ 20 - 0
src/veditor.h

@@ -16,6 +16,7 @@ class VNote;
 
 class VEditor : public QStackedWidget
 {
+    Q_OBJECT
 public:
     VEditor(const QString &path, bool modifiable, QWidget *parent = 0);
     ~VEditor();
@@ -29,6 +30,13 @@ public:
 
     inline bool getIsEditMode() const;
     inline bool isModified() const;
+    void focusTab();
+
+signals:
+    void getFocused();
+
+private slots:
+    void handleFocusChanged(QWidget *old, QWidget *now);
 
 private:
     bool isMarkdown(const QString &name);
@@ -37,6 +45,7 @@ private:
     void showFileEditMode();
     void setupMarkdownPreview();
     void previewByConverter();
+    inline bool isChild(QObject *obj);
 
     VNoteFile *noteFile;
     bool isEditMode;
@@ -57,4 +66,15 @@ inline bool VEditor::isModified() const
     return textEditor->isModified();
 }
 
+inline bool VEditor::isChild(QObject *obj)
+{
+    while (obj) {
+        if (obj == this) {
+            return true;
+        }
+        obj = obj->parent();
+    }
+    return false;
+}
+
 #endif // VEDITOR_H

+ 296 - 0
src/veditwindow.cpp

@@ -0,0 +1,296 @@
+#include <QtWidgets>
+#include <QtDebug>
+#include "veditwindow.h"
+#include "veditor.h"
+#include "vnote.h"
+#include "vconfigmanager.h"
+
+extern VConfigManager vconfig;
+
+VEditWindow::VEditWindow(VNote *vnote, QWidget *parent)
+    : QTabWidget(parent), vnote(vnote)
+{
+    setupCornerWidget();
+
+    setTabsClosable(true);
+    setMovable(true);
+
+    connect(this, &VEditWindow::tabCloseRequested,
+            this, &VEditWindow::handleTabCloseRequest);
+    connect(this, &VEditWindow::tabBarClicked,
+            this, &VEditWindow::handleTabbarClicked);
+}
+
+void VEditWindow::setupCornerWidget()
+{
+    // Right corner button
+    // Actions
+    splitAct = new QAction(QIcon(":/resources/icons/split_window.svg"),
+                                    tr("Split"), this);
+    splitAct->setStatusTip(tr("Split current window vertically"));
+    connect(splitAct, &QAction::triggered,
+            this, &VEditWindow::splitWindow);
+
+    removeSplitAct = new QAction(QIcon(":/resources/icons/remove_split.svg"),
+                                          tr("Remove split"), this);
+    removeSplitAct->setStatusTip(tr("Remove current split window"));
+    connect(removeSplitAct, &QAction::triggered,
+            this, &VEditWindow::removeSplit);
+
+    rightBtn = new QPushButton(QIcon(":/resources/icons/corner_menu.svg"),
+                               "", this);
+    QMenu *rightMenu = new QMenu(this);
+    rightMenu->addAction(splitAct);
+    rightMenu->addAction(removeSplitAct);
+    rightBtn->setMenu(rightMenu);
+
+    setCornerWidget(rightBtn, Qt::TopRightCorner);
+}
+
+void VEditWindow::splitWindow()
+{
+    emit requestSplitWindow(this);
+}
+
+void VEditWindow::removeSplit()
+{
+    // Close all the files one by one
+    // If user do not want to close a file, just stop removing
+    if (closeAllFiles()) {
+        Q_ASSERT(count() == 0);
+        emit requestRemoveSplit(this);
+    }
+}
+
+void VEditWindow::setRemoveSplitEnable(bool enabled)
+{
+    removeSplitAct->setVisible(enabled);
+}
+
+void VEditWindow::openWelcomePage()
+{
+    int idx = openFileInTab("", vconfig.getWelcomePagePath(), false);
+    setTabText(idx, "Welcome to VNote");
+    setTabToolTip(idx, "VNote");
+}
+
+int VEditWindow::insertTabWithData(int index, QWidget *page,
+                                   const QJsonObject &tabData)
+{
+    QString label = getFileName(tabData["relative_path"].toString());
+    int idx = insertTab(index, page, label);
+    QTabBar *tabs = tabBar();
+    tabs->setTabData(idx, tabData);
+    noticeTabStatus(currentIndex());
+    return idx;
+}
+
+int VEditWindow::appendTabWithData(QWidget *page, const QJsonObject &tabData)
+{
+    return insertTabWithData(count(), page, tabData);
+}
+
+int VEditWindow::openFile(const QString &notebook, const QString &relativePath, int mode)
+{
+    // Find if it has been opened already
+    int idx = findTabByFile(notebook, relativePath);
+    if (idx > -1) {
+        goto out;
+    }
+    idx = openFileInTab(notebook, relativePath, true);
+out:
+    setCurrentIndex(idx);
+    if (mode == OpenFileMode::Edit) {
+        editFile();
+    }
+    focusWindow();
+    return idx;
+}
+
+void VEditWindow::closeFile(const QString &notebook, const QString &relativePath)
+{
+    // Find if it has been opened already
+    int idx = findTabByFile(notebook, relativePath);
+    if (idx == -1) {
+        return;
+    }
+
+    // Do not check if modified
+    VEditor *editor = getTab(idx);
+    Q_ASSERT(editor);
+    removeTab(idx);
+    delete editor;
+}
+
+bool VEditWindow::closeAllFiles()
+{
+    int nrTab = count();
+    for (int i = 0; i < nrTab; ++i) {
+        // Always close the first tab
+        if (!handleTabCloseRequest(0)) {
+            return false;
+        }
+    }
+    return true;
+}
+
+int VEditWindow::openFileInTab(const QString &notebook, const QString &relativePath,
+                              bool modifiable)
+{
+    QString rootPath;
+    const QVector<VNotebook> &notebooks = vnote->getNotebooks();
+    for (int i = 0; i < notebooks.size(); ++i) {
+        if (notebooks[i].getName() == notebook) {
+            rootPath = notebooks[i].getPath();
+            break;
+        }
+    }
+
+    VEditor *editor = new VEditor(QDir::cleanPath(QDir(rootPath).filePath(relativePath)),
+                                  modifiable);
+    connect(editor, &VEditor::getFocused,
+            this, &VEditWindow::getFocused);
+
+    QJsonObject tabJson;
+    tabJson["notebook"] = notebook;
+    tabJson["relative_path"] = relativePath;
+    tabJson["modifiable"] = modifiable;
+    return appendTabWithData(editor, tabJson);
+}
+
+int VEditWindow::findTabByFile(const QString &notebook, const QString &relativePath) const
+{
+    QTabBar *tabs = tabBar();
+    int nrTabs = tabs->count();
+
+    for (int i = 0; i < nrTabs; ++i) {
+        QJsonObject tabJson = tabs->tabData(i).toJsonObject();
+        if (tabJson["notebook"] == notebook && tabJson["relative_path"] == relativePath) {
+            return i;
+        }
+    }
+    return -1;
+}
+
+bool VEditWindow::handleTabCloseRequest(int index)
+{
+    qDebug() << "request closing tab" << index;
+    VEditor *editor = getTab(index);
+    Q_ASSERT(editor);
+    bool ok = editor->requestClose();
+    if (ok) {
+        removeTab(index);
+        delete editor;
+    }
+    noticeTabStatus(currentIndex());
+    // User clicks the close button. We should make this window
+    // to be current window.
+    emit getFocused();
+    return ok;
+}
+
+void VEditWindow::readFile()
+{
+    VEditor *editor = getTab(currentIndex());
+    Q_ASSERT(editor);
+    editor->readFile();
+    noticeTabStatus(currentIndex());
+}
+
+void VEditWindow::saveAndReadFile()
+{
+    saveFile();
+    readFile();
+    noticeTabStatus(currentIndex());
+}
+
+void VEditWindow::editFile()
+{
+    VEditor *editor = getTab(currentIndex());
+    Q_ASSERT(editor);
+    editor->editFile();
+    noticeTabStatus(currentIndex());
+}
+
+void VEditWindow::saveFile()
+{
+    VEditor *editor = getTab(currentIndex());
+    Q_ASSERT(editor);
+    editor->saveFile();
+}
+
+void VEditWindow::handleNotebookRenamed(const QVector<VNotebook> &notebooks,
+                                        const QString &oldName, const QString &newName)
+{
+    QTabBar *tabs = tabBar();
+    int nrTabs = tabs->count();
+    for (int i = 0; i < nrTabs; ++i) {
+        QJsonObject tabJson = tabs->tabData(i).toJsonObject();
+        if (tabJson["notebook"] == oldName) {
+            tabJson["notebook"] = newName;
+            tabs->setTabData(i, tabJson);
+        }
+    }
+}
+
+void VEditWindow::noticeTabStatus(int index)
+{
+    if (index == -1) {
+        emit tabStatusChanged("", "", false, false);
+        return;
+    }
+
+    QJsonObject tabJson = tabBar()->tabData(index).toJsonObject();
+    Q_ASSERT(!tabJson.isEmpty());
+
+    QString notebook = tabJson["notebook"].toString();
+    QString relativePath = tabJson["relative_path"].toString();
+    VEditor *editor = getTab(index);
+    bool editMode = editor->getIsEditMode();
+    bool modifiable = tabJson["modifiable"].toBool();
+
+    emit tabStatusChanged(notebook, relativePath,
+                          editMode, modifiable);
+}
+
+void VEditWindow::getTabStatus(QString &notebook, QString &relativePath,
+                               bool &editMode, bool &modifiable) const
+{
+    int idx = currentIndex();
+    if (idx == -1) {
+        notebook = relativePath = "";
+        editMode = modifiable = false;
+        return;
+    }
+
+    QJsonObject tabJson = tabBar()->tabData(idx).toJsonObject();
+    Q_ASSERT(!tabJson.isEmpty());
+    notebook = tabJson["notebook"].toString();
+    relativePath = tabJson["relative_path"].toString();
+    VEditor *editor = getTab(idx);
+    editMode = editor->getIsEditMode();
+    modifiable = tabJson["modifiable"].toBool();
+}
+
+void VEditWindow::focusWindow()
+{
+    int idx = currentIndex();
+    if (idx == -1) {
+        setFocus();
+        return;
+    }
+    getTab(idx)->focusTab();
+}
+
+void VEditWindow::handleTabbarClicked(int index)
+{
+    // The child will emit getFocused here
+    focusWindow();
+    noticeTabStatus(index);
+}
+
+void VEditWindow::mousePressEvent(QMouseEvent *event)
+{
+    emit getFocused();
+    QTabWidget::mousePressEvent(event);
+}

+ 84 - 0
src/veditwindow.h

@@ -0,0 +1,84 @@
+#ifndef VEDITWINDOW_H
+#define VEDITWINDOW_H
+
+#include <QTabWidget>
+#include <QJsonObject>
+#include <QString>
+#include <QFileInfo>
+#include <QDir>
+#include "vnotebook.h"
+#include "veditor.h"
+
+class VNote;
+class QPushButton;
+
+class VEditWindow : public QTabWidget
+{
+    Q_OBJECT
+public:
+    explicit VEditWindow(VNote *vnote, QWidget *parent = 0);
+    int findTabByFile(const QString &notebook, const QString &relativePath) const;
+    int openFile(const QString &notebook, const QString &relativePath,
+                 int mode);
+    // Close the file forcely
+    void closeFile(const QString &notebook, const QString &relativePath);
+    void editFile();
+    void saveFile();
+    void readFile();
+    void saveAndReadFile();
+    void handleNotebookRenamed(const QVector<VNotebook> &notebooks, const QString &oldName,
+                               const QString &newName);
+    bool closeAllFiles();
+    void setRemoveSplitEnable(bool enabled);
+    void getTabStatus(QString &notebook, QString &relativePath,
+                      bool &editMode, bool &modifiable) const;
+    // Focus to current tab's editor
+    void focusWindow();
+
+protected:
+    void mousePressEvent(QMouseEvent *event) Q_DECL_OVERRIDE;
+
+signals:
+    void tabStatusChanged(const QString &notebook, const QString &relativePath,
+                          bool editMode, bool modifiable);
+    void requestSplitWindow(VEditWindow *curWindow);
+    void requestRemoveSplit(VEditWindow *curWindow);
+    // This widget or its children get the focus
+    void getFocused();
+
+private slots:
+    bool handleTabCloseRequest(int index);
+    void splitWindow();
+    void removeSplit();
+    void handleTabbarClicked(int index);
+
+private:
+    void setupCornerWidget();
+    void openWelcomePage();
+    int insertTabWithData(int index, QWidget *page, const QJsonObject &tabData);
+    int appendTabWithData(QWidget *page, const QJsonObject &tabData);
+    int openFileInTab(const QString &notebook, const QString &relativePath, bool modifiable);
+    inline QString getFileName(const QString &relativePath) const;
+    inline VEditor *getTab(int tabIndex) const;
+    void noticeTabStatus(int index);
+
+    VNote *vnote;
+    // Button in the right corner
+    QPushButton *rightBtn;
+
+    // Actions
+    QAction *splitAct;
+    QAction *removeSplitAct;
+};
+
+inline QString VEditWindow::getFileName(const QString &path) const
+{
+    return QFileInfo(QDir::cleanPath(path)).fileName();
+}
+
+inline VEditor* VEditWindow::getTab(int tabIndex) const
+{
+    return dynamic_cast<VEditor *>(widget(tabIndex));
+}
+
+#endif // VEDITWINDOW_H

+ 13 - 14
src/vmainwindow.cpp

@@ -4,11 +4,11 @@
 #include "vdirectorytree.h"
 #include "vnote.h"
 #include "vfilelist.h"
-#include "vtabwidget.h"
 #include "vconfigmanager.h"
 #include "dialog/vnewnotebookdialog.h"
 #include "dialog/vnotebookinfodialog.h"
 #include "utils/vutils.h"
+#include "veditarea.h"
 
 extern VConfigManager vconfig;
 
@@ -79,15 +79,14 @@ void VMainWindow::setupUI()
     fileList = new VFileList(vnote);
     fileList->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Expanding);
 
-    // Editor tab widget
-    tabs = new VTabWidget(vnote);
-    tabs->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
+    editArea = new VEditArea(vnote);
+    editArea->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
 
     // Main Splitter
     mainSplitter = new QSplitter();
     mainSplitter->addWidget(nbContainer);
     mainSplitter->addWidget(fileList);
-    mainSplitter->addWidget(tabs);
+    mainSplitter->addWidget(editArea);
     mainSplitter->setStretchFactor(0, 0);
     mainSplitter->setStretchFactor(1, 0);
     mainSplitter->setStretchFactor(2, 1);
@@ -106,14 +105,14 @@ void VMainWindow::setupUI()
             this, &VMainWindow::handleFileListDirectoryChanged);
 
     connect(fileList, &VFileList::fileClicked,
-            tabs, &VTabWidget::openFile);
+            editArea, &VEditArea::openFile);
     connect(fileList, &VFileList::fileDeleted,
-            tabs, &VTabWidget::closeFile);
+            editArea, &VEditArea::closeFile);
     connect(fileList, &VFileList::fileCreated,
-            tabs, &VTabWidget::openFile);
+            editArea, &VEditArea::openFile);
     connect(vnote, &VNote::notebooksRenamed,
-            tabs, &VTabWidget::handleNotebookRenamed);
-    connect(tabs, &VTabWidget::tabModeChanged,
+            editArea, &VEditArea::handleNotebookRenamed);
+    connect(editArea, &VEditArea::curTabStatusChanged,
             this, &VMainWindow::updateToolbarFromTabChage);
 
     connect(newNotebookBtn, &QPushButton::clicked,
@@ -160,26 +159,26 @@ void VMainWindow::initActions()
                               tr("&Edit"), this);
     editNoteAct->setStatusTip(tr("Edit current note"));
     connect(editNoteAct, &QAction::triggered,
-            tabs, &VTabWidget::editFile);
+            editArea, &VEditArea::editFile);
 
     discardExitAct = new QAction(QIcon(":/resources/icons/discard_exit.svg"),
                                  tr("Discard changes and exit"), this);
     discardExitAct->setStatusTip(tr("Discard changes and exit edit mode"));
     connect(discardExitAct, &QAction::triggered,
-            tabs, &VTabWidget::readFile);
+            editArea, &VEditArea::readFile);
 
     saveExitAct = new QAction(QIcon(":/resources/icons/save_exit.svg"),
                               tr("Save changes and exit"), this);
     saveExitAct->setStatusTip(tr("Save changes and exit edit mode"));
     connect(saveExitAct, &QAction::triggered,
-            tabs, &VTabWidget::saveAndReadFile);
+            editArea, &VEditArea::saveAndReadFile);
 
     saveNoteAct = new QAction(QIcon(":/resources/icons/save_note.svg"),
                               tr("&Save"), this);
     saveNoteAct->setStatusTip(tr("Save current note"));
     saveNoteAct->setShortcut(QKeySequence::Save);
     connect(saveNoteAct, &QAction::triggered,
-            tabs, &VTabWidget::saveFile);
+            editArea, &VEditArea::saveFile);
 
     viewAct = new QActionGroup(this);
     twoPanelViewAct = new QAction(QIcon(":/resources/icons/two_panels.svg"),

+ 3 - 3
src/vmainwindow.h

@@ -11,12 +11,12 @@ class QListWidget;
 class QTabWidget;
 class QToolBar;
 class VNote;
-class VTabWidget;
 class QAction;
 class QPushButton;
 class VNotebook;
 class QActionGroup;
 class VFileList;
+class VEditArea;
 
 class VMainWindow : public QMainWindow
 {
@@ -65,6 +65,7 @@ private:
 
     // If true, comboBox changes will not trigger any signal out
     bool notebookComboMuted;
+    VNote *vnote;
 
     QLabel *notebookLabel;
     QLabel *directoryLabel;
@@ -77,9 +78,8 @@ private:
     QPushButton *dirInfoBtn;
     VFileList *fileList;
     VDirectoryTree *directoryTree;
-    VTabWidget *tabs;
     QSplitter *mainSplitter;
-    VNote *vnote;
+    VEditArea *editArea;
 
     // Actions
     QAction *newNoteAct;

+ 3 - 0
src/vnote.qrc

@@ -53,5 +53,8 @@
         <file>resources/icons/expand.svg</file>
         <file>resources/icons/two_panels.svg</file>
         <file>resources/icons/one_panel.svg</file>
+        <file>resources/icons/split_window.svg</file>
+        <file>resources/icons/corner_menu.svg</file>
+        <file>resources/icons/remove_split.svg</file>
     </qresource>
 </RCC>

+ 0 - 210
src/vtabwidget.cpp

@@ -1,210 +0,0 @@
-#include <QtWidgets>
-#include <QtDebug>
-#include "vtabwidget.h"
-#include "veditor.h"
-#include "vnote.h"
-#include "vconfigmanager.h"
-
-extern VConfigManager vconfig;
-
-VTabWidget::VTabWidget(VNote *vnote, QWidget *parent)
-    : QTabWidget(parent), vnote(vnote)
-{
-    setTabsClosable(true);
-    setMovable(true);
-    connect(this, &VTabWidget::tabCloseRequested,
-            this, &VTabWidget::handleTabCloseRequest);
-    connect(this, &VTabWidget::currentChanged,
-            this, &VTabWidget::onCurrentChanged);
-}
-
-void VTabWidget::openWelcomePage()
-{
-    int idx = openFileInTab("", vconfig.getWelcomePagePath(), false);
-    setTabText(idx, "Welcome to VNote");
-    setTabToolTip(idx, "VNote");
-}
-
-int VTabWidget::insertTabWithData(int index, QWidget *page,
-                                  const QJsonObject &tabData)
-{
-    QString label = getFileName(tabData["relative_path"].toString());
-    int idx = insertTab(index, page, label);
-    QTabBar *tabs = tabBar();
-    tabs->setTabData(idx, tabData);
-
-    // Need to update again with tabData
-    onCurrentChanged(idx);
-    return idx;
-}
-
-int VTabWidget::appendTabWithData(QWidget *page, const QJsonObject &tabData)
-{
-    return insertTabWithData(count(), page, tabData);
-}
-
-void VTabWidget::openFile(QJsonObject fileJson)
-{
-    if (fileJson.isEmpty()) {
-        return;
-    }
-
-    QString notebook = fileJson["notebook"].toString();
-    QString relativePath = fileJson["relative_path"].toString();
-    int mode = OpenFileMode::Read;
-    if (fileJson.contains("mode")) {
-        mode = fileJson["mode"].toInt();
-    }
-
-    qDebug() << "open notebook" << notebook << "path" << relativePath << mode;
-
-    // Find if it has been opened already
-    int idx = findTabByFile(notebook, relativePath);
-    if (idx > -1) {
-        goto out;
-    }
-
-    idx = openFileInTab(notebook, relativePath, true);
-
-out:
-    setCurrentIndex(idx);
-    if (mode == OpenFileMode::Edit) {
-        editFile();
-    }
-}
-
-void VTabWidget::closeFile(QJsonObject fileJson)
-{
-    if (fileJson.isEmpty()) {
-        return;
-    }
-    qDebug() << "close file:" << fileJson;
-
-    QString notebook = fileJson["notebook"].toString();
-    QString relativePath = fileJson["relative_path"].toString();
-
-    // Find if it has been opened already
-    int idx = findTabByFile(notebook, relativePath);
-    if (idx == -1) {
-        return;
-    }
-
-    QWidget* page = widget(idx);
-    Q_ASSERT(page);
-    removeTab(idx);
-    delete page;
-}
-
-int VTabWidget::openFileInTab(const QString &notebook, const QString &relativePath,
-                              bool modifiable)
-{
-    QString rootPath;
-    const QVector<VNotebook> &notebooks = vnote->getNotebooks();
-    for (int i = 0; i < notebooks.size(); ++i) {
-        if (notebooks[i].getName() == notebook) {
-            rootPath = notebooks[i].getPath();
-            break;
-        }
-    }
-
-    VEditor *editor = new VEditor(QDir::cleanPath(QDir(rootPath).filePath(relativePath)),
-                                  modifiable);
-    QJsonObject tabJson;
-    tabJson["notebook"] = notebook;
-    tabJson["relative_path"] = relativePath;
-    tabJson["modifiable"] = modifiable;
-    return appendTabWithData(editor, tabJson);
-}
-
-int VTabWidget::findTabByFile(const QString &notebook, const QString &relativePath) const
-{
-    QTabBar *tabs = tabBar();
-    int nrTabs = tabs->count();
-
-    for (int i = 0; i < nrTabs; ++i) {
-        QJsonObject tabJson = tabs->tabData(i).toJsonObject();
-        if (tabJson["notebook"] == notebook && tabJson["relative_path"] == relativePath) {
-            return i;
-        }
-    }
-    return -1;
-}
-
-void VTabWidget::handleTabCloseRequest(int index)
-{
-    qDebug() << "request closing tab" << index;
-    VEditor *editor = dynamic_cast<VEditor *>(widget(index));
-    Q_ASSERT(editor);
-    bool ok = editor->requestClose();
-    if (ok) {
-        removeTab(index);
-        delete editor;
-    }
-}
-
-void VTabWidget::readFile()
-{
-    VEditor *editor = dynamic_cast<VEditor *>(currentWidget());
-    Q_ASSERT(editor);
-    editor->readFile();
-    onCurrentChanged(currentIndex());
-}
-
-void VTabWidget::saveAndReadFile()
-{
-    saveFile();
-    readFile();
-    onCurrentChanged(currentIndex());
-}
-
-void VTabWidget::editFile()
-{
-    VEditor *editor = dynamic_cast<VEditor *>(currentWidget());
-    Q_ASSERT(editor);
-    editor->editFile();
-    onCurrentChanged(currentIndex());
-}
-
-void VTabWidget::saveFile()
-{
-    VEditor *editor = dynamic_cast<VEditor *>(currentWidget());
-    Q_ASSERT(editor);
-    editor->saveFile();
-}
-
-void VTabWidget::handleNotebookRenamed(const QVector<VNotebook> &notebooks,
-                                       const QString &oldName, const QString &newName)
-{
-    QTabBar *tabs = tabBar();
-    int nrTabs = tabs->count();
-    for (int i = 0; i < nrTabs; ++i) {
-        QJsonObject tabJson = tabs->tabData(i).toJsonObject();
-        if (tabJson["notebook"] == oldName) {
-            tabJson["notebook"] = newName;
-            tabs->setTabData(i, tabJson);
-        }
-    }
-}
-
-void VTabWidget::onCurrentChanged(int index)
-{
-    if (index == -1) {
-        emit tabModeChanged("", "", false, false);
-        return;
-    }
-
-    QJsonObject tabJson = tabBar()->tabData(index).toJsonObject();
-    if (tabJson.isEmpty()) {
-        // Maybe the tab data has not been set yet
-        return;
-    }
-
-    QString notebook = tabJson["notebook"].toString();
-    QString relativePath = tabJson["relative_path"].toString();
-    VEditor *editor = (VEditor *)widget(index);
-    bool editMode = editor->getIsEditMode();
-    bool modifiable = tabJson["modifiable"].toBool();
-
-    emit tabModeChanged(notebook, relativePath,
-                        editMode, modifiable);
-}

+ 0 - 54
src/vtabwidget.h

@@ -1,54 +0,0 @@
-#ifndef VTABWIDGET_H
-#define VTABWIDGET_H
-
-#include <QTabWidget>
-#include <QJsonObject>
-#include <QString>
-#include <QFileInfo>
-#include <QDir>
-#include "vnotebook.h"
-
-class VNote;
-
-class VTabWidget : public QTabWidget
-{
-    Q_OBJECT
-public:
-    explicit VTabWidget(VNote *vnote, QWidget *parent = 0);
-
-signals:
-    void tabModeChanged(const QString &notebook, const QString &relativePath,
-                        bool editMode, bool modifiable);
-
-public slots:
-    void openFile(QJsonObject fileJson);
-    // Close the file forcely
-    void closeFile(QJsonObject fileJson);
-    void editFile();
-    void saveFile();
-    void readFile();
-    void saveAndReadFile();
-    void handleNotebookRenamed(const QVector<VNotebook> &notebooks, const QString &oldName,
-                               const QString &newName);
-
-private slots:
-    void handleTabCloseRequest(int index);
-    void onCurrentChanged(int index);
-
-private:
-    void openWelcomePage();
-    int insertTabWithData(int index, QWidget *page, const QJsonObject &tabData);
-    int appendTabWithData(QWidget *page, const QJsonObject &tabData);
-    int findTabByFile(const QString &notebook, const QString &relativePath) const;
-    int openFileInTab(const QString &notebook, const QString &relativePath, bool modifiable);
-    inline QString getFileName(const QString &relativePath) const;
-
-    VNote *vnote;
-};
-
-inline QString VTabWidget::getFileName(const QString &path) const
-{
-    return QFileInfo(QDir::cleanPath(path)).fileName();
-}
-
-#endif // VTABWIDGET_H