diff --git a/src/resources/markdown-it.js b/src/resources/markdown-it.js index 31282867..cee6ac5b 100644 --- a/src/resources/markdown-it.js +++ b/src/resources/markdown-it.js @@ -83,85 +83,6 @@ var markdownToHtml = function(markdown, needToc) { } }; -// Handle wrong title levels, such as '#' followed by '###' -var toPerfectToc = function(toc) { - var i; - var curLevel = 1; - var perfToc = []; - for (i in toc) { - var item = toc[i]; - while (item.level > curLevel + 1) { - curLevel += 1; - var tmp = { level: curLevel, - anchor: item.anchor, - title: '[EMPTY]' - }; - perfToc.push(tmp); - } - perfToc.push(item); - curLevel = item.level; - } - return perfToc; -}; - -var itemToHtml = function(item) { - return '' + item.title + ''; -}; - -// Turn a perfect toc to a tree using ') { - curLevel--; - } - } - front += ''; - front += '
  • '; - front += itemToHtml(item); - } - } - while (ending.length > 0) { - front += ending.pop(); - } - front = front.replace("
  • ", ""); - front = ''; - return front; -}; - -var handleToc = function(needToc) { - var tocTree = tocToTree(toPerfectToc(toc)); - content.setToc(tocTree); - - // Add it to html - if (needToc) { - var eles = document.getElementsByClassName('vnote-toc'); - for (var i = 0; i < eles.length; ++i) { - eles[i].innerHTML = tocTree; - } - } -}; - var updateText = function(text) { var needToc = mdHasTocSection(text); var html = markdownToHtml(text, needToc); diff --git a/src/resources/markdown_template.js b/src/resources/markdown_template.js index 1ea3b30b..c6c1dbdf 100644 --- a/src/resources/markdown_template.js +++ b/src/resources/markdown_template.js @@ -41,11 +41,23 @@ new QWebChannel(qt.webChannelTransport, } }); +var g_muteScroll = false; + var scrollToAnchor = function(anchor) { + g_muteScroll = true; + if (!anchor) { + window.scrollTo(0, 0); + g_muteScroll = false; + return; + } + var anc = document.getElementById(anchor); if (anc != null) { anc.scrollIntoView(); } + + // Disable scroll temporarily. + setTimeout("g_muteScroll = false", 100); }; window.onwheel = function(e) { @@ -57,13 +69,19 @@ window.onwheel = function(e) { } window.onscroll = function() { + if (g_muteScroll) { + return; + } + var scrollTop = document.documentElement.scrollTop || document.body.scrollTop || window.pageYOffset; var eles = document.querySelectorAll("h1, h2, h3, h4, h5, h6"); if (eles.length == 0) { + content.setHeader(""); return; } - var curIdx = 0; + + var curIdx = -1; var biaScrollTop = scrollTop + 50; for (var i = 0; i < eles.length; ++i) { if (biaScrollTop >= eles[i].offsetTop) { @@ -73,10 +91,12 @@ window.onscroll = function() { } } - var curHeader = eles[curIdx].getAttribute("id"); - if (curHeader != null) { - content.setHeader(curHeader); + var curHeader = null; + if (curIdx != -1) { + curHeader = eles[curIdx].getAttribute("id"); } + + content.setHeader(curHeader ? curHeader : ""); }; document.onkeydown = function(e) { @@ -326,3 +346,106 @@ var escapeHtml = function(text) { return text.replace(/[&<>"']/g, function(m) { return map[m]; }); }; + +// Return the topest level of @toc, starting from 1. +var baseLevelOfToc = function(p_toc) { + var level = -1; + for (i in p_toc) { + if (level == -1) { + level = p_toc[i].level; + } else if (level > p_toc[i].level) { + level = p_toc[i].level; + } + } + + if (level == -1) { + level = 1; + } + + return level; +}; + +// Handle wrong title levels, such as '#' followed by '###' +var toPerfectToc = function(p_toc, p_baseLevel) { + var i; + var curLevel = p_baseLevel - 1; + var perfToc = []; + for (i in p_toc) { + var item = p_toc[i]; + + // Insert empty header. + while (item.level > curLevel + 1) { + curLevel += 1; + var tmp = { level: curLevel, + anchor: '', + title: '[EMPTY]' + }; + perfToc.push(tmp); + } + + perfToc.push(item); + curLevel = item.level; + } + + return perfToc; +}; + +var itemToHtml = function(item) { + return '' + item.title + ''; +}; + +// Turn a perfect toc to a tree using ') { + curLevel--; + } + } + front += ''; + front += '
  • '; + front += itemToHtml(item); + } + } + while (ending.length > 0) { + front += ending.pop(); + } + front = front.replace("
  • ", ""); + front = ''; + return front; +}; + +var handleToc = function(needToc) { + var baseLevel = baseLevelOfToc(toc); + var tocTree = tocToTree(toPerfectToc(toc, baseLevel), baseLevel); + content.setToc(tocTree, baseLevel); + + // Add it to html + if (needToc) { + var eles = document.getElementsByClassName('vnote-toc'); + for (var i = 0; i < eles.length; ++i) { + eles[i].innerHTML = tocTree; + } + } +}; + diff --git a/src/resources/marked.js b/src/resources/marked.js index 5bdd4e8a..55300c9f 100644 --- a/src/resources/marked.js +++ b/src/resources/marked.js @@ -36,85 +36,6 @@ var markdownToHtml = function(markdown, needToc) { } }; -// Handle wrong title levels, such as '#' followed by '###' -var toPerfectToc = function(toc) { - var i; - var curLevel = 1; - var perfToc = []; - for (i in toc) { - var item = toc[i]; - while (item.level > curLevel + 1) { - curLevel += 1; - var tmp = { level: curLevel, - anchor: item.anchor, - title: '[EMPTY]' - }; - perfToc.push(tmp); - } - perfToc.push(item); - curLevel = item.level; - } - return perfToc; -}; - -var itemToHtml = function(item) { - return '' + item.title + ''; -}; - -// Turn a perfect toc to a tree using ') { - curLevel--; - } - } - front += ''; - front += '
  • '; - front += itemToHtml(item); - } - } - while (ending.length > 0) { - front += ending.pop(); - } - front = front.replace("
  • ", ""); - front = ''; - return front; -}; - -var handleToc = function(needToc) { - var tocTree = tocToTree(toPerfectToc(toc)); - content.setToc(tocTree); - - // Add it to html - if (needToc) { - var eles = document.getElementsByClassName('vnote-toc'); - for (var i = 0; i < eles.length; ++i) { - eles[i].innerHTML = tocTree; - } - } -}; - var mdHasTocSection = function(markdown) { var n = markdown.search(/(\n|^)\[toc\]/i); return n != -1; diff --git a/src/resources/showdown.js b/src/resources/showdown.js index cec52b3e..e05f14ec 100644 --- a/src/resources/showdown.js +++ b/src/resources/showdown.js @@ -44,85 +44,6 @@ var markdownToHtml = function(markdown, needToc) { } }; -// Handle wrong title levels, such as '#' followed by '###' -var toPerfectToc = function(toc) { - var i; - var curLevel = 1; - var perfToc = []; - for (i in toc) { - var item = toc[i]; - while (item.level > curLevel + 1) { - curLevel += 1; - var tmp = { level: curLevel, - anchor: item.anchor, - title: '[EMPTY]' - }; - perfToc.push(tmp); - } - perfToc.push(item); - curLevel = item.level; - } - return perfToc; -}; - -var itemToHtml = function(item) { - return '' + item.title + ''; -}; - -// Turn a perfect toc to a tree using ') { - curLevel--; - } - } - front += ''; - front += '
  • '; - front += itemToHtml(item); - } - } - while (ending.length > 0) { - front += ending.pop(); - } - front = front.replace("
  • ", ""); - front = ''; - return front; -}; - -var handleToc = function(needToc) { - var tocTree = tocToTree(toPerfectToc(toc)); - content.setToc(tocTree); - - // Add it to html - if (needToc) { - var eles = document.getElementsByClassName('vnote-toc'); - for (var i = 0; i < eles.length; ++i) { - eles[i].innerHTML = tocTree; - } - } -}; - var mdHasTocSection = function(markdown) { var n = markdown.search(/(\n|^)\[toc\]/i); return n != -1; diff --git a/src/vconstants.h b/src/vconstants.h index f8ad17b9..6317dceb 100644 --- a/src/vconstants.h +++ b/src/vconstants.h @@ -32,4 +32,6 @@ namespace DirConfig static const QString c_imageFolder = "image_folder"; static const QString c_name = "name"; } + +static const QString c_emptyHeaderName = "[EMPTY]"; #endif diff --git a/src/vdocument.cpp b/src/vdocument.cpp index 98d4cc23..92ea9d19 100644 --- a/src/vdocument.cpp +++ b/src/vdocument.cpp @@ -14,7 +14,7 @@ void VDocument::updateText() } } -void VDocument::setToc(const QString &toc) +void VDocument::setToc(const QString &toc, int /* baseLevel */) { if (toc == m_toc) { return; @@ -30,6 +30,8 @@ QString VDocument::getToc() void VDocument::scrollToAnchor(const QString &anchor) { + m_header = anchor; + emit requestScrollToAnchor(anchor); } @@ -38,6 +40,7 @@ void VDocument::setHeader(const QString &anchor) if (anchor == m_header) { return; } + m_header = anchor; emit headerChanged(m_header); } diff --git a/src/vdocument.h b/src/vdocument.h index abae4062..40b00bbb 100644 --- a/src/vdocument.h +++ b/src/vdocument.h @@ -27,8 +27,16 @@ public: public slots: // Will be called in the HTML side - void setToc(const QString &toc); + + // @toc: the HTML of the TOC. + // @baseLevel: the base level of @toc, starting from 1. It is the top level + // in the @toc. + void setToc(const QString &toc, int baseLevel); + + // When the Web view has been scrolled, it will signal current header anchor. + // Empty @anchor to indicate an invalid header. void setHeader(const QString &anchor); + void setLog(const QString &p_log); void keyPressEvent(int p_key, bool p_ctrl, bool p_shift); void updateText(); diff --git a/src/vhtmltab.cpp b/src/vhtmltab.cpp index 85e47f7d..8576684b 100644 --- a/src/vhtmltab.cpp +++ b/src/vhtmltab.cpp @@ -195,7 +195,7 @@ void VHtmlTab::discardAndRead() readFile(); } -void VHtmlTab::scrollToAnchor(const VAnchor &p_anchor) +void VHtmlTab::scrollToAnchor(const VAnchor & /* p_anchor */) { } @@ -240,7 +240,7 @@ void VHtmlTab::clearSearchedWordHighlight() m_editor->clearSearchedWordHighlight(); } -void VHtmlTab::zoom(bool p_zoomIn, qreal p_step) +void VHtmlTab::zoom(bool /* p_zoomIn */, qreal /* p_step */) { } diff --git a/src/vmdedit.cpp b/src/vmdedit.cpp index 6eb7c495..f52e9a77 100644 --- a/src/vmdedit.cpp +++ b/src/vmdedit.cpp @@ -244,56 +244,87 @@ void VMdEdit::clearUnusedImages() void VMdEdit::updateCurHeader() { - int curHeader = 0; - QTextCursor cursor(this->textCursor()); - int curLine = cursor.block().firstLineNumber(); + if (m_headers.isEmpty()) { + return; + } + + int curLine = textCursor().block().firstLineNumber(); int i = 0; for (i = m_headers.size() - 1; i >= 0; --i) { - if (m_headers[i].lineNumber <= curLine) { - curHeader = m_headers[i].lineNumber; - break; + if (!m_headers[i].isEmpty()) { + if (m_headers[i].lineNumber <= curLine) { + break; + } } } - emit curHeaderChanged(curHeader, i == -1 ? 0 : i); + + if (i == -1) { + emit curHeaderChanged(VAnchor(m_file, "", -1, -1)); + return; + } + + V_ASSERT(m_headers[i].index == i); + + emit curHeaderChanged(VAnchor(m_file, "", m_headers[i].lineNumber, m_headers[i].index)); } void VMdEdit::generateEditOutline() { QTextDocument *doc = document(); + m_headers.clear(); + + QVector headers; + // Assume that each block contains only one line // Only support # syntax for now QRegExp headerReg("(#{1,6})\\s*(\\S.*)"); // Need to trim the spaces - int lastLevel = 0; + int baseLevel = -1; for (QTextBlock block = doc->begin(); block != doc->end(); block = block.next()) { - Q_ASSERT(block.lineCount() == 1); + V_ASSERT(block.lineCount() == 1); if ((block.userState() == HighlightBlockState::Normal) && headerReg.exactMatch(block.text())) { int level = headerReg.cap(1).length(); VHeader header(level, headerReg.cap(2).trimmed(), - "", block.firstLineNumber()); - while (level > lastLevel + 1) { - // Insert empty level. - m_headers.append(VHeader(++lastLevel, "[EMPTY]", - "", block.firstLineNumber())); + "", block.firstLineNumber(), headers.size()); + headers.append(header); + + if (baseLevel == -1) { + baseLevel = level; + } else if (baseLevel > level) { + baseLevel = level; } - m_headers.append(header); - lastLevel = level; } } + int curLevel = baseLevel - 1; + for (auto & item : headers) { + while (item.level > curLevel + 1) { + curLevel += 1; + + // Insert empty level which is an invalid header. + m_headers.append(VHeader(curLevel, c_emptyHeaderName, "", -1, m_headers.size())); + } + + item.index = m_headers.size(); + m_headers.append(item); + curLevel = item.level; + } + emit headersChanged(m_headers); + updateCurHeader(); } -void VMdEdit::scrollToHeader(int p_headerIndex) +void VMdEdit::scrollToHeader(const VAnchor &p_anchor) { - Q_ASSERT(p_headerIndex >= 0); - if (p_headerIndex < m_headers.size()) { - int line = m_headers[p_headerIndex].lineNumber; - qDebug() << "scroll editor to" << p_headerIndex << "line" << line; - scrollToLine(line); + if (p_anchor.lineNumber == -1 + || p_anchor.m_outlineIndex < 0 + || p_anchor.m_outlineIndex >= m_headers.size()) { + return; } + + scrollToLine(p_anchor.lineNumber); } QString VMdEdit::toPlainTextWithoutImg() const @@ -407,3 +438,8 @@ void VMdEdit::resizeEvent(QResizeEvent *p_event) VEdit::resizeEvent(p_event); } + +const QVector &VMdEdit::getHeaders() const +{ + return m_headers; +} diff --git a/src/vmdedit.h b/src/vmdedit.h index f5c61f1b..03a2dc16 100644 --- a/src/vmdedit.h +++ b/src/vmdedit.h @@ -32,19 +32,27 @@ public: // @p_path is the absolute path of the inserted image. void imageInserted(const QString &p_path); - // Scroll to m_headers[p_headerIndex]. - void scrollToHeader(int p_headerIndex); + void scrollToHeader(const VAnchor &p_anchor); + // Like toPlainText(), but remove special blocks containing images. QString toPlainTextWithoutImg() const; + const QVector &getHeaders() const; + signals: void headersChanged(const QVector &headers); - void curHeaderChanged(int p_lineNumber, int p_outlineIndex); + + // Signal when current header change. + void curHeaderChanged(VAnchor p_anchor); + void statusChanged(); private slots: void generateEditOutline(); + + // When there is no header in current cursor, will signal an invalid header. void updateCurHeader(); + void handleEditStateChanged(KeyState p_state); void handleSelectionChanged(); void handleClipboardChanged(QClipboard::Mode p_mode); diff --git a/src/vmdtab.cpp b/src/vmdtab.cpp index 0385e639..760325a8 100644 --- a/src/vmdtab.cpp +++ b/src/vmdtab.cpp @@ -50,8 +50,8 @@ void VMdTab::setupUI() this, &VMdTab::updateTocFromHeaders); connect(dynamic_cast(m_editor), &VMdEdit::statusChanged, this, &VMdTab::noticeStatusChanged); - connect(m_editor, SIGNAL(curHeaderChanged(int, int)), - this, SLOT(updateCurHeader(int, int))); + connect(m_editor, SIGNAL(curHeaderChanged(VAnchor)), + this, SLOT(updateCurHeader(VAnchor))); connect(m_editor, &VEdit::textChanged, this, &VMdTab::handleTextChanged); connect(m_editor, &VEdit::saveAndRead, @@ -91,6 +91,7 @@ void VMdTab::showFileReadMode() m_isEditMode = false; int outlineIndex = m_curHeader.m_outlineIndex; + if (m_mdConType == MarkdownConverterType::Hoedown) { viewWebByConverter(); } else { @@ -100,6 +101,7 @@ void VMdTab::showFileReadMode() m_stacks->setCurrentWidget(m_webViewer); clearSearchedWordHighlight(); + scrollWebViewToHeader(outlineIndex); noticeStatusChanged(); @@ -107,14 +109,20 @@ void VMdTab::showFileReadMode() void VMdTab::scrollWebViewToHeader(int p_outlineIndex) { - V_ASSERT(p_outlineIndex >= 0); + QString anchor; - if (p_outlineIndex < m_toc.headers.size()) { - QString anchor = m_toc.headers[p_outlineIndex].anchor; - if (!anchor.isEmpty()) { - m_document->scrollToAnchor(anchor.mid(1)); - } + m_curHeader = VAnchor(m_file, anchor, -1, p_outlineIndex); + + if (p_outlineIndex < m_toc.headers.size() && p_outlineIndex >= 0) { + QString tmp = m_toc.headers[p_outlineIndex].anchor; + V_ASSERT(!tmp.isEmpty()); + m_curHeader.anchor = tmp; + anchor = tmp.mid(1); } + + m_document->scrollToAnchor(anchor); + + emit curHeaderChanged(m_curHeader); } void VMdTab::viewWebByConverter() @@ -136,14 +144,28 @@ void VMdTab::showFileEditMode() m_isEditMode = true; + VMdEdit *mdEdit = dynamic_cast(m_editor); + V_ASSERT(mdEdit); + // beginEdit() may change m_curHeader. int outlineIndex = m_curHeader.m_outlineIndex; + int lineNumber = -1; + auto headers = mdEdit->getHeaders(); + if (outlineIndex < 0 || outlineIndex >= headers.size()) { + lineNumber = -1; + outlineIndex = -1; + } else { + lineNumber = headers[outlineIndex].lineNumber; + } - m_editor->beginEdit(); - m_stacks->setCurrentWidget(m_editor); + VAnchor anchor(m_file, "", lineNumber, outlineIndex); - dynamic_cast(m_editor)->scrollToHeader(outlineIndex); - m_editor->setFocus(); + mdEdit->beginEdit(); + m_stacks->setCurrentWidget(mdEdit); + + mdEdit->scrollToHeader(anchor); + + mdEdit->setFocus(); noticeStatusChanged(); } @@ -368,7 +390,7 @@ static void parseTocLi(QXmlStreamReader &p_xml, QVector &p_headers, int return; } - VHeader header(p_level, name, anchor, -1); + VHeader header(p_level, name, anchor, -1, p_headers.size()); p_headers.append(header); } else { // Error @@ -376,7 +398,7 @@ static void parseTocLi(QXmlStreamReader &p_xml, QVector &p_headers, int } } else if (p_xml.name() == "ul") { // Such as header 3 under header 1 directly - VHeader header(p_level, "[EMPTY]", "#", -1); + VHeader header(p_level, c_emptyHeaderName, "#", -1, p_headers.size()); p_headers.append(header); parseTocUl(p_xml, p_headers, p_level + 1); } else { @@ -478,10 +500,9 @@ void VMdTab::scrollToAnchor(const VAnchor &p_anchor) } m_curHeader = p_anchor; + if (m_isEditMode) { - if (p_anchor.lineNumber > -1) { - m_editor->scrollToLine(p_anchor.lineNumber); - } + dynamic_cast(m_editor)->scrollToHeader(p_anchor); } else { if (!p_anchor.anchor.isEmpty()) { m_document->scrollToAnchor(p_anchor.anchor.mid(1)); @@ -500,26 +521,31 @@ void VMdTab::updateCurHeader(const QString &p_anchor) const QVector &headers = m_toc.headers; for (int i = 0; i < headers.size(); ++i) { if (headers[i].anchor == m_curHeader.anchor) { - m_curHeader.m_outlineIndex = i; + V_ASSERT(headers[i].index == i); + m_curHeader.m_outlineIndex = headers[i].index; break; } } - - emit curHeaderChanged(m_curHeader); } + + emit curHeaderChanged(m_curHeader); } -void VMdTab::updateCurHeader(int p_lineNumber, int p_outlineIndex) +void VMdTab::updateCurHeader(VAnchor p_anchor) { - if (!m_isEditMode || m_curHeader.lineNumber == p_lineNumber) { - return; + if (m_isEditMode) { + if (!p_anchor.anchor.isEmpty() || p_anchor.lineNumber == m_curHeader.lineNumber) { + return; + } + } else { + if (p_anchor.lineNumber != -1 || p_anchor.anchor == m_curHeader.anchor) { + return; + } } - m_curHeader = VAnchor(m_file, "", p_lineNumber); - m_curHeader.m_outlineIndex = p_outlineIndex; - if (p_lineNumber > -1) { - emit curHeaderChanged(m_curHeader); - } + m_curHeader = p_anchor; + + emit curHeaderChanged(m_curHeader); } void VMdTab::insertImage() diff --git a/src/vmdtab.h b/src/vmdtab.h index 82d04220..b3050232 100644 --- a/src/vmdtab.h +++ b/src/vmdtab.h @@ -76,7 +76,7 @@ private slots: void updateCurHeader(const QString &p_anchor); // Editor requests to update current header. - void updateCurHeader(int p_lineNumber, int p_outlineIndex); + void updateCurHeader(VAnchor p_anchor); // Handle key press event in Web view. void handleWebKeyPressed(int p_key, bool p_ctrl, bool p_shift); diff --git a/src/voutline.cpp b/src/voutline.cpp index 7158424e..212a3d87 100644 --- a/src/voutline.cpp +++ b/src/voutline.cpp @@ -1,7 +1,6 @@ #include #include #include -#include #include #include #include @@ -9,8 +8,10 @@ #include "vtoc.h" #include "utils/vutils.h" #include "vnote.h" +#include "vconfigmanager.h" extern VNote *g_vnote; +extern VConfigManager vconfig; VOutline::VOutline(QWidget *parent) : QTreeWidget(parent), VNavigationMode() @@ -23,23 +24,38 @@ VOutline::VOutline(QWidget *parent) this, &VOutline::handleCurItemChanged); } +void VOutline::checkOutline(const VToc &p_toc) const +{ + const QVector &headers = p_toc.headers; + + for (int i = 0; i < headers.size(); ++i) { + V_ASSERT(headers[i].index == i); + } +} + void VOutline::updateOutline(const VToc &toc) { // Clear current header curHeader = VAnchor(); + + checkOutline(toc); + outline = toc; - updateTreeFromOutline(outline); + + updateTreeFromOutline(); + expandTree(); } -void VOutline::updateTreeFromOutline(const VToc &toc) +void VOutline::updateTreeFromOutline() { clear(); - if (!toc.valid) { + if (!outline.valid) { return; } - const QVector &headers = toc.headers; + + const QVector &headers = outline.headers; int idx = 0; updateTreeByLevel(headers, idx, NULL, NULL, 1); } @@ -56,13 +72,8 @@ void VOutline::updateTreeByLevel(const QVector &headers, int &index, } else { item = new QTreeWidgetItem(this); } - QJsonObject itemJson; - itemJson["anchor"] = header.anchor; - itemJson["line_number"] = header.lineNumber; - itemJson["outline_index"] = index; - item->setData(0, Qt::UserRole, itemJson); - item->setText(0, header.name); - item->setToolTip(0, header.name); + + fillItem(item, header); last = item; ++index; @@ -74,6 +85,17 @@ void VOutline::updateTreeByLevel(const QVector &headers, int &index, } } +void VOutline::fillItem(QTreeWidgetItem *p_item, const VHeader &p_header) +{ + p_item->setData(0, Qt::UserRole, p_header.index); + p_item->setText(0, p_header.name); + p_item->setToolTip(0, p_header.name); + + if (p_header.isEmpty()) { + p_item->setForeground(0, QColor("grey")); + } +} + void VOutline::expandTree() { if (topLevelItemCount() == 0) { @@ -87,20 +109,22 @@ void VOutline::handleCurItemChanged(QTreeWidgetItem *p_curItem, QTreeWidgetItem if (!p_curItem) { return; } - QJsonObject itemJson = p_curItem->data(0, Qt::UserRole).toJsonObject(); - QString anchor = itemJson["anchor"].toString(); - int lineNumber = itemJson["line_number"].toInt(); - int outlineIndex = itemJson["outline_index"].toInt(); - VAnchor tmp; - tmp.m_file = outline.m_file; - tmp.anchor = anchor; - tmp.lineNumber = lineNumber; - tmp.m_outlineIndex = outlineIndex; + + const VHeader *header = getHeaderFromItem(p_curItem); + if (!header) { + return; + } + + VAnchor tmp(outline.m_file, header->anchor, header->lineNumber, header->index); if (tmp == curHeader) { return; } + curHeader = tmp; - emit outlineItemActivated(curHeader); + + if (!header->isEmpty()) { + emit outlineItemActivated(curHeader); + } } void VOutline::updateCurHeader(const VAnchor &anchor) @@ -108,17 +132,24 @@ void VOutline::updateCurHeader(const VAnchor &anchor) if (anchor == curHeader) { return; } + curHeader = anchor; if (outline.type == VHeaderType::Anchor) { selectAnchor(anchor.anchor); } else { - // Select by lineNumber + // Select by lineNumber. selectLineNumber(anchor.lineNumber); } } void VOutline::selectAnchor(const QString &anchor) { + setCurrentItem(NULL); + + if (anchor.isEmpty()) { + return; + } + int nrTop = topLevelItemCount(); for (int i = 0; i < nrTop; ++i) { if (selectAnchorOne(topLevelItem(i), anchor)) { @@ -132,9 +163,13 @@ bool VOutline::selectAnchorOne(QTreeWidgetItem *item, const QString &anchor) if (!item) { return false; } - QJsonObject itemJson = item->data(0, Qt::UserRole).toJsonObject(); - QString itemAnchor = itemJson["anchor"].toString(); - if (itemAnchor == anchor) { + + const VHeader *header = getHeaderFromItem(item); + if (!header) { + return false; + } + + if (header->anchor == anchor) { setCurrentItem(item); return true; } @@ -150,6 +185,12 @@ bool VOutline::selectAnchorOne(QTreeWidgetItem *item, const QString &anchor) void VOutline::selectLineNumber(int lineNumber) { + setCurrentItem(NULL); + + if (lineNumber == -1) { + return; + } + int nrTop = topLevelItemCount(); for (int i = 0; i < nrTop; ++i) { if (selectLineNumberOne(topLevelItem(i), lineNumber)) { @@ -163,9 +204,13 @@ bool VOutline::selectLineNumberOne(QTreeWidgetItem *item, int lineNumber) if (!item) { return false; } - QJsonObject itemJson = item->data(0, Qt::UserRole).toJsonObject(); - int itemLineNum = itemJson["line_number"].toInt(); - if (itemLineNum == lineNumber) { + + const VHeader *header = getHeaderFromItem(item); + if (!header) { + return false; + } + + if (header->lineNumber == lineNumber) { // Select this item setCurrentItem(item); return true; @@ -329,3 +374,18 @@ QList VOutline::getVisibleChildItems(const QTreeWidgetItem *p } return items; } + +const VHeader *VOutline::getHeaderFromItem(QTreeWidgetItem *p_item) const +{ + const VHeader *header = NULL; + + int index = p_item->data(0, Qt::UserRole).toInt(); + if (index < 0 || index >= outline.headers.size()) { + return header; + } + + header = &(outline.headers[index]); + Q_ASSERT(header->index == index); + + return header; +} diff --git a/src/voutline.h b/src/voutline.h index 04c4f5dc..6867e1c0 100644 --- a/src/voutline.h +++ b/src/voutline.h @@ -36,9 +36,13 @@ private slots: void handleCurItemChanged(QTreeWidgetItem *p_curItem, QTreeWidgetItem *p_preItem); private: - void updateTreeFromOutline(const VToc &toc); + // Update tree according to outline. + void updateTreeFromOutline(); + + // @index: the index in @headers. void updateTreeByLevel(const QVector &headers, int &index, QTreeWidgetItem *parent, QTreeWidgetItem *last, int level); + void expandTree(); void selectAnchor(const QString &anchor); bool selectAnchorOne(QTreeWidgetItem *item, const QString &anchor); @@ -47,6 +51,15 @@ private: QList getVisibleItems() const; QList getVisibleChildItems(const QTreeWidgetItem *p_item) const; + // Fill the info of @p_item. + void fillItem(QTreeWidgetItem *p_item, const VHeader &p_header); + + // Check if @p_toc is valid. + void checkOutline(const VToc &p_toc) const; + + // Return NULL if no corresponding header in outline. + const VHeader *getHeaderFromItem(QTreeWidgetItem *p_item) const; + VToc outline; VAnchor curHeader; diff --git a/src/vtoc.h b/src/vtoc.h index 8b0227c3..36f66eef 100644 --- a/src/vtoc.h +++ b/src/vtoc.h @@ -14,30 +14,53 @@ enum VHeaderType struct VHeader { - VHeader() : level(1), lineNumber(-1) {} - VHeader(int level, const QString &name, const QString &anchor, int lineNumber) - : level(level), name(name), anchor(anchor), lineNumber(lineNumber) {} + VHeader() : level(1), lineNumber(-1), index(-1) {} + VHeader(int level, const QString &name, const QString &anchor, int lineNumber, int index) + : level(level), name(name), anchor(anchor), lineNumber(lineNumber), index(index) {} int level; QString name; QString anchor; int lineNumber; + + // Index in the outline, based on 0. + int index; + + // Whether it is an empty (fake) header. + bool isEmpty() const + { + if (anchor.isEmpty()) { + return lineNumber == -1; + } else { + return anchor == "#"; + } + } }; struct VAnchor { - VAnchor() : lineNumber(-1), m_outlineIndex(0) {} - VAnchor(const VFile *file, const QString &anchor, int lineNumber) - : m_file(file), anchor(anchor), lineNumber(lineNumber), m_outlineIndex(0) {} + VAnchor() : m_file(NULL), lineNumber(-1), m_outlineIndex(-1) {} + + VAnchor(const VFile *file, const QString &anchor, int lineNumber, int outlineIndex = -1) + : m_file(file), anchor(anchor), lineNumber(lineNumber), m_outlineIndex(outlineIndex) {} + + // The file this anchor points to. const VFile *m_file; + + // The string anchor. For Web view. QString anchor; + + // The line number anchor. For edit view. int lineNumber; - // Index of this anchor in VToc outline. + + // Index of the header for this anchor in VToc outline. + // Used to translate current header between read and edit mode. int m_outlineIndex; bool operator==(const VAnchor &p_anchor) const { return (p_anchor.m_file == m_file && p_anchor.anchor == anchor - && p_anchor.lineNumber == lineNumber); + && p_anchor.lineNumber == lineNumber + && p_anchor.m_outlineIndex == m_outlineIndex); } };