refactor TOC logics

This commit is contained in:
Le Tan 2017-05-31 19:42:52 +08:00
parent 3b011cd8de
commit 97051badf0
15 changed files with 402 additions and 337 deletions

View File

@ -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 '<a href="#' + item.anchor + '">' + item.title + '</a>';
};
// Turn a perfect toc to a tree using <ul>
var tocToTree = function(toc) {
var i;
var front = '<li>';
var ending = ['</li>'];
var curLevel = 1;
for (i in toc) {
var item = toc[i];
if (item.level == curLevel) {
front += '</li>';
front += '<li>';
front += itemToHtml(item);
} else if (item.level > curLevel) {
// assert(item.level - curLevel == 1)
front += '<ul>';
ending.push('</ul>');
front += '<li>';
front += itemToHtml(item);
ending.push('</li>');
curLevel = item.level;
} else {
while (item.level < curLevel) {
var ele = ending.pop();
front += ele;
if (ele == '</ul>') {
curLevel--;
}
}
front += '</li>';
front += '<li>';
front += itemToHtml(item);
}
}
while (ending.length > 0) {
front += ending.pop();
}
front = front.replace("<li></li>", "");
front = '<ul>' + front + '</ul>';
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);

View File

@ -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 '<a href="#' + item.anchor + '">' + item.title + '</a>';
};
// Turn a perfect toc to a tree using <ul>
var tocToTree = function(p_toc, p_baseLevel) {
var i;
var front = '<li>';
var ending = ['</li>'];
var curLevel = p_baseLevel;
for (i in p_toc) {
var item = p_toc[i];
if (item.level == curLevel) {
front += '</li>';
front += '<li>';
front += itemToHtml(item);
} else if (item.level > curLevel) {
// assert(item.level - curLevel == 1)
front += '<ul>';
ending.push('</ul>');
front += '<li>';
front += itemToHtml(item);
ending.push('</li>');
curLevel = item.level;
} else {
while (item.level < curLevel) {
var ele = ending.pop();
front += ele;
if (ele == '</ul>') {
curLevel--;
}
}
front += '</li>';
front += '<li>';
front += itemToHtml(item);
}
}
while (ending.length > 0) {
front += ending.pop();
}
front = front.replace("<li></li>", "");
front = '<ul>' + front + '</ul>';
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;
}
}
};

View File

@ -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 '<a href="#' + item.anchor + '">' + item.title + '</a>';
};
// Turn a perfect toc to a tree using <ul>
var tocToTree = function(toc) {
var i;
var front = '<li>';
var ending = ['</li>'];
var curLevel = 1;
for (i in toc) {
var item = toc[i];
if (item.level == curLevel) {
front += '</li>';
front += '<li>';
front += itemToHtml(item);
} else if (item.level > curLevel) {
// assert(item.level - curLevel == 1)
front += '<ul>';
ending.push('</ul>');
front += '<li>';
front += itemToHtml(item);
ending.push('</li>');
curLevel = item.level;
} else {
while (item.level < curLevel) {
var ele = ending.pop();
front += ele;
if (ele == '</ul>') {
curLevel--;
}
}
front += '</li>';
front += '<li>';
front += itemToHtml(item);
}
}
while (ending.length > 0) {
front += ending.pop();
}
front = front.replace("<li></li>", "");
front = '<ul>' + front + '</ul>';
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;

View File

@ -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 '<a href="#' + item.anchor + '">' + item.title + '</a>';
};
// Turn a perfect toc to a tree using <ul>
var tocToTree = function(toc) {
var i;
var front = '<li>';
var ending = ['</li>'];
var curLevel = 1;
for (i in toc) {
var item = toc[i];
if (item.level == curLevel) {
front += '</li>';
front += '<li>';
front += itemToHtml(item);
} else if (item.level > curLevel) {
// assert(item.level - curLevel == 1)
front += '<ul>';
ending.push('</ul>');
front += '<li>';
front += itemToHtml(item);
ending.push('</li>');
curLevel = item.level;
} else {
while (item.level < curLevel) {
var ele = ending.pop();
front += ele;
if (ele == '</ul>') {
curLevel--;
}
}
front += '</li>';
front += '<li>';
front += itemToHtml(item);
}
}
while (ending.length > 0) {
front += ending.pop();
}
front = front.replace("<li></li>", "");
front = '<ul>' + front + '</ul>';
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;

View File

@ -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

View File

@ -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);
}

View File

@ -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();

View File

@ -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 */)
{
}

View File

@ -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<VHeader> 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<VHeader> &VMdEdit::getHeaders() const
{
return m_headers;
}

View File

@ -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<VHeader> &getHeaders() const;
signals:
void headersChanged(const QVector<VHeader> &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);

View File

@ -50,8 +50,8 @@ void VMdTab::setupUI()
this, &VMdTab::updateTocFromHeaders);
connect(dynamic_cast<VMdEdit *>(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<VMdEdit *>(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<VMdEdit *>(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<VHeader> &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<VHeader> &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<VMdEdit *>(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<VHeader> &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()

View File

@ -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);

View File

@ -1,7 +1,6 @@
#include <QDebug>
#include <QVector>
#include <QString>
#include <QJsonObject>
#include <QKeyEvent>
#include <QLabel>
#include <QCoreApplication>
@ -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<VHeader> &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<VHeader> &headers = toc.headers;
const QVector<VHeader> &headers = outline.headers;
int idx = 0;
updateTreeByLevel(headers, idx, NULL, NULL, 1);
}
@ -56,13 +72,8 @@ void VOutline::updateTreeByLevel(const QVector<VHeader> &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<VHeader> &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<QTreeWidgetItem *> 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;
}

View File

@ -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<VHeader> &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<QTreeWidgetItem *> getVisibleItems() const;
QList<QTreeWidgetItem *> 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;

View File

@ -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);
}
};