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 updateText = function(text) {
var needToc = mdHasTocSection(text); var needToc = mdHasTocSection(text);
var html = markdownToHtml(text, needToc); var html = markdownToHtml(text, needToc);

View File

@ -41,11 +41,23 @@ new QWebChannel(qt.webChannelTransport,
} }
}); });
var g_muteScroll = false;
var scrollToAnchor = function(anchor) { var scrollToAnchor = function(anchor) {
g_muteScroll = true;
if (!anchor) {
window.scrollTo(0, 0);
g_muteScroll = false;
return;
}
var anc = document.getElementById(anchor); var anc = document.getElementById(anchor);
if (anc != null) { if (anc != null) {
anc.scrollIntoView(); anc.scrollIntoView();
} }
// Disable scroll temporarily.
setTimeout("g_muteScroll = false", 100);
}; };
window.onwheel = function(e) { window.onwheel = function(e) {
@ -57,13 +69,19 @@ window.onwheel = function(e) {
} }
window.onscroll = function() { window.onscroll = function() {
if (g_muteScroll) {
return;
}
var scrollTop = document.documentElement.scrollTop || document.body.scrollTop || window.pageYOffset; var scrollTop = document.documentElement.scrollTop || document.body.scrollTop || window.pageYOffset;
var eles = document.querySelectorAll("h1, h2, h3, h4, h5, h6"); var eles = document.querySelectorAll("h1, h2, h3, h4, h5, h6");
if (eles.length == 0) { if (eles.length == 0) {
content.setHeader("");
return; return;
} }
var curIdx = 0;
var curIdx = -1;
var biaScrollTop = scrollTop + 50; var biaScrollTop = scrollTop + 50;
for (var i = 0; i < eles.length; ++i) { for (var i = 0; i < eles.length; ++i) {
if (biaScrollTop >= eles[i].offsetTop) { if (biaScrollTop >= eles[i].offsetTop) {
@ -73,10 +91,12 @@ window.onscroll = function() {
} }
} }
var curHeader = eles[curIdx].getAttribute("id"); var curHeader = null;
if (curHeader != null) { if (curIdx != -1) {
content.setHeader(curHeader); curHeader = eles[curIdx].getAttribute("id");
} }
content.setHeader(curHeader ? curHeader : "");
}; };
document.onkeydown = function(e) { document.onkeydown = function(e) {
@ -326,3 +346,106 @@ var escapeHtml = function(text) {
return text.replace(/[&<>"']/g, function(m) { return map[m]; }); 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 mdHasTocSection = function(markdown) {
var n = markdown.search(/(\n|^)\[toc\]/i); var n = markdown.search(/(\n|^)\[toc\]/i);
return n != -1; 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 mdHasTocSection = function(markdown) {
var n = markdown.search(/(\n|^)\[toc\]/i); var n = markdown.search(/(\n|^)\[toc\]/i);
return n != -1; return n != -1;

View File

@ -32,4 +32,6 @@ namespace DirConfig
static const QString c_imageFolder = "image_folder"; static const QString c_imageFolder = "image_folder";
static const QString c_name = "name"; static const QString c_name = "name";
} }
static const QString c_emptyHeaderName = "[EMPTY]";
#endif #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) { if (toc == m_toc) {
return; return;
@ -30,6 +30,8 @@ QString VDocument::getToc()
void VDocument::scrollToAnchor(const QString &anchor) void VDocument::scrollToAnchor(const QString &anchor)
{ {
m_header = anchor;
emit requestScrollToAnchor(anchor); emit requestScrollToAnchor(anchor);
} }
@ -38,6 +40,7 @@ void VDocument::setHeader(const QString &anchor)
if (anchor == m_header) { if (anchor == m_header) {
return; return;
} }
m_header = anchor; m_header = anchor;
emit headerChanged(m_header); emit headerChanged(m_header);
} }

View File

@ -27,8 +27,16 @@ public:
public slots: public slots:
// Will be called in the HTML side // 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 setHeader(const QString &anchor);
void setLog(const QString &p_log); void setLog(const QString &p_log);
void keyPressEvent(int p_key, bool p_ctrl, bool p_shift); void keyPressEvent(int p_key, bool p_ctrl, bool p_shift);
void updateText(); void updateText();

View File

@ -195,7 +195,7 @@ void VHtmlTab::discardAndRead()
readFile(); 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(); 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() void VMdEdit::updateCurHeader()
{ {
int curHeader = 0; if (m_headers.isEmpty()) {
QTextCursor cursor(this->textCursor()); return;
int curLine = cursor.block().firstLineNumber(); }
int curLine = textCursor().block().firstLineNumber();
int i = 0; int i = 0;
for (i = m_headers.size() - 1; i >= 0; --i) { for (i = m_headers.size() - 1; i >= 0; --i) {
if (m_headers[i].lineNumber <= curLine) { if (!m_headers[i].isEmpty()) {
curHeader = m_headers[i].lineNumber; if (m_headers[i].lineNumber <= curLine) {
break; 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() void VMdEdit::generateEditOutline()
{ {
QTextDocument *doc = document(); QTextDocument *doc = document();
m_headers.clear(); m_headers.clear();
QVector<VHeader> headers;
// Assume that each block contains only one line // Assume that each block contains only one line
// Only support # syntax for now // Only support # syntax for now
QRegExp headerReg("(#{1,6})\\s*(\\S.*)"); // Need to trim the spaces 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()) { 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) && if ((block.userState() == HighlightBlockState::Normal) &&
headerReg.exactMatch(block.text())) { headerReg.exactMatch(block.text())) {
int level = headerReg.cap(1).length(); int level = headerReg.cap(1).length();
VHeader header(level, headerReg.cap(2).trimmed(), VHeader header(level, headerReg.cap(2).trimmed(),
"", block.firstLineNumber()); "", block.firstLineNumber(), headers.size());
while (level > lastLevel + 1) { headers.append(header);
// Insert empty level.
m_headers.append(VHeader(++lastLevel, "[EMPTY]", if (baseLevel == -1) {
"", block.firstLineNumber())); 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); emit headersChanged(m_headers);
updateCurHeader(); updateCurHeader();
} }
void VMdEdit::scrollToHeader(int p_headerIndex) void VMdEdit::scrollToHeader(const VAnchor &p_anchor)
{ {
Q_ASSERT(p_headerIndex >= 0); if (p_anchor.lineNumber == -1
if (p_headerIndex < m_headers.size()) { || p_anchor.m_outlineIndex < 0
int line = m_headers[p_headerIndex].lineNumber; || p_anchor.m_outlineIndex >= m_headers.size()) {
qDebug() << "scroll editor to" << p_headerIndex << "line" << line; return;
scrollToLine(line);
} }
scrollToLine(p_anchor.lineNumber);
} }
QString VMdEdit::toPlainTextWithoutImg() const QString VMdEdit::toPlainTextWithoutImg() const
@ -407,3 +438,8 @@ void VMdEdit::resizeEvent(QResizeEvent *p_event)
VEdit::resizeEvent(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. // @p_path is the absolute path of the inserted image.
void imageInserted(const QString &p_path); void imageInserted(const QString &p_path);
// Scroll to m_headers[p_headerIndex]. void scrollToHeader(const VAnchor &p_anchor);
void scrollToHeader(int p_headerIndex);
// Like toPlainText(), but remove special blocks containing images. // Like toPlainText(), but remove special blocks containing images.
QString toPlainTextWithoutImg() const; QString toPlainTextWithoutImg() const;
const QVector<VHeader> &getHeaders() const;
signals: signals:
void headersChanged(const QVector<VHeader> &headers); 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(); void statusChanged();
private slots: private slots:
void generateEditOutline(); void generateEditOutline();
// When there is no header in current cursor, will signal an invalid header.
void updateCurHeader(); void updateCurHeader();
void handleEditStateChanged(KeyState p_state); void handleEditStateChanged(KeyState p_state);
void handleSelectionChanged(); void handleSelectionChanged();
void handleClipboardChanged(QClipboard::Mode p_mode); void handleClipboardChanged(QClipboard::Mode p_mode);

View File

@ -50,8 +50,8 @@ void VMdTab::setupUI()
this, &VMdTab::updateTocFromHeaders); this, &VMdTab::updateTocFromHeaders);
connect(dynamic_cast<VMdEdit *>(m_editor), &VMdEdit::statusChanged, connect(dynamic_cast<VMdEdit *>(m_editor), &VMdEdit::statusChanged,
this, &VMdTab::noticeStatusChanged); this, &VMdTab::noticeStatusChanged);
connect(m_editor, SIGNAL(curHeaderChanged(int, int)), connect(m_editor, SIGNAL(curHeaderChanged(VAnchor)),
this, SLOT(updateCurHeader(int, int))); this, SLOT(updateCurHeader(VAnchor)));
connect(m_editor, &VEdit::textChanged, connect(m_editor, &VEdit::textChanged,
this, &VMdTab::handleTextChanged); this, &VMdTab::handleTextChanged);
connect(m_editor, &VEdit::saveAndRead, connect(m_editor, &VEdit::saveAndRead,
@ -91,6 +91,7 @@ void VMdTab::showFileReadMode()
m_isEditMode = false; m_isEditMode = false;
int outlineIndex = m_curHeader.m_outlineIndex; int outlineIndex = m_curHeader.m_outlineIndex;
if (m_mdConType == MarkdownConverterType::Hoedown) { if (m_mdConType == MarkdownConverterType::Hoedown) {
viewWebByConverter(); viewWebByConverter();
} else { } else {
@ -100,6 +101,7 @@ void VMdTab::showFileReadMode()
m_stacks->setCurrentWidget(m_webViewer); m_stacks->setCurrentWidget(m_webViewer);
clearSearchedWordHighlight(); clearSearchedWordHighlight();
scrollWebViewToHeader(outlineIndex); scrollWebViewToHeader(outlineIndex);
noticeStatusChanged(); noticeStatusChanged();
@ -107,14 +109,20 @@ void VMdTab::showFileReadMode()
void VMdTab::scrollWebViewToHeader(int p_outlineIndex) void VMdTab::scrollWebViewToHeader(int p_outlineIndex)
{ {
V_ASSERT(p_outlineIndex >= 0); QString anchor;
if (p_outlineIndex < m_toc.headers.size()) { m_curHeader = VAnchor(m_file, anchor, -1, p_outlineIndex);
QString anchor = m_toc.headers[p_outlineIndex].anchor;
if (!anchor.isEmpty()) { if (p_outlineIndex < m_toc.headers.size() && p_outlineIndex >= 0) {
m_document->scrollToAnchor(anchor.mid(1)); 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() void VMdTab::viewWebByConverter()
@ -136,14 +144,28 @@ void VMdTab::showFileEditMode()
m_isEditMode = true; m_isEditMode = true;
VMdEdit *mdEdit = dynamic_cast<VMdEdit *>(m_editor);
V_ASSERT(mdEdit);
// beginEdit() may change m_curHeader. // beginEdit() may change m_curHeader.
int outlineIndex = m_curHeader.m_outlineIndex; 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(); VAnchor anchor(m_file, "", lineNumber, outlineIndex);
m_stacks->setCurrentWidget(m_editor);
dynamic_cast<VMdEdit *>(m_editor)->scrollToHeader(outlineIndex); mdEdit->beginEdit();
m_editor->setFocus(); m_stacks->setCurrentWidget(mdEdit);
mdEdit->scrollToHeader(anchor);
mdEdit->setFocus();
noticeStatusChanged(); noticeStatusChanged();
} }
@ -368,7 +390,7 @@ static void parseTocLi(QXmlStreamReader &p_xml, QVector<VHeader> &p_headers, int
return; return;
} }
VHeader header(p_level, name, anchor, -1); VHeader header(p_level, name, anchor, -1, p_headers.size());
p_headers.append(header); p_headers.append(header);
} else { } else {
// Error // Error
@ -376,7 +398,7 @@ static void parseTocLi(QXmlStreamReader &p_xml, QVector<VHeader> &p_headers, int
} }
} else if (p_xml.name() == "ul") { } else if (p_xml.name() == "ul") {
// Such as header 3 under header 1 directly // 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); p_headers.append(header);
parseTocUl(p_xml, p_headers, p_level + 1); parseTocUl(p_xml, p_headers, p_level + 1);
} else { } else {
@ -478,10 +500,9 @@ void VMdTab::scrollToAnchor(const VAnchor &p_anchor)
} }
m_curHeader = p_anchor; m_curHeader = p_anchor;
if (m_isEditMode) { if (m_isEditMode) {
if (p_anchor.lineNumber > -1) { dynamic_cast<VMdEdit *>(m_editor)->scrollToHeader(p_anchor);
m_editor->scrollToLine(p_anchor.lineNumber);
}
} else { } else {
if (!p_anchor.anchor.isEmpty()) { if (!p_anchor.anchor.isEmpty()) {
m_document->scrollToAnchor(p_anchor.anchor.mid(1)); 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; const QVector<VHeader> &headers = m_toc.headers;
for (int i = 0; i < headers.size(); ++i) { for (int i = 0; i < headers.size(); ++i) {
if (headers[i].anchor == m_curHeader.anchor) { 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; 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) { if (m_isEditMode) {
return; 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 = p_anchor;
m_curHeader.m_outlineIndex = p_outlineIndex;
if (p_lineNumber > -1) { emit curHeaderChanged(m_curHeader);
emit curHeaderChanged(m_curHeader);
}
} }
void VMdTab::insertImage() void VMdTab::insertImage()

View File

@ -76,7 +76,7 @@ private slots:
void updateCurHeader(const QString &p_anchor); void updateCurHeader(const QString &p_anchor);
// Editor requests to update current header. // 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. // Handle key press event in Web view.
void handleWebKeyPressed(int p_key, bool p_ctrl, bool p_shift); void handleWebKeyPressed(int p_key, bool p_ctrl, bool p_shift);

View File

@ -1,7 +1,6 @@
#include <QDebug> #include <QDebug>
#include <QVector> #include <QVector>
#include <QString> #include <QString>
#include <QJsonObject>
#include <QKeyEvent> #include <QKeyEvent>
#include <QLabel> #include <QLabel>
#include <QCoreApplication> #include <QCoreApplication>
@ -9,8 +8,10 @@
#include "vtoc.h" #include "vtoc.h"
#include "utils/vutils.h" #include "utils/vutils.h"
#include "vnote.h" #include "vnote.h"
#include "vconfigmanager.h"
extern VNote *g_vnote; extern VNote *g_vnote;
extern VConfigManager vconfig;
VOutline::VOutline(QWidget *parent) VOutline::VOutline(QWidget *parent)
: QTreeWidget(parent), VNavigationMode() : QTreeWidget(parent), VNavigationMode()
@ -23,23 +24,38 @@ VOutline::VOutline(QWidget *parent)
this, &VOutline::handleCurItemChanged); 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) void VOutline::updateOutline(const VToc &toc)
{ {
// Clear current header // Clear current header
curHeader = VAnchor(); curHeader = VAnchor();
checkOutline(toc);
outline = toc; outline = toc;
updateTreeFromOutline(outline);
updateTreeFromOutline();
expandTree(); expandTree();
} }
void VOutline::updateTreeFromOutline(const VToc &toc) void VOutline::updateTreeFromOutline()
{ {
clear(); clear();
if (!toc.valid) { if (!outline.valid) {
return; return;
} }
const QVector<VHeader> &headers = toc.headers;
const QVector<VHeader> &headers = outline.headers;
int idx = 0; int idx = 0;
updateTreeByLevel(headers, idx, NULL, NULL, 1); updateTreeByLevel(headers, idx, NULL, NULL, 1);
} }
@ -56,13 +72,8 @@ void VOutline::updateTreeByLevel(const QVector<VHeader> &headers, int &index,
} else { } else {
item = new QTreeWidgetItem(this); item = new QTreeWidgetItem(this);
} }
QJsonObject itemJson;
itemJson["anchor"] = header.anchor; fillItem(item, header);
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);
last = item; last = item;
++index; ++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() void VOutline::expandTree()
{ {
if (topLevelItemCount() == 0) { if (topLevelItemCount() == 0) {
@ -87,20 +109,22 @@ void VOutline::handleCurItemChanged(QTreeWidgetItem *p_curItem, QTreeWidgetItem
if (!p_curItem) { if (!p_curItem) {
return; return;
} }
QJsonObject itemJson = p_curItem->data(0, Qt::UserRole).toJsonObject();
QString anchor = itemJson["anchor"].toString(); const VHeader *header = getHeaderFromItem(p_curItem);
int lineNumber = itemJson["line_number"].toInt(); if (!header) {
int outlineIndex = itemJson["outline_index"].toInt(); return;
VAnchor tmp; }
tmp.m_file = outline.m_file;
tmp.anchor = anchor; VAnchor tmp(outline.m_file, header->anchor, header->lineNumber, header->index);
tmp.lineNumber = lineNumber;
tmp.m_outlineIndex = outlineIndex;
if (tmp == curHeader) { if (tmp == curHeader) {
return; return;
} }
curHeader = tmp; curHeader = tmp;
emit outlineItemActivated(curHeader);
if (!header->isEmpty()) {
emit outlineItemActivated(curHeader);
}
} }
void VOutline::updateCurHeader(const VAnchor &anchor) void VOutline::updateCurHeader(const VAnchor &anchor)
@ -108,17 +132,24 @@ void VOutline::updateCurHeader(const VAnchor &anchor)
if (anchor == curHeader) { if (anchor == curHeader) {
return; return;
} }
curHeader = anchor; curHeader = anchor;
if (outline.type == VHeaderType::Anchor) { if (outline.type == VHeaderType::Anchor) {
selectAnchor(anchor.anchor); selectAnchor(anchor.anchor);
} else { } else {
// Select by lineNumber // Select by lineNumber.
selectLineNumber(anchor.lineNumber); selectLineNumber(anchor.lineNumber);
} }
} }
void VOutline::selectAnchor(const QString &anchor) void VOutline::selectAnchor(const QString &anchor)
{ {
setCurrentItem(NULL);
if (anchor.isEmpty()) {
return;
}
int nrTop = topLevelItemCount(); int nrTop = topLevelItemCount();
for (int i = 0; i < nrTop; ++i) { for (int i = 0; i < nrTop; ++i) {
if (selectAnchorOne(topLevelItem(i), anchor)) { if (selectAnchorOne(topLevelItem(i), anchor)) {
@ -132,9 +163,13 @@ bool VOutline::selectAnchorOne(QTreeWidgetItem *item, const QString &anchor)
if (!item) { if (!item) {
return false; return false;
} }
QJsonObject itemJson = item->data(0, Qt::UserRole).toJsonObject();
QString itemAnchor = itemJson["anchor"].toString(); const VHeader *header = getHeaderFromItem(item);
if (itemAnchor == anchor) { if (!header) {
return false;
}
if (header->anchor == anchor) {
setCurrentItem(item); setCurrentItem(item);
return true; return true;
} }
@ -150,6 +185,12 @@ bool VOutline::selectAnchorOne(QTreeWidgetItem *item, const QString &anchor)
void VOutline::selectLineNumber(int lineNumber) void VOutline::selectLineNumber(int lineNumber)
{ {
setCurrentItem(NULL);
if (lineNumber == -1) {
return;
}
int nrTop = topLevelItemCount(); int nrTop = topLevelItemCount();
for (int i = 0; i < nrTop; ++i) { for (int i = 0; i < nrTop; ++i) {
if (selectLineNumberOne(topLevelItem(i), lineNumber)) { if (selectLineNumberOne(topLevelItem(i), lineNumber)) {
@ -163,9 +204,13 @@ bool VOutline::selectLineNumberOne(QTreeWidgetItem *item, int lineNumber)
if (!item) { if (!item) {
return false; return false;
} }
QJsonObject itemJson = item->data(0, Qt::UserRole).toJsonObject();
int itemLineNum = itemJson["line_number"].toInt(); const VHeader *header = getHeaderFromItem(item);
if (itemLineNum == lineNumber) { if (!header) {
return false;
}
if (header->lineNumber == lineNumber) {
// Select this item // Select this item
setCurrentItem(item); setCurrentItem(item);
return true; return true;
@ -329,3 +374,18 @@ QList<QTreeWidgetItem *> VOutline::getVisibleChildItems(const QTreeWidgetItem *p
} }
return items; 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); void handleCurItemChanged(QTreeWidgetItem *p_curItem, QTreeWidgetItem *p_preItem);
private: 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, void updateTreeByLevel(const QVector<VHeader> &headers, int &index, QTreeWidgetItem *parent,
QTreeWidgetItem *last, int level); QTreeWidgetItem *last, int level);
void expandTree(); void expandTree();
void selectAnchor(const QString &anchor); void selectAnchor(const QString &anchor);
bool selectAnchorOne(QTreeWidgetItem *item, const QString &anchor); bool selectAnchorOne(QTreeWidgetItem *item, const QString &anchor);
@ -47,6 +51,15 @@ private:
QList<QTreeWidgetItem *> getVisibleItems() const; QList<QTreeWidgetItem *> getVisibleItems() const;
QList<QTreeWidgetItem *> getVisibleChildItems(const QTreeWidgetItem *p_item) 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; VToc outline;
VAnchor curHeader; VAnchor curHeader;

View File

@ -14,30 +14,53 @@ enum VHeaderType
struct VHeader struct VHeader
{ {
VHeader() : level(1), lineNumber(-1) {} VHeader() : level(1), lineNumber(-1), index(-1) {}
VHeader(int level, const QString &name, const QString &anchor, int lineNumber) VHeader(int level, const QString &name, const QString &anchor, int lineNumber, int index)
: level(level), name(name), anchor(anchor), lineNumber(lineNumber) {} : level(level), name(name), anchor(anchor), lineNumber(lineNumber), index(index) {}
int level; int level;
QString name; QString name;
QString anchor; QString anchor;
int lineNumber; 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 struct VAnchor
{ {
VAnchor() : lineNumber(-1), m_outlineIndex(0) {} VAnchor() : m_file(NULL), lineNumber(-1), m_outlineIndex(-1) {}
VAnchor(const VFile *file, const QString &anchor, int lineNumber)
: m_file(file), anchor(anchor), lineNumber(lineNumber), m_outlineIndex(0) {} 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; const VFile *m_file;
// The string anchor. For Web view.
QString anchor; QString anchor;
// The line number anchor. For edit view.
int lineNumber; 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; int m_outlineIndex;
bool operator==(const VAnchor &p_anchor) const { bool operator==(const VAnchor &p_anchor) const {
return (p_anchor.m_file == m_file return (p_anchor.m_file == m_file
&& p_anchor.anchor == anchor && p_anchor.anchor == anchor
&& p_anchor.lineNumber == lineNumber); && p_anchor.lineNumber == lineNumber
&& p_anchor.m_outlineIndex == m_outlineIndex);
} }
}; };