vnote/src/vedit.cpp
2018-09-18 19:27:42 +08:00

1429 lines
40 KiB
C++

#include <QtWidgets>
#include <QVector>
#include <QDebug>
#include "vedit.h"
#include "vnote.h"
#include "vconfigmanager.h"
#include "vtableofcontent.h"
#include "utils/vutils.h"
#include "utils/veditutils.h"
#include "utils/vmetawordmanager.h"
#include "veditoperations.h"
#include "vedittab.h"
#include "dialog/vinsertlinkdialog.h"
#include "utils/viconutils.h"
extern VConfigManager *g_config;
extern VNote *g_vnote;
extern VMetaWordManager *g_mwMgr;
VEdit::VEdit(VFile *p_file, QWidget *p_parent)
: QTextEdit(p_parent), m_file(p_file),
m_editOps(NULL), m_enableInputMethod(true)
{
const int labelTimerInterval = 500;
const int extraSelectionHighlightTimer = 500;
const int labelSize = 64;
m_selectedWordColor = QColor(g_config->getEditorSelectedWordBg());
m_searchedWordColor = QColor(g_config->getEditorSearchedWordBg());
m_searchedWordCursorColor = QColor(g_config->getEditorSearchedWordCursorBg());
m_incrementalSearchedWordColor = QColor(g_config->getEditorIncrementalSearchedWordBg());
m_trailingSpaceColor = QColor(g_config->getEditorTrailingSpaceBg());
QPixmap wrapPixmap(":/resources/icons/search_wrap.svg");
m_wrapLabel = new QLabel(this);
m_wrapLabel->setPixmap(wrapPixmap.scaled(labelSize, labelSize));
m_wrapLabel->hide();
m_labelTimer = new QTimer(this);
m_labelTimer->setSingleShot(true);
m_labelTimer->setInterval(labelTimerInterval);
connect(m_labelTimer, &QTimer::timeout,
this, &VEdit::labelTimerTimeout);
m_highlightTimer = new QTimer(this);
m_highlightTimer->setSingleShot(true);
m_highlightTimer->setInterval(extraSelectionHighlightTimer);
connect(m_highlightTimer, &QTimer::timeout,
this, &VEdit::doHighlightExtraSelections);
m_extraSelections.resize((int)SelectionId::MaxSelection);
updateFontAndPalette();
m_config.init(QFontMetrics(font()), false);
updateConfig();
connect(this, &VEdit::cursorPositionChanged,
this, &VEdit::handleCursorPositionChanged);
connect(this, &VEdit::selectionChanged,
this, &VEdit::highlightSelectedWord);
m_lineNumberArea = new LineNumberArea(this);
connect(document(), &QTextDocument::blockCountChanged,
this, &VEdit::updateLineNumberAreaMargin);
connect(this, &QTextEdit::textChanged,
this, &VEdit::updateLineNumberArea);
connect(verticalScrollBar(), &QScrollBar::valueChanged,
this, &VEdit::updateLineNumberArea);
updateLineNumberAreaMargin();
connect(document(), &QTextDocument::contentsChange,
this, &VEdit::updateBlockLineDistanceHeight);
}
VEdit::~VEdit()
{
}
void VEdit::updateConfig()
{
m_config.update(QFontMetrics(font()));
if (m_config.m_tabStopWidth > 0) {
setTabStopWidth(m_config.m_tabStopWidth);
}
emit configUpdated();
}
void VEdit::beginEdit()
{
updateFontAndPalette();
updateConfig();
setReadOnlyAndHighlight(false);
setModified(false);
}
void VEdit::endEdit()
{
setReadOnlyAndHighlight(true);
}
void VEdit::saveFile()
{
Q_ASSERT(m_file->isModifiable());
if (!document()->isModified()) {
return;
}
m_file->setContent(toHtml());
setModified(false);
}
void VEdit::reloadFile()
{
setHtml(m_file->getContent());
setModified(false);
}
bool VEdit::scrollToBlock(int p_blockNumber)
{
QTextBlock block = document()->findBlockByNumber(p_blockNumber);
if (block.isValid()) {
VEditUtils::scrollBlockInPage(this, block.blockNumber(), 0);
moveCursor(QTextCursor::EndOfBlock);
return true;
}
return false;
}
bool VEdit::isModified() const
{
return document()->isModified();
}
void VEdit::setModified(bool p_modified)
{
document()->setModified(p_modified);
}
void VEdit::insertImage()
{
if (m_editOps) {
m_editOps->insertImage();
}
}
void VEdit::insertLink()
{
if (!m_editOps) {
return;
}
QString text;
QString linkText, linkUrl;
QTextCursor cursor = textCursor();
if (cursor.hasSelection()) {
text = VEditUtils::selectedText(cursor).trimmed();
// Only pure space is accepted.
QRegExp reg("[\\S ]*");
if (reg.exactMatch(text)) {
QUrl url = QUrl::fromUserInput(text,
m_file->fetchBasePath());
QRegExp urlReg("[\\.\\\\/]");
if (url.isValid()
&& text.contains(urlReg)) {
// Url.
linkUrl = text;
} else {
// Text.
linkText = text;
}
}
}
VInsertLinkDialog dialog(tr("Insert Link"),
"",
"",
linkText,
linkUrl,
false,
this);
if (dialog.exec() == QDialog::Accepted) {
linkText = dialog.getLinkText();
linkUrl = dialog.getLinkUrl();
Q_ASSERT(!linkText.isEmpty() && !linkUrl.isEmpty());
m_editOps->insertLink(linkText, linkUrl);
}
}
bool VEdit::peekText(const QString &p_text, uint p_options, bool p_forward)
{
if (p_text.isEmpty()) {
makeBlockVisible(document()->findBlock(textCursor().selectionStart()));
highlightIncrementalSearchedWord(QTextCursor());
return false;
}
bool wrapped = false;
QTextCursor retCursor;
bool found = findTextHelper(p_text, p_options, p_forward,
p_forward ? textCursor().position() + 1
: textCursor().position(),
wrapped, retCursor);
if (found) {
makeBlockVisible(document()->findBlock(retCursor.selectionStart()));
highlightIncrementalSearchedWord(retCursor);
}
return found;
}
// Use QTextEdit::find() instead of QTextDocument::find() because the later has
// bugs in searching backward.
bool VEdit::findTextHelper(const QString &p_text, uint p_options,
bool p_forward, int p_start,
bool &p_wrapped, QTextCursor &p_cursor)
{
p_wrapped = false;
bool found = false;
// Options
QTextDocument::FindFlags findFlags;
bool caseSensitive = false;
if (p_options & FindOption::CaseSensitive) {
findFlags |= QTextDocument::FindCaseSensitively;
caseSensitive = true;
}
if (p_options & FindOption::WholeWordOnly) {
findFlags |= QTextDocument::FindWholeWords;
}
if (!p_forward) {
findFlags |= QTextDocument::FindBackward;
}
// Use regular expression
bool useRegExp = false;
QRegExp exp;
if (p_options & FindOption::RegularExpression) {
useRegExp = true;
exp = QRegExp(p_text,
caseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive);
}
// Store current state of the cursor.
QTextCursor cursor = textCursor();
if (cursor.position() != p_start) {
if (p_start < 0) {
p_start = 0;
} else if (p_start > document()->characterCount()) {
p_start = document()->characterCount();
}
QTextCursor startCursor = cursor;
startCursor.setPosition(p_start);
setTextCursor(startCursor);
}
while (!found) {
if (useRegExp) {
found = find(exp, findFlags);
} else {
found = find(p_text, findFlags);
}
if (p_wrapped) {
break;
}
if (!found) {
// Wrap to the other end of the document to search again.
p_wrapped = true;
QTextCursor wrapCursor = textCursor();
if (p_forward) {
wrapCursor.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor);
} else {
wrapCursor.movePosition(QTextCursor::End, QTextCursor::MoveAnchor);
}
setTextCursor(wrapCursor);
}
}
if (found) {
p_cursor = textCursor();
}
// Restore the original cursor.
setTextCursor(cursor);
return found;
}
QList<QTextCursor> VEdit::findTextAll(const QString &p_text, uint p_options)
{
QList<QTextCursor> results;
if (p_text.isEmpty()) {
return results;
}
// Options
QTextDocument::FindFlags findFlags;
bool caseSensitive = false;
if (p_options & FindOption::CaseSensitive) {
findFlags |= QTextDocument::FindCaseSensitively;
caseSensitive = true;
}
if (p_options & FindOption::WholeWordOnly) {
findFlags |= QTextDocument::FindWholeWords;
}
// Use regular expression
bool useRegExp = false;
QRegExp exp;
if (p_options & FindOption::RegularExpression) {
useRegExp = true;
exp = QRegExp(p_text,
caseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive);
}
int startPos = 0;
QTextCursor cursor;
QTextDocument *doc = document();
while (true) {
if (useRegExp) {
cursor = doc->find(exp, startPos, findFlags);
} else {
cursor = doc->find(p_text, startPos, findFlags);
}
if (cursor.isNull()) {
break;
} else {
results.append(cursor);
startPos = cursor.selectionEnd();
}
}
return results;
}
bool VEdit::findText(const QString &p_text, uint p_options, bool p_forward,
QTextCursor *p_cursor, QTextCursor::MoveMode p_moveMode)
{
clearIncrementalSearchedWordHighlight();
if (p_text.isEmpty()) {
clearSearchedWordHighlight();
return false;
}
QTextCursor cursor = textCursor();
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();
}
bool 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);
} else {
cursor.setPosition(retCursor.selectionStart(), p_moveMode);
setTextCursor(cursor);
}
highlightSearchedWord(p_text, p_options);
highlightSearchedWordUnderCursor(retCursor);
matches = m_extraSelections[(int)SelectionId::SearchedKeyword].size();
} else {
clearSearchedWordHighlight();
}
if (matches == 0) {
statusMessage(tr("Found no match"));
} else {
statusMessage(tr("Found %1 %2").arg(matches)
.arg(matches > 1 ? tr("matches") : tr("match")));
}
return found;
}
void VEdit::replaceText(const QString &p_text, uint p_options,
const QString &p_replaceText, bool p_findNext)
{
QTextCursor cursor = textCursor();
bool wrapped = false;
QTextCursor retCursor;
bool found = findTextHelper(p_text, p_options, true,
cursor.position(), wrapped, retCursor);
if (found) {
if (retCursor.selectionStart() == cursor.position()) {
// Matched.
retCursor.beginEditBlock();
retCursor.insertText(p_replaceText);
retCursor.endEditBlock();
setTextCursor(retCursor);
}
if (p_findNext) {
findText(p_text, p_options, true);
}
}
}
void VEdit::replaceTextAll(const QString &p_text, uint p_options,
const QString &p_replaceText)
{
// Replace from the start to the end and restore the cursor.
QTextCursor cursor = textCursor();
int nrReplaces = 0;
QTextCursor tmpCursor = cursor;
tmpCursor.setPosition(0);
setTextCursor(tmpCursor);
int start = tmpCursor.position();
while (true) {
bool wrapped = false;
QTextCursor retCursor;
bool found = findTextHelper(p_text, p_options, true,
start, wrapped, retCursor);
if (!found) {
break;
} else {
if (wrapped) {
// Wrap back.
break;
}
nrReplaces++;
retCursor.beginEditBlock();
retCursor.insertText(p_replaceText);
retCursor.endEditBlock();
setTextCursor(retCursor);
start = retCursor.position();
}
}
// Restore cursor position.
cursor.clearSelection();
setTextCursor(cursor);
qDebug() << "replace all" << nrReplaces << "occurences";
emit statusMessage(tr("Replace %1 %2").arg(nrReplaces)
.arg(nrReplaces > 1 ? tr("occurences")
: tr("occurence")));
}
void VEdit::showWrapLabel()
{
int labelW = m_wrapLabel->width();
int labelH = m_wrapLabel->height();
int x = (width() - labelW) / 2;
int y = (height() - labelH) / 2;
if (x < 0) {
x = 0;
}
if (y < 0) {
y = 0;
}
m_wrapLabel->move(x, y);
m_wrapLabel->show();
m_labelTimer->stop();
m_labelTimer->start();
}
void VEdit::labelTimerTimeout()
{
m_wrapLabel->hide();
}
void VEdit::updateFontAndPalette()
{
setFont(g_config->getBaseEditFont());
setPalette(g_config->getBaseEditPalette());
}
void VEdit::highlightExtraSelections(bool p_now)
{
m_highlightTimer->stop();
if (p_now) {
doHighlightExtraSelections();
} else {
m_highlightTimer->start();
}
}
void VEdit::doHighlightExtraSelections()
{
int nrExtra = m_extraSelections.size();
Q_ASSERT(nrExtra == (int)SelectionId::MaxSelection);
QList<QTextEdit::ExtraSelection> extraSelects;
for (int i = 0; i < nrExtra; ++i) {
extraSelects.append(m_extraSelections[i]);
}
setExtraSelections(extraSelects);
}
void VEdit::highlightCurrentLine()
{
QList<QTextEdit::ExtraSelection> &selects = m_extraSelections[(int)SelectionId::CurrentLine];
if (g_config->getHighlightCursorLine() && !isReadOnly()) {
// Need to highlight current line.
selects.clear();
// A long block maybe splited into multiple visual lines.
QTextEdit::ExtraSelection select;
select.format.setBackground(m_config.m_cursorLineBg);
select.format.setProperty(QTextFormat::FullWidthSelection, true);
QTextCursor cursor = textCursor();
if (m_config.m_highlightWholeBlock) {
cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::MoveAnchor, 1);
QTextBlock block = cursor.block();
int blockEnd = block.position() + block.length();
int pos = -1;
while (cursor.position() < blockEnd && pos != cursor.position()) {
QTextEdit::ExtraSelection newSelect = select;
newSelect.cursor = cursor;
selects.append(newSelect);
pos = cursor.position();
cursor.movePosition(QTextCursor::Down, QTextCursor::MoveAnchor, 1);
}
} else {
cursor.clearSelection();
select.cursor = cursor;
selects.append(select);
}
} else {
// Need to clear current line highlight.
if (selects.isEmpty()) {
return;
}
selects.clear();
}
highlightExtraSelections(true);
}
void VEdit::highlightSelectedWord()
{
QList<QTextEdit::ExtraSelection> &selects = m_extraSelections[(int)SelectionId::SelectedWord];
if (!g_config->getHighlightSelectedWord()) {
if (!selects.isEmpty()) {
selects.clear();
highlightExtraSelections(true);
}
return;
}
QString text = textCursor().selectedText().trimmed();
if (text.isEmpty() || wordInSearchedSelection(text)) {
selects.clear();
highlightExtraSelections(true);
return;
}
QTextCharFormat format;
format.setBackground(m_selectedWordColor);
highlightTextAll(text, FindOption::CaseSensitive, SelectionId::SelectedWord,
format);
}
// Do not highlight trailing spaces with current cursor right behind.
static void trailingSpaceFilter(VEdit *p_editor, QList<QTextEdit::ExtraSelection> &p_result)
{
QTextCursor cursor = p_editor->textCursor();
if (!cursor.atBlockEnd()) {
return;
}
int cursorPos = cursor.position();
for (auto it = p_result.begin(); it != p_result.end(); ++it) {
if (it->cursor.selectionEnd() == cursorPos) {
p_result.erase(it);
// There will be only one.
return;
}
}
}
void VEdit::highlightTrailingSpace()
{
if (!g_config->getEnableTrailingSpaceHighlight()) {
QList<QTextEdit::ExtraSelection> &selects = m_extraSelections[(int)SelectionId::TrailingSpace];
if (!selects.isEmpty()) {
selects.clear();
highlightExtraSelections(true);
}
return;
}
QTextCharFormat format;
format.setBackground(m_trailingSpaceColor);
QString text("\\s+$");
highlightTextAll(text, FindOption::RegularExpression,
SelectionId::TrailingSpace, format,
trailingSpaceFilter);
}
bool VEdit::wordInSearchedSelection(const QString &p_text)
{
QString text = p_text.trimmed();
QList<QTextEdit::ExtraSelection> &selects = m_extraSelections[(int)SelectionId::SearchedKeyword];
for (int i = 0; i < selects.size(); ++i) {
QString searchedWord = selects[i].cursor.selectedText();
if (text == searchedWord.trimmed()) {
return true;
}
}
return false;
}
void VEdit::highlightTextAll(const QString &p_text, uint p_options,
SelectionId p_id, QTextCharFormat p_format,
void (*p_filter)(VEdit *, QList<QTextEdit::ExtraSelection> &))
{
QList<QTextEdit::ExtraSelection> &selects = m_extraSelections[(int)p_id];
if (!p_text.isEmpty()) {
selects.clear();
QList<QTextCursor> occurs = findTextAll(p_text, p_options);
for (int i = 0; i < occurs.size(); ++i) {
QTextEdit::ExtraSelection select;
select.format = p_format;
select.cursor = occurs[i];
selects.append(select);
}
} else {
if (selects.isEmpty()) {
return;
}
selects.clear();
}
if (p_filter) {
p_filter(this, selects);
}
highlightExtraSelections();
}
void VEdit::highlightSearchedWord(const QString &p_text, uint p_options)
{
QList<QTextEdit::ExtraSelection> &selects = m_extraSelections[(int)SelectionId::SearchedKeyword];
if (!g_config->getHighlightSearchedWord() || p_text.isEmpty()) {
if (!selects.isEmpty()) {
selects.clear();
highlightExtraSelections(true);
}
return;
}
QTextCharFormat format;
format.setBackground(m_searchedWordColor);
highlightTextAll(p_text, p_options, SelectionId::SearchedKeyword, format);
}
void VEdit::highlightSearchedWordUnderCursor(const QTextCursor &p_cursor)
{
QList<QTextEdit::ExtraSelection> &selects = m_extraSelections[(int)SelectionId::SearchedKeywordUnderCursor];
if (!p_cursor.hasSelection()) {
if (!selects.isEmpty()) {
selects.clear();
highlightExtraSelections(true);
}
return;
}
selects.clear();
QTextEdit::ExtraSelection select;
select.format.setBackground(m_searchedWordCursorColor);
select.cursor = p_cursor;
selects.append(select);
highlightExtraSelections(true);
}
void VEdit::highlightIncrementalSearchedWord(const QTextCursor &p_cursor)
{
QList<QTextEdit::ExtraSelection> &selects = m_extraSelections[(int)SelectionId::IncrementalSearchedKeyword];
if (!g_config->getHighlightSearchedWord() || !p_cursor.hasSelection()) {
if (!selects.isEmpty()) {
selects.clear();
highlightExtraSelections(true);
}
return;
}
selects.clear();
QTextEdit::ExtraSelection select;
select.format.setBackground(m_incrementalSearchedWordColor);
select.cursor = p_cursor;
selects.append(select);
highlightExtraSelections(true);
}
void VEdit::clearSearchedWordHighlight()
{
clearIncrementalSearchedWordHighlight(false);
clearSearchedWordUnderCursorHighlight(false);
QList<QTextEdit::ExtraSelection> &selects = m_extraSelections[(int)SelectionId::SearchedKeyword];
if (selects.isEmpty()) {
return;
}
selects.clear();
highlightExtraSelections(true);
}
void VEdit::clearSearchedWordUnderCursorHighlight(bool p_now)
{
QList<QTextEdit::ExtraSelection> &selects = m_extraSelections[(int)SelectionId::SearchedKeywordUnderCursor];
if (selects.isEmpty()) {
return;
}
selects.clear();
highlightExtraSelections(p_now);
}
void VEdit::clearIncrementalSearchedWordHighlight(bool p_now)
{
QList<QTextEdit::ExtraSelection> &selects = m_extraSelections[(int)SelectionId::IncrementalSearchedKeyword];
if (selects.isEmpty()) {
return;
}
selects.clear();
highlightExtraSelections(p_now);
}
void VEdit::contextMenuEvent(QContextMenuEvent *p_event)
{
QMenu *menu = createStandardContextMenu();
menu->setToolTipsVisible(true);
const QList<QAction *> actions = menu->actions();
if (!textCursor().hasSelection()) {
VEditTab *editTab = dynamic_cast<VEditTab *>(parent());
V_ASSERT(editTab);
if (editTab->isEditMode()) {
QAction *saveExitAct = new QAction(VIconUtils::menuIcon(":/resources/icons/save_exit.svg"),
tr("&Save Changes And Read"), this);
saveExitAct->setToolTip(tr("Save changes and exit edit mode"));
connect(saveExitAct, &QAction::triggered,
this, &VEdit::handleSaveExitAct);
QAction *discardExitAct = new QAction(VIconUtils::menuIcon(":/resources/icons/discard_exit.svg"),
tr("&Discard Changes And Read"), this);
discardExitAct->setToolTip(tr("Discard changes and exit edit mode"));
connect(discardExitAct, &QAction::triggered,
this, &VEdit::handleDiscardExitAct);
menu->insertAction(actions.isEmpty() ? NULL : actions[0], discardExitAct);
menu->insertAction(discardExitAct, saveExitAct);
if (!actions.isEmpty()) {
menu->insertSeparator(actions[0]);
}
} else if (m_file->isModifiable()) {
// HTML.
QAction *editAct= new QAction(VIconUtils::menuIcon(":/resources/icons/edit_note.svg"),
tr("&Edit"), this);
editAct->setToolTip(tr("Edit current note"));
connect(editAct, &QAction::triggered,
this, &VEdit::handleEditAct);
menu->insertAction(actions.isEmpty() ? NULL : actions[0], editAct);
// actions does not contain editAction.
if (!actions.isEmpty()) {
menu->insertSeparator(actions[0]);
}
}
}
alterContextMenu(menu, actions);
menu->exec(p_event->globalPos());
delete menu;
}
void VEdit::handleSaveExitAct()
{
emit saveAndRead();
}
void VEdit::handleDiscardExitAct()
{
emit discardAndRead();
}
void VEdit::handleEditAct()
{
emit editNote();
}
VFile *VEdit::getFile() const
{
return m_file;
}
void VEdit::handleCursorPositionChanged()
{
static QTextCursor lastCursor;
QTextCursor cursor = textCursor();
if (lastCursor.isNull() || cursor.blockNumber() != lastCursor.blockNumber()) {
highlightCurrentLine();
highlightTrailingSpace();
} else {
// Judge whether we have trailing space at current line.
QString text = cursor.block().text();
if (text.rbegin()->isSpace()) {
highlightTrailingSpace();
}
// Handle word-wrap in one block.
// Highlight current line if in different visual line.
if ((lastCursor.positionInBlock() - lastCursor.columnNumber()) !=
(cursor.positionInBlock() - cursor.columnNumber())) {
highlightCurrentLine();
}
}
lastCursor = cursor;
}
VEditConfig &VEdit::getConfig()
{
return m_config;
}
void VEdit::mousePressEvent(QMouseEvent *p_event)
{
if (p_event->button() == Qt::LeftButton
&& p_event->modifiers() == Qt::ControlModifier
&& !textCursor().hasSelection()) {
m_oriMouseX = p_event->x();
m_oriMouseY = p_event->y();
m_readyToScroll = true;
m_mouseMoveScrolled = false;
p_event->accept();
return;
}
m_readyToScroll = false;
m_mouseMoveScrolled = false;
QTextEdit::mousePressEvent(p_event);
emit selectionChangedByMouse(textCursor().hasSelection());
}
void VEdit::mouseReleaseEvent(QMouseEvent *p_event)
{
if (m_mouseMoveScrolled || m_readyToScroll) {
viewport()->setCursor(Qt::IBeamCursor);
m_readyToScroll = false;
m_mouseMoveScrolled = false;
p_event->accept();
return;
}
m_readyToScroll = false;
m_mouseMoveScrolled = false;
QTextEdit::mouseReleaseEvent(p_event);
}
void VEdit::mouseMoveEvent(QMouseEvent *p_event)
{
const int threshold = 5;
if (m_readyToScroll) {
int deltaX = p_event->x() - m_oriMouseX;
int deltaY = p_event->y() - m_oriMouseY;
if (qAbs(deltaX) >= threshold || qAbs(deltaY) >= threshold) {
m_oriMouseX = p_event->x();
m_oriMouseY = p_event->y();
if (!m_mouseMoveScrolled) {
m_mouseMoveScrolled = true;
viewport()->setCursor(Qt::SizeAllCursor);
}
QScrollBar *verBar = verticalScrollBar();
QScrollBar *horBar = horizontalScrollBar();
if (verBar->isVisible()) {
verBar->setValue(verBar->value() - deltaY);
}
if (horBar->isVisible()) {
horBar->setValue(horBar->value() - deltaX);
}
}
p_event->accept();
return;
}
QTextEdit::mouseMoveEvent(p_event);
emit selectionChangedByMouse(textCursor().hasSelection());
}
void VEdit::requestUpdateVimStatus()
{
if (m_editOps) {
m_editOps->requestUpdateVimStatus();
} else {
emit vimStatusUpdated(NULL);
}
}
bool VEdit::jumpTitle(bool p_forward, int p_relativeLevel, int p_repeat)
{
Q_UNUSED(p_forward);
Q_UNUSED(p_relativeLevel);
Q_UNUSED(p_repeat);
return false;
}
QVariant VEdit::inputMethodQuery(Qt::InputMethodQuery p_query) const
{
if (p_query == Qt::ImEnabled) {
return m_enableInputMethod;
}
return QTextEdit::inputMethodQuery(p_query);
}
void VEdit::setInputMethodEnabled(bool p_enabled)
{
if (m_enableInputMethod != p_enabled) {
m_enableInputMethod = p_enabled;
QInputMethod *im = QGuiApplication::inputMethod();
im->reset();
// Ask input method to query current state, which will call inputMethodQuery().
im->update(Qt::ImEnabled);
}
}
void VEdit::decorateText(TextDecoration p_decoration)
{
if (m_editOps) {
m_editOps->decorateText(p_decoration);
}
}
void VEdit::updateLineNumberAreaMargin()
{
int width = 0;
if (g_config->getEditorLineNumber()) {
width = m_lineNumberArea->calculateWidth();
}
setViewportMargins(width, 0, 0, 0);
}
void VEdit::updateLineNumberArea()
{
if (g_config->getEditorLineNumber()) {
if (!m_lineNumberArea->isVisible()) {
updateLineNumberAreaMargin();
m_lineNumberArea->show();
}
m_lineNumberArea->update();
} else if (m_lineNumberArea->isVisible()) {
updateLineNumberAreaMargin();
m_lineNumberArea->hide();
}
}
void VEdit::resizeEvent(QResizeEvent *p_event)
{
QTextEdit::resizeEvent(p_event);
if (g_config->getEditorLineNumber()) {
QRect rect = contentsRect();
m_lineNumberArea->setGeometry(QRect(rect.left(),
rect.top(),
m_lineNumberArea->calculateWidth(),
rect.height()));
}
}
void VEdit::lineNumberAreaPaintEvent(QPaintEvent *p_event)
{
int lineNumberType = g_config->getEditorLineNumber();
if (!lineNumberType) {
updateLineNumberAreaMargin();
m_lineNumberArea->hide();
return;
}
QPainter painter(m_lineNumberArea);
painter.fillRect(p_event->rect(), g_config->getEditorLineNumberBg());
QAbstractTextDocumentLayout *layout = document()->documentLayout();
QTextBlock block = firstVisibleBlock();
if (!block.isValid()) {
return;
}
int blockNumber = block.blockNumber();
int offsetY = contentOffsetY();
QRectF rect = layout->blockBoundingRect(block);
int top = offsetY + (int)rect.y();
int bottom = top + (int)rect.height();
int eventTop = p_event->rect().top();
int eventBtm = p_event->rect().bottom();
const int digitHeight = m_lineNumberArea->getDigitHeight();
const int curBlockNumber = textCursor().block().blockNumber();
const QString &fg = g_config->getEditorLineNumberFg();
const int lineDistanceHeight = m_config.m_lineDistanceHeight;
painter.setPen(fg);
// Display line number only in code block.
if (lineNumberType == 3) {
if (m_file->getDocType() != DocType::Markdown) {
return;
}
int number = 0;
while (block.isValid() && top <= eventBtm) {
int blockState = block.userState();
switch (blockState) {
case HighlightBlockState::CodeBlockStart:
Q_ASSERT(number == 0);
number = 1;
break;
case HighlightBlockState::CodeBlockEnd:
number = 0;
break;
case HighlightBlockState::CodeBlock:
if (number == 0) {
// Need to find current line number in code block.
QTextBlock startBlock = block.previous();
while (startBlock.isValid()) {
if (startBlock.userState() == HighlightBlockState::CodeBlockStart) {
number = block.blockNumber() - startBlock.blockNumber();
break;
}
startBlock = startBlock.previous();
}
}
break;
default:
break;
}
if (blockState == HighlightBlockState::CodeBlock) {
if (block.isVisible() && bottom >= eventTop) {
QString numberStr = QString::number(number);
painter.drawText(0,
top + 2,
m_lineNumberArea->width(),
digitHeight,
Qt::AlignRight,
numberStr);
}
++number;
}
block = block.next();
top = bottom;
bottom = top + (int)layout->blockBoundingRect(block).height() + lineDistanceHeight;
}
return;
}
// Handle lineNumberType 1 and 2.
Q_ASSERT(lineNumberType == 1 || lineNumberType == 2);
while (block.isValid() && top <= eventBtm) {
if (block.isVisible() && bottom >= eventTop) {
bool currentLine = false;
int number = blockNumber + 1;
if (lineNumberType == 2) {
number = blockNumber - curBlockNumber;
if (number == 0) {
currentLine = true;
number = blockNumber + 1;
} else if (number < 0) {
number = -number;
}
} else if (blockNumber == curBlockNumber) {
currentLine = true;
}
QString numberStr = QString::number(number);
if (currentLine) {
QFont font = painter.font();
font.setBold(true);
painter.setFont(font);
}
painter.drawText(0,
top + 2,
m_lineNumberArea->width(),
digitHeight,
Qt::AlignRight,
numberStr);
if (currentLine) {
QFont font = painter.font();
font.setBold(false);
painter.setFont(font);
}
}
block = block.next();
top = bottom;
bottom = top + (int)layout->blockBoundingRect(block).height() + lineDistanceHeight;
++blockNumber;
}
}
int VEdit::contentOffsetY()
{
int offsety = 0;
QScrollBar *sb = verticalScrollBar();
offsety = sb->value();
return -offsety;
}
QTextBlock VEdit::firstVisibleBlock()
{
QTextDocument *doc = document();
QAbstractTextDocumentLayout *layout = doc->documentLayout();
int offsetY = contentOffsetY();
// Binary search.
int idx = -1;
int start = 0, end = doc->blockCount() - 1;
while (start <= end) {
int mid = start + (end - start) / 2;
QTextBlock block = doc->findBlockByNumber(mid);
if (!block.isValid()) {
break;
}
int y = offsetY + (int)layout->blockBoundingRect(block).y();
if (y == 0) {
return block;
} else if (y < 0) {
start = mid + 1;
} else {
if (idx == -1 || mid < idx) {
idx = mid;
}
end = mid - 1;
}
}
if (idx != -1) {
return doc->findBlockByNumber(idx);
}
// Linear search.
qDebug() << "fall back to linear search for first visible block";
QTextBlock block = doc->begin();
while (block.isValid()) {
int y = offsetY + (int)layout->blockBoundingRect(block).y();
if (y >= 0) {
return block;
}
block = block.next();
}
return QTextBlock();
}
int LineNumberArea::calculateWidth() const
{
int bc = m_document->blockCount();
if (m_blockCount == bc) {
return m_width;
}
const_cast<LineNumberArea *>(this)->m_blockCount = bc;
int digits = 1;
int max = qMax(1, m_blockCount);
while (max >= 10) {
max /= 10;
++digits;
}
int width = m_digitWidth * digits;
const_cast<LineNumberArea *>(this)->m_width = width;
return m_width;
}
void VEdit::makeBlockVisible(const QTextBlock &p_block)
{
if (!p_block.isValid() || !p_block.isVisible()) {
return;
}
QScrollBar *vbar = verticalScrollBar();
if (!vbar || !vbar->isVisible()) {
// No vertical scrollbar. No need to scroll.
return;
}
QAbstractTextDocumentLayout *layout = document()->documentLayout();
int height = rect().height();
QScrollBar *hbar = horizontalScrollBar();
if (hbar && hbar->isVisible()) {
height -= hbar->height();
}
bool moved = false;
QRectF rect = layout->blockBoundingRect(p_block);
int y = contentOffsetY() + (int)rect.y();
int rectHeight = (int)rect.height();
// Handle the case rectHeight >= height.
if (rectHeight >= height) {
if (y <= 0) {
if (y + rectHeight < height) {
// Need to scroll up.
while (y + rectHeight < height && vbar->value() > vbar->minimum()) {
moved = true;
vbar->setValue(vbar->value() - vbar->singleStep());
rect = layout->blockBoundingRect(p_block);
rectHeight = (int)rect.height();
y = contentOffsetY() + (int)rect.y();
}
}
} else {
// Need to scroll down.
while (y > 0 && vbar->value() < vbar->maximum()) {
moved = true;
vbar->setValue(vbar->value() + vbar->singleStep());
rect = layout->blockBoundingRect(p_block);
rectHeight = (int)rect.height();
y = contentOffsetY() + (int)rect.y();
}
}
if (moved) {
qDebug() << "scroll to make huge block visible";
}
return;
}
while (y < 0 && vbar->value() > vbar->minimum()) {
moved = true;
vbar->setValue(vbar->value() - vbar->singleStep());
rect = layout->blockBoundingRect(p_block);
rectHeight = (int)rect.height();
y = contentOffsetY() + (int)rect.y();
}
if (moved) {
qDebug() << "scroll page down to make block visible";
return;
}
while (y + rectHeight > height && vbar->value() < vbar->maximum()) {
moved = true;
vbar->setValue(vbar->value() + vbar->singleStep());
rect = layout->blockBoundingRect(p_block);
rectHeight = (int)rect.height();
y = contentOffsetY() + (int)rect.y();
}
if (moved) {
qDebug() << "scroll page up to make block visible";
}
}
bool VEdit::isBlockVisible(const QTextBlock &p_block)
{
if (!p_block.isValid() || !p_block.isVisible()) {
return false;
}
QScrollBar *vbar = verticalScrollBar();
if (!vbar || !vbar->isVisible()) {
// No vertical scrollbar.
return true;
}
QAbstractTextDocumentLayout *layout = document()->documentLayout();
int height = rect().height();
QScrollBar *hbar = horizontalScrollBar();
if (hbar && hbar->isVisible()) {
height -= hbar->height();
}
QRectF rect = layout->blockBoundingRect(p_block);
int y = contentOffsetY() + (int)rect.y();
int rectHeight = (int)rect.height();
return (y >= 0 && y < height) || (y < 0 && y + rectHeight > 0);
}
void VEdit::alterContextMenu(QMenu *p_menu, const QList<QAction *> &p_actions)
{
Q_UNUSED(p_menu);
Q_UNUSED(p_actions);
}
void VEdit::updateBlockLineDistanceHeight(int p_pos,
int p_charsRemoved,
int p_charsAdded)
{
if ((p_charsRemoved == 0 && p_charsAdded == 0)
|| m_config.m_lineDistanceHeight <= 0) {
return;
}
QTextDocument *doc = document();
QTextBlock block = doc->findBlock(p_pos);
QTextBlock lastBlock = doc->findBlock(p_pos + p_charsRemoved + p_charsAdded);
QTextCursor cursor(block);
bool changed = false;
while (block.isValid()) {
cursor.setPosition(block.position());
QTextBlockFormat fmt = cursor.blockFormat();
if (fmt.lineHeightType() != QTextBlockFormat::LineDistanceHeight
|| fmt.lineHeight() != m_config.m_lineDistanceHeight) {
fmt.setLineHeight(m_config.m_lineDistanceHeight,
QTextBlockFormat::LineDistanceHeight);
if (!changed) {
changed = true;
cursor.joinPreviousEditBlock();
}
cursor.mergeBlockFormat(fmt);
qDebug() << "merge block format line distance" << block.blockNumber();
}
if (block == lastBlock) {
break;
}
block = block.next();
}
if (changed) {
cursor.endEditBlock();
}
}
void VEdit::evaluateMagicWords()
{
QString text;
QTextCursor cursor = textCursor();
if (!cursor.hasSelection()) {
// Get the WORD in current cursor.
int start, end;
VEditUtils::findCurrentWORD(cursor, start, end);
if (start == end) {
return;
} else {
cursor.setPosition(start);
cursor.setPosition(end, QTextCursor::KeepAnchor);
}
}
text = VEditUtils::selectedText(cursor);
Q_ASSERT(!text.isEmpty());
QString evaText = g_mwMgr->evaluate(text);
if (text != evaText) {
qDebug() << "evaluateMagicWords" << text << evaText;
cursor.insertText(evaText);
if (m_editOps) {
m_editOps->setVimMode(VimMode::Insert);
}
setTextCursor(cursor);
}
}
void VEdit::setReadOnlyAndHighlight(bool p_readonly)
{
setReadOnly(p_readonly);
highlightCurrentLine();
}