Browse Source

LivePreview: support bidirectional smart live preview

Le Tan 7 years ago
parent
commit
060c02297b

+ 2 - 1
src/resources/markdown_template.js

@@ -1626,7 +1626,8 @@ var htmlToText = function(identifier, id, timeStamp, html) {
 };
 
 var performSmartLivePreview = function(lang, text, hints, isRegex) {
-    if (previewDiv.style.display == 'none') {
+    if (previewDiv.style.display == 'none'
+        || document.getSelection().type == 'Range') {
         return;
     }
 

+ 8 - 1
src/resources/vnote.ini

@@ -247,7 +247,10 @@ max_tag_label_length=10
 max_num_of_tag_labels=3
 
 ; Smart live preview
-smart_live_preview=true
+; 0 - disable smart live preview
+; 1 - editor to web
+; 2 - web to editor
+smart_live_preview=3
 
 ; Support multiple keyboard layout
 multiple_keyboard_layout=true
@@ -467,8 +470,12 @@ ApplySnippet=S
 Export=O
 ; Toggle live preview
 LivePreview=I
+; Expand or restore live preview area
+ExpandLivePreview=U
 ; Focus edit area
 FocusEditArea=Y
+; Parse HTML and paste
+ParseAndPaste=P
 
 [external_editors]
 ; Define external editors which could be called to edit notes

+ 1 - 1
src/vconfigmanager.cpp

@@ -312,7 +312,7 @@ void VConfigManager::initialize()
                                                 "max_num_of_tag_labels").toInt();
 
     m_smartLivePreview = getConfigFromSettings("global",
-                                               "smart_live_preview").toBool();
+                                               "smart_live_preview").toInt();
 
     m_multipleKeyboardLayout = getConfigFromSettings("global",
                                                      "multiple_keyboard_layout").toBool();

+ 20 - 3
src/vconfigmanager.h

@@ -60,6 +60,12 @@ enum class KeyMode
     Invalid
 };
 
+enum SmartLivePreview
+{
+    Disabled = 0,
+    EditorToWeb = 0x1,
+    WebToEditor = 0x2
+};
 
 class VConfigManager : public QObject
 {
@@ -547,7 +553,8 @@ public:
 
     QChar getVimLeaderKey() const;
 
-    bool getSmartLivePreview() const;
+    int getSmartLivePreview() const;
+    void setSmartLivePreview(int p_preview);
 
     bool getMultipleKeyboardLayout() const;
 
@@ -989,7 +996,7 @@ private:
     QChar m_vimLeaderKey;
 
     // Smart live preview.
-    bool m_smartLivePreview;
+    int m_smartLivePreview;
 
     // Support multiple keyboard layout.
     bool m_multipleKeyboardLayout;
@@ -2550,11 +2557,21 @@ inline QChar VConfigManager::getVimLeaderKey() const
     return m_vimLeaderKey;
 }
 
-inline bool VConfigManager::getSmartLivePreview() const
+inline int VConfigManager::getSmartLivePreview() const
 {
     return m_smartLivePreview;
 }
 
+inline void VConfigManager::setSmartLivePreview(int p_preview)
+{
+    if (m_smartLivePreview == p_preview) {
+        return;
+    }
+
+    m_smartLivePreview = p_preview;
+    setConfigToSettings("global", "smart_live_preview", m_smartLivePreview);
+}
+
 inline bool VConfigManager::getMultipleKeyboardLayout() const
 {
     return m_multipleKeyboardLayout;

+ 43 - 2
src/veditarea.cpp

@@ -14,6 +14,8 @@
 #include "vcaptain.h"
 #include "vfilelist.h"
 #include "vmathjaxpreviewhelper.h"
+#include "vmdtab.h"
+#include "vmdeditor.h"
 
 extern VConfigManager *g_config;
 
@@ -954,6 +956,14 @@ void VEditArea::registerCaptainTargets()
                                    g_config->getCaptainShortcutKeySequence("LivePreview"),
                                    this,
                                    toggleLivePreviewByCaptain);
+    captain->registerCaptainTarget(tr("ExpandLivePreview"),
+                                   g_config->getCaptainShortcutKeySequence("ExpandLivePreview"),
+                                   this,
+                                   expandLivePreviewByCaptain);
+    captain->registerCaptainTarget(tr("ParseAndPaste"),
+                                   g_config->getCaptainShortcutKeySequence("ParseAndPaste"),
+                                   this,
+                                   parseAndPasteByCaptain);
 }
 
 bool VEditArea::activateTabByCaptain(void *p_target, void *p_data, int p_idx)
@@ -1139,9 +1149,40 @@ bool VEditArea::toggleLivePreviewByCaptain(void *p_target, void *p_data)
     Q_UNUSED(p_data);
     VEditArea *obj = static_cast<VEditArea *>(p_target);
 
-    VEditTab *tab = obj->getCurrentTab();
+    VMdTab *tab = dynamic_cast<VMdTab *>(obj->getCurrentTab());
+    if (tab) {
+        if (tab->toggleLivePreview()) {
+            return false;
+        }
+    }
+
+    return true;
+}
+
+bool VEditArea::expandLivePreviewByCaptain(void *p_target, void *p_data)
+{
+    Q_UNUSED(p_data);
+    VEditArea *obj = static_cast<VEditArea *>(p_target);
+
+    VMdTab *tab = dynamic_cast<VMdTab *>(obj->getCurrentTab());
     if (tab) {
-        tab->toggleLivePreview();
+        if (tab->expandRestorePreviewArea()) {
+            return false;
+        }
+    }
+
+    return true;
+}
+
+bool VEditArea::parseAndPasteByCaptain(void *p_target, void *p_data)
+{
+    Q_UNUSED(p_data);
+    VEditArea *obj = static_cast<VEditArea *>(p_target);
+
+    const VMdTab *tab = dynamic_cast<const VMdTab *>(obj->getCurrentTab());
+    if (tab && tab->isEditMode()) {
+        VMdEditor *editor = tab->getEditor();
+        editor->parseAndPaste();
     }
 
     return true;

+ 6 - 0
src/veditarea.h

@@ -234,6 +234,12 @@ private:
     // Toggle live preview.
     static bool toggleLivePreviewByCaptain(void *p_target, void *p_data);
 
+    // Expand live preview.
+    static bool expandLivePreviewByCaptain(void *p_target, void *p_data);
+
+    // Parse and paste.
+    static bool parseAndPasteByCaptain(void *p_target, void *p_data);
+
     // End Captain mode functions.
 
     int curWindowIndex;

+ 41 - 21
src/veditor.cpp

@@ -519,35 +519,43 @@ bool VEditor::findText(const QString &p_text,
     bool wrapped = false;
     QTextCursor retCursor;
     int matches = 0;
-    int start = p_forward ? cursor.position() + 1 : cursor.position();
-    if (p_cursor) {
-        start = p_forward ? p_cursor->position() + 1 : p_cursor->position();
-    }
-
+    int start = p_cursor ? p_cursor->position() : cursor.position();
     if (p_useLeftSideOfCursor) {
         --start;
     }
+    int skipPosition = start;
 
-    bool found = findTextHelper(p_text, p_options, p_forward, start,
-                                wrapped, retCursor);
-    if (found) {
-        Q_ASSERT(!retCursor.isNull());
-        if (wrapped) {
-            showWrapLabel();
-        }
+    bool found = false;
+    while (true) {
+        found = findTextHelper(p_text, p_options, p_forward, start, wrapped, retCursor);
+        if (found) {
+            Q_ASSERT(!retCursor.isNull());
+            if (wrapped) {
+                showWrapLabel();
+            }
 
-        if (p_cursor) {
-            p_cursor->setPosition(retCursor.selectionStart(), p_moveMode);
+            if (p_forward && retCursor.selectionStart() == skipPosition) {
+                // Skip the first match.
+                skipPosition = -1;
+                start = retCursor.selectionEnd();
+                continue;
+            }
+
+            if (p_cursor) {
+                p_cursor->setPosition(retCursor.selectionStart(), p_moveMode);
+            } else {
+                cursor.setPosition(retCursor.selectionStart(), p_moveMode);
+                setTextCursorW(cursor);
+            }
+
+            highlightSearchedWord(p_text, p_options);
+            highlightSearchedWordUnderCursor(retCursor);
+            matches = m_extraSelections[(int)SelectionId::SearchedKeyword].size();
         } else {
-            cursor.setPosition(retCursor.selectionStart(), p_moveMode);
-            setTextCursorW(cursor);
+            clearSearchedWordHighlight();
         }
 
-        highlightSearchedWord(p_text, p_options);
-        highlightSearchedWordUnderCursor(retCursor);
-        matches = m_extraSelections[(int)SelectionId::SearchedKeyword].size();
-    } else {
-        clearSearchedWordHighlight();
+        break;
     }
 
     if (matches == 0) {
@@ -561,6 +569,18 @@ bool VEditor::findText(const QString &p_text,
     return found;
 }
 
+bool VEditor::findTextInRange(const QString &p_text,
+                              uint p_options,
+                              bool p_forward,
+                              int p_start,
+                              int p_end)
+{
+    Q_UNUSED(p_start);
+    Q_UNUSED(p_end);
+    // TODO
+    return findText(p_text, p_options, p_forward);
+}
+
 void VEditor::highlightIncrementalSearchedWord(const QTextCursor &p_cursor)
 {
     QList<QTextEdit::ExtraSelection> &selects = m_extraSelections[(int)SelectionId::IncrementalSearchedKeyword];

+ 7 - 0
src/veditor.h

@@ -83,6 +83,13 @@ public:
                   QTextCursor::MoveMode p_moveMode = QTextCursor::MoveAnchor,
                   bool p_useLeftSideOfCursor = false);
 
+    // Constrain the scope.
+    bool findTextInRange(const QString &p_text,
+                         uint p_options,
+                         bool p_forward,
+                         int p_start,
+                         int p_end);
+
     void replaceText(const QString &p_text,
                      uint p_options,
                      const QString &p_replaceText,

+ 0 - 4
src/vedittab.h

@@ -119,10 +119,6 @@ public:
     // Fetch tab stat info.
     virtual VWordCountInfo fetchWordCountInfo(bool p_editMode) const;
 
-    virtual void toggleLivePreview()
-    {
-    }
-
 public slots:
     // Enter edit mode
     virtual void editFile() = 0;

+ 6 - 1
src/vlivepreviewhelper.cpp

@@ -202,6 +202,7 @@ void VLivePreviewHelper::updateCodeBlocks(TimeStamp p_timeStamp, const QVector<V
             && vcb.m_endBlock >= cursorBlock) {
             if (lastIndex == idx && cached && !oldCache) {
                 needUpdate = false;
+                m_curLivePreviewInfo.update(vcb);
             }
 
             m_cbIndex = idx;
@@ -260,12 +261,16 @@ void VLivePreviewHelper::handleCursorPositionChanged()
 void VLivePreviewHelper::updateLivePreview()
 {
     if (m_cbIndex < 0) {
+        m_curLivePreviewInfo.clear();
         return;
     }
 
     Q_ASSERT(!(m_cbIndex & ~INDEX_MASK));
     const CodeBlockPreviewInfo &cb = m_codeBlocks[m_cbIndex];
     const VCodeBlock &vcb = cb.codeBlock();
+
+    m_curLivePreviewInfo.update(vcb);
+
     if (vcb.m_lang == "dot") {
         if (!m_graphvizHelper) {
             m_graphvizHelper = new VGraphvizHelper(this);
@@ -530,7 +535,7 @@ void VLivePreviewHelper::performSmartLivePreview()
 {
     if (m_cbIndex < 0
         || m_cbIndex >= m_codeBlocks.size()
-        || !g_config->getSmartLivePreview()) {
+        || !(g_config->getSmartLivePreview() & SmartLivePreview::EditorToWeb)) {
         return;
     }
 

+ 39 - 1
src/vlivepreviewhelper.h

@@ -92,6 +92,36 @@ private:
 };
 
 
+struct LivePreviewInfo
+{
+    LivePreviewInfo()
+        : m_startPos(),
+          m_endPos()
+    {
+    }
+
+    void clear()
+    {
+        m_startPos = 0;
+        m_endPos = 0;
+    }
+
+    void update(const VCodeBlock &p_cb)
+    {
+        m_startPos = p_cb.m_startPos;
+        m_endPos = p_cb.m_startPos + p_cb.m_text.size();
+    }
+
+    bool isValid() const
+    {
+        return m_endPos > m_startPos;
+    }
+
+    int m_startPos;
+    int m_endPos;
+};
+
+
 // Manage live preview and inplace preview.
 class VLivePreviewHelper : public QObject
 {
@@ -109,6 +139,8 @@ public:
 
     bool isPreviewEnabled() const;
 
+    const LivePreviewInfo &getLivePreviewInfo() const;
+
 public slots:
     void updateCodeBlocks(TimeStamp p_timeStamp, const QVector<VCodeBlock> &p_codeBlocks);
 
@@ -220,7 +252,6 @@ private:
         QString m_imageBackground;
     };
 
-
     void checkLang(const QString &p_lang, bool &p_livePreview, bool &p_inplacePreview) const;
 
     // Get image data for this code block for inplace preview.
@@ -277,6 +308,8 @@ private:
     int m_lastCursorBlock;
 
     QTimer *m_livePreviewTimer;
+
+    LivePreviewInfo m_curLivePreviewInfo;
 };
 
 inline bool VLivePreviewHelper::isPreviewEnabled() const
@@ -292,4 +325,9 @@ inline qreal VLivePreviewHelper::getScaleFactor(const CodeBlockPreviewInfo &p_cb
         return m_scaleFactor;
     }
 }
+
+inline const LivePreviewInfo &VLivePreviewHelper::getLivePreviewInfo() const
+{
+    return m_curLivePreviewInfo;
+}
 #endif // VLIVEPREVIEWHELPER_H

+ 38 - 22
src/vmdeditor.cpp

@@ -27,6 +27,7 @@
 #include "utils/vclipboardutils.h"
 #include "vplantumlhelper.h"
 #include "vgraphvizhelper.h"
+#include "vmdtab.h"
 
 extern VWebUtils *g_webUtils;
 
@@ -382,19 +383,25 @@ void VMdEditor::contextMenuEvent(QContextMenuEvent *p_event)
                     emit m_object->discardAndRead();
                 });
 
-        QAction *toggleLivePreviewAct = new QAction(tr("Live Preview For Graphs"), menu.data());
-        toggleLivePreviewAct->setToolTip(tr("Toggle live preview panel for graphs"));
-        VUtils::fixTextWithCaptainShortcut(toggleLivePreviewAct, "LivePreview");
-        connect(toggleLivePreviewAct, &QAction::triggered,
-                this, [this]() {
-                    m_editTab->toggleLivePreview();
-                });
-
-        menu->insertAction(firstAct, toggleLivePreviewAct);
-        menu->insertAction(toggleLivePreviewAct, discardExitAct);
-        menu->insertAction(discardExitAct, saveExitAct);
+        VMdTab *mdtab = dynamic_cast<VMdTab *>(m_editTab);
+        if (mdtab) {
+            QAction *toggleLivePreviewAct = new QAction(tr("Live Preview For Graphs"), menu.data());
+            toggleLivePreviewAct->setToolTip(tr("Toggle live preview panel for graphs"));
+            VUtils::fixTextWithCaptainShortcut(toggleLivePreviewAct, "LivePreview");
+            connect(toggleLivePreviewAct, &QAction::triggered,
+                    this, [this, mdtab]() {
+                        mdtab->toggleLivePreview();
+                    });
 
-        menu->insertSeparator(toggleLivePreviewAct);
+            menu->insertAction(firstAct, toggleLivePreviewAct);
+            menu->insertAction(toggleLivePreviewAct, discardExitAct);
+            menu->insertAction(discardExitAct, saveExitAct);
+            menu->insertSeparator(toggleLivePreviewAct);
+        } else {
+            menu->insertAction(firstAct, discardExitAct);
+            menu->insertAction(discardExitAct, saveExitAct);
+            menu->insertSeparator(discardExitAct);
+        }
 
         if (firstAct) {
             menu->insertSeparator(firstAct);
@@ -1389,18 +1396,10 @@ QAction *VMdEditor::initPasteAsBlockQuoteMenu(QAction *p_after, QMenu *p_menu)
 QAction *VMdEditor::initPasteAfterParseMenu(QAction *p_after, QMenu *p_menu)
 {
     QAction *papAct = new QAction(tr("Paste Parsed &Markdown Text"), p_menu);
+    VUtils::fixTextWithCaptainShortcut(papAct, "ParseAndPaste");
     papAct->setToolTip(tr("Parse HTML to Markdown text and paste"));
     connect(papAct, &QAction::triggered,
-            this, [this]() {
-                QClipboard *clipboard = QApplication::clipboard();
-                const QMimeData *mimeData = clipboard->mimeData();
-                QString html(mimeData->html());
-                if (!html.isEmpty()) {
-                    ++m_copyTimeStamp;
-                    emit requestHtmlToText(html, 0, m_copyTimeStamp);
-                }
-            });
-
+            this, &VMdEditor::parseAndPaste);
 
     insertActionAfter(p_after, papAct, p_menu);
     return papAct;
@@ -1824,3 +1823,20 @@ void VMdEditor::exportGraphAndCopy(const QString &p_lang,
 
     m_exportTempFile->close();
 }
+
+void VMdEditor::parseAndPaste()
+{
+    if (!m_editTab
+        || !m_editTab->isEditMode()
+        || isReadOnly()) {
+        return;
+    }
+
+    QClipboard *clipboard = QApplication::clipboard();
+    const QMimeData *mimeData = clipboard->mimeData();
+    QString html(mimeData->html());
+    if (!html.isEmpty()) {
+        ++m_copyTimeStamp;
+        emit requestHtmlToText(html, 0, m_copyTimeStamp);
+    }
+}

+ 2 - 0
src/vmdeditor.h

@@ -88,6 +88,8 @@ public slots:
 
     void htmlToTextFinished(int p_id, int p_timeStamp, const QString &p_html);
 
+    void parseAndPaste();
+
 // Wrapper functions for QPlainTextEdit/QTextEdit.
 public:
     void setExtraSelectionsW(const QList<QTextEdit::ExtraSelection> &p_selections) Q_DECL_OVERRIDE

+ 58 - 1
src/vmdtab.cpp

@@ -65,6 +65,26 @@ VMdTab::VMdTab(VFile *p_file, VEditArea *p_editArea,
                 writeBackupFile();
             });
 
+    m_livePreviewTimer = new QTimer(this);
+    m_livePreviewTimer->setSingleShot(true);
+    m_livePreviewTimer->setInterval(500);
+    connect(m_livePreviewTimer, &QTimer::timeout,
+            this, [this]() {
+                QString text = m_webViewer->selectedText();
+                if (text.isEmpty()) {
+                    return;
+                }
+
+                const LivePreviewInfo &info = m_livePreviewHelper->getLivePreviewInfo();
+                if (info.isValid()) {
+                    m_editor->findTextInRange(text,
+                                              FindOption::CaseSensitive,
+                                              true,
+                                              info.m_startPos,
+                                              info.m_endPos);
+                }
+            });
+
     if (p_mode == OpenFileMode::Edit) {
         showFileEditMode();
     } else {
@@ -396,6 +416,10 @@ void VMdTab::setupMarkdownViewer()
             this, &VMdTab::editFile);
     connect(m_webViewer, &VWebView::requestSavePage,
             this, &VMdTab::handleSavePageRequested);
+    connect(m_webViewer, &VWebView::selectionChanged,
+            this, &VMdTab::handleWebSelectionChanged);
+    connect(m_webViewer, &VWebView::requestExpandRestorePreviewArea,
+            this, &VMdTab::expandRestorePreviewArea);
 
     VPreviewPage *page = new VPreviewPage(m_webViewer);
     m_webViewer->setPage(page);
@@ -1516,18 +1540,51 @@ void VMdTab::setCurrentMode(Mode p_mode)
     focusChild();
 }
 
-void VMdTab::toggleLivePreview()
+bool VMdTab::toggleLivePreview()
 {
+    bool ret = false;
+
     switch (m_mode) {
     case Mode::EditPreview:
         setCurrentMode(Mode::Edit);
+        ret = true;
         break;
 
     case Mode::Edit:
         setCurrentMode(Mode::EditPreview);
+        ret = true;
         break;
 
     default:
         break;
     }
+
+    return ret;
+}
+
+void VMdTab::handleWebSelectionChanged()
+{
+    if (m_mode != Mode::EditPreview
+        || !(g_config->getSmartLivePreview() & SmartLivePreview::WebToEditor)) {
+        return;
+    }
+
+    m_livePreviewTimer->start();
+}
+
+bool VMdTab::expandRestorePreviewArea()
+{
+    if (m_mode != Mode::EditPreview) {
+        return false;
+    }
+
+    if (m_editor->isVisible()) {
+        m_editor->hide();
+        m_webViewer->setFocus();
+    } else {
+        m_editor->show();
+        m_editor->setFocus();
+    }
+
+    return true;
 }

+ 8 - 1
src/vmdtab.h

@@ -95,7 +95,9 @@ public:
     VWordCountInfo fetchWordCountInfo(bool p_editMode) const Q_DECL_OVERRIDE;
 
     // Toggle live preview in edit mode.
-    void toggleLivePreview() Q_DECL_OVERRIDE;
+    bool toggleLivePreview();
+
+    bool expandRestorePreviewArea();
 
 public slots:
     // Enter edit mode.
@@ -148,6 +150,9 @@ private slots:
     // Handle save page request.
     void handleSavePageRequested();
 
+    // Selection changed in web.
+    void handleWebSelectionChanged();
+
 private:
     enum TabReady { None = 0, ReadMode = 0x1, EditMode = 0x2 };
 
@@ -243,6 +248,8 @@ private:
     // Timer to write backup file when content has been changed.
     QTimer *m_backupTimer;
 
+    QTimer *m_livePreviewTimer;
+
     bool m_backupFileChecked;
 
     // Used to scroll to the header of edit mode in read mode.

+ 85 - 23
src/vwebview.cpp

@@ -45,6 +45,9 @@ void VWebView::contextMenuEvent(QContextMenuEvent *p_event)
     menu->setToolTipsVisible(true);
 
     const QList<QAction *> actions = menu->actions();
+    QAction *firstAction = actions.isEmpty() ? NULL : actions[0];
+
+    bool selection = hasSelection();
 
 #if defined(Q_OS_WIN)
     if (!m_copyImageUrlActionHooked) {
@@ -64,26 +67,46 @@ void VWebView::contextMenuEvent(QContextMenuEvent *p_event)
     }
 #endif
 
-    if (!hasSelection()
-        && !m_inPreview
-        && m_file
-        && m_file->isModifiable()) {
-        QAction *editAct= new QAction(VIconUtils::menuIcon(":/resources/icons/edit_note.svg"),
-                                      tr("&Edit"), menu);
-        editAct->setToolTip(tr("Edit current note"));
-        connect(editAct, &QAction::triggered,
-                this, &VWebView::handleEditAction);
-        menu->insertAction(actions.isEmpty() ? NULL : actions[0], editAct);
-        // actions does not contain editAction.
-        if (!actions.isEmpty()) {
-            menu->insertSeparator(actions[0]);
+    if (!selection) {
+        if (m_inPreview) {
+            QAction *expandAct = new QAction(tr("Expand/Restore Preview Area"), menu);
+            VUtils::fixTextWithCaptainShortcut(expandAct, "ExpandLivePreview");
+            connect(expandAct, &QAction::triggered,
+                    this, &VWebView::requestExpandRestorePreviewArea);
+            menu->insertAction(firstAction, expandAct);
+
+            initPreviewTunnelMenu(firstAction, menu);
+
+            if (firstAction) {
+                menu->insertSeparator(firstAction);
+            }
+        } else {
+            if (m_file && m_file->isModifiable()) {
+                QAction *editAct= new QAction(VIconUtils::menuIcon(":/resources/icons/edit_note.svg"),
+                                              tr("&Edit"),
+                                              menu);
+                editAct->setToolTip(tr("Edit current note"));
+                connect(editAct, &QAction::triggered,
+                        this, &VWebView::handleEditAction);
+                menu->insertAction(firstAction, editAct);
+                if (firstAction) {
+                    menu->insertSeparator(firstAction);
+                }
+            }
+
+            QAction *savePageAct = new QAction(QWebEnginePage::tr("Save &Page"), menu);
+            connect(savePageAct, &QAction::triggered,
+                    this, &VWebView::requestSavePage);
+            menu->addAction(savePageAct);
         }
     }
 
     // Add Copy As menu.
-    QAction *copyAct = pageAction(QWebEnginePage::Copy);
-    if (actions.contains(copyAct) && !m_inPreview) {
-        initCopyAsMenu(copyAct, menu);
+    {
+        QAction *copyAct = pageAction(QWebEnginePage::Copy);
+        if (actions.contains(copyAct) && !m_inPreview) {
+            initCopyAsMenu(copyAct, menu);
+        }
     }
 
     // We need to replace the "Copy Image" action:
@@ -100,13 +123,6 @@ void VWebView::contextMenuEvent(QContextMenuEvent *p_event)
         defaultCopyImageAct->setVisible(false);
     }
 
-    if (!hasSelection() && !m_inPreview) {
-        QAction *savePageAct = new QAction(QWebEnginePage::tr("Save &Page"), menu);
-        connect(savePageAct, &QAction::triggered,
-                this, &VWebView::requestSavePage);
-        menu->addAction(savePageAct);
-    }
-
     // Add Copy All As menu.
     if (!m_inPreview) {
         initCopyAllAsMenu(menu);
@@ -419,3 +435,49 @@ void VWebView::handleCopyAllAsAction(QAction *p_act)
 
     triggerPageAction(QWebEnginePage::Unselect);
 }
+
+void VWebView::initPreviewTunnelMenu(QAction *p_before, QMenu *p_menu)
+{
+    QMenu *subMenu = new QMenu(tr("Live Preview Tunnel"), p_menu);
+
+    int config = g_config->getSmartLivePreview();
+
+    QActionGroup *ag = new QActionGroup(subMenu);
+    QAction *act = ag->addAction(tr("Disabled"));
+    act->setData(SmartLivePreview::Disabled);
+    act->setCheckable(true);
+    if (act->data().toInt() == config) {
+        act->setChecked(true);
+    }
+
+    act = ag->addAction(tr("Editor -> Preview"));
+    act->setData(SmartLivePreview::EditorToWeb);
+    act->setCheckable(true);
+    if (act->data().toInt() == config) {
+        act->setChecked(true);
+    }
+
+    act = ag->addAction(tr("Preview -> Editor"));
+    act->setData(SmartLivePreview::WebToEditor);
+    act->setCheckable(true);
+    if (act->data().toInt() == config) {
+        act->setChecked(true);
+    }
+
+    act = ag->addAction(tr("Bidirectional"));
+    act->setData(SmartLivePreview::EditorToWeb | SmartLivePreview::WebToEditor);
+    act->setCheckable(true);
+    if (act->data().toInt() == config) {
+        act->setChecked(true);
+    }
+
+    connect(ag, &QActionGroup::triggered,
+            this, [this](QAction *p_act) {
+                int data = p_act->data().toInt();
+                g_config->setSmartLivePreview(data);
+            });
+
+    subMenu->addActions(ag->actions());
+
+    p_menu->insertMenu(p_before, subMenu);
+}

+ 4 - 0
src/vwebview.h

@@ -22,6 +22,8 @@ signals:
 
     void requestSavePage();
 
+    void requestExpandRestorePreviewArea();
+
 protected:
     void contextMenuEvent(QContextMenuEvent *p_event);
 
@@ -56,6 +58,8 @@ private:
 
     void initCopyAllAsMenu(QMenu *p_menu);
 
+    void initPreviewTunnelMenu(QAction *p_before, QMenu *p_menu);
+
     VFile *m_file;
 
     // Whether this view has hooked the Copy Image Url action.