You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
886 lines
33 KiB
886 lines
33 KiB
#include "noteeditorlogic.h"
|
|
#include "customDocument.h"
|
|
#include "customMarkdownHighlighter.h"
|
|
#include "dbmanager.h"
|
|
#include "taglistview.h"
|
|
#include "taglistmodel.h"
|
|
#include "tagpool.h"
|
|
#include "taglistdelegate.h"
|
|
#include <QScrollBar>
|
|
#include <QLabel>
|
|
#include <QLineEdit>
|
|
#include <QListWidget>
|
|
#include <QDebug>
|
|
#include <QCursor>
|
|
|
|
#define FIRST_LINE_MAX 80
|
|
// 构造函数,根据 Qt 版本不同,参数有所不同
|
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 2, 0)
|
|
|
|
NoteEditorLogic::NoteEditorLogic(CustomDocument *textEdit, QLabel *editorDateLabel,
|
|
QLineEdit *searchEdit, QWidget *kanbanWidget,
|
|
TagListView *tagListView, TagPool *tagPool, DBManager *dbManager,
|
|
QObject *parent)
|
|
#else
|
|
NoteEditorLogic::NoteEditorLogic(CustomDocument *textEdit, QLabel *editorDateLabel,
|
|
QLineEdit *searchEdit, TagListView *tagListView, TagPool *tagPool,
|
|
DBManager *dbManager, QObject *parent)
|
|
#endif
|
|
: QObject(parent),
|
|
m_textEdit{ textEdit },
|
|
m_highlighter{ new CustomMarkdownHighlighter{ m_textEdit->document() } },
|
|
m_editorDateLabel{ editorDateLabel },
|
|
m_searchEdit{ searchEdit },
|
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 2, 0)
|
|
m_kanbanWidget{ kanbanWidget },
|
|
#endif
|
|
m_tagListView{ tagListView },
|
|
m_dbManager{ dbManager },
|
|
m_isContentModified{ false },
|
|
m_spacerColor{ 191, 191, 191 },
|
|
m_currentAdaptableEditorPadding{ 0 },
|
|
m_currentMinimumEditorPadding{ 0 }
|
|
{// 连接文本编辑器的文本变化信号
|
|
connect(m_textEdit, &QTextEdit::textChanged, this, &NoteEditorLogic::onTextEditTextChanged);
|
|
// 连接创建或更新笔记请求信号
|
|
connect(this, &NoteEditorLogic::requestCreateUpdateNote, m_dbManager,
|
|
&DBManager::onCreateUpdateRequestedNoteContent, Qt::QueuedConnection);
|
|
// auto save timer 自动保存定时器
|
|
m_autoSaveTimer.setSingleShot(true);
|
|
m_autoSaveTimer.setInterval(50);
|
|
connect(&m_autoSaveTimer, &QTimer::timeout, this, [this]() { saveNoteToDB(); });
|
|
// 初始化标签列表模型和委托
|
|
m_tagListModel = new TagListModel{ this };
|
|
m_tagListModel->setTagPool(tagPool);
|
|
m_tagListView->setModel(m_tagListModel);
|
|
m_tagListDelegate = new TagListDelegate{ this };
|
|
m_tagListView->setItemDelegate(m_tagListDelegate);
|
|
// 连接标签池数据更新信号
|
|
connect(tagPool, &TagPool::dataUpdated, this, [this](int) { showTagListForCurrentNote(); });
|
|
// 连接滚动条位置变化信号
|
|
connect(m_textEdit->verticalScrollBar(), &QScrollBar::valueChanged, this, [this](int value) {
|
|
if (m_currentNotes.size() == 1 && m_currentNotes[0].id() != SpecialNodeID::InvalidNodeId) {
|
|
m_currentNotes[0].setScrollBarPosition(value);
|
|
emit updateNoteDataInList(m_currentNotes[0]);
|
|
m_isContentModified = true;
|
|
m_autoSaveTimer.start();
|
|
}
|
|
});
|
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 2, 0)
|
|
// 连接显示看板视图信号
|
|
connect(this, &NoteEditorLogic::showKanbanView, this, [this]() {
|
|
if (m_kanbanWidget != nullptr) {
|
|
emit setVisibilityOfFrameRightNonEditor(false);
|
|
bool shouldRecheck = checkForTasksInEditor();
|
|
if (shouldRecheck) {
|
|
checkForTasksInEditor();
|
|
}
|
|
m_kanbanWidget->show();
|
|
m_textEdit->hide();
|
|
m_textEdit->clearFocus();
|
|
emit kanbanShown();
|
|
}
|
|
});
|
|
// 连接隐藏看板视图信号
|
|
connect(this, &NoteEditorLogic::hideKanbanView, this, [this]() {
|
|
if (m_kanbanWidget != nullptr) {
|
|
emit setVisibilityOfFrameRightNonEditor(true);
|
|
m_kanbanWidget->hide();
|
|
m_textEdit->show();
|
|
emit clearKanbanModel();
|
|
emit textShown();
|
|
}
|
|
});
|
|
#endif
|
|
}
|
|
// 获取是否启用 Markdown
|
|
bool NoteEditorLogic::markdownEnabled() const
|
|
{
|
|
return m_highlighter->document() != nullptr;
|
|
}
|
|
// 设置是否启用 Markdown
|
|
void NoteEditorLogic::setMarkdownEnabled(bool enabled)
|
|
{
|
|
m_highlighter->setDocument(enabled ? m_textEdit->document() : nullptr);
|
|
}
|
|
// 在编辑器中显示笔记
|
|
void NoteEditorLogic::showNotesInEditor(const QVector<NodeData> ¬es)
|
|
{
|
|
auto currentId = currentEditingNoteId();
|
|
if (notes.size() == 1 && notes[0].id() != SpecialNodeID::InvalidNodeId) {
|
|
if (currentId != SpecialNodeID::InvalidNodeId && notes[0].id() != currentId) {
|
|
emit noteEditClosed(m_currentNotes[0], false);
|
|
}
|
|
|
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 2, 0)
|
|
emit resetKanbanSettings();
|
|
emit checkMultipleNotesSelected(
|
|
QVariant(false)); // TODO: if not PRO version, should be true
|
|
#endif
|
|
|
|
m_textEdit->blockSignals(true);
|
|
|
|
m_currentNotes = notes;
|
|
showTagListForCurrentNote();
|
|
// fixing bug #202
|
|
m_textEdit->setTextBackgroundColor(QColor(247, 247, 247, 0));
|
|
|
|
QString content = notes[0].content();
|
|
QDateTime dateTime = notes[0].lastModificationdateTime();
|
|
int scrollbarPos = notes[0].scrollBarPosition();
|
|
|
|
// set text and date
|
|
bool isTextChanged = content != m_textEdit->toPlainText();
|
|
if (isTextChanged) {
|
|
m_textEdit->setText(content);
|
|
}
|
|
QString noteDate = dateTime.toString(Qt::ISODate);
|
|
QString noteDateEditor = getNoteDateEditor(noteDate);
|
|
m_editorDateLabel->setText(noteDateEditor);
|
|
// set scrollbar position
|
|
m_textEdit->verticalScrollBar()->setValue(scrollbarPos);
|
|
m_textEdit->blockSignals(false);
|
|
m_textEdit->setReadOnly(false);
|
|
m_textEdit->setTextInteractionFlags(Qt::TextEditorInteraction);
|
|
m_textEdit->setFocusPolicy(Qt::StrongFocus);
|
|
highlightSearch();
|
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 2, 0)
|
|
if (m_kanbanWidget != nullptr && m_kanbanWidget->isVisible()) {
|
|
emit clearKanbanModel();
|
|
bool shouldRecheck = checkForTasksInEditor();
|
|
if (shouldRecheck) {
|
|
checkForTasksInEditor();
|
|
}
|
|
m_textEdit->setVisible(false);
|
|
return;
|
|
} else {
|
|
m_textEdit->setVisible(true);
|
|
}
|
|
#else
|
|
m_textEdit->setVisible(true);
|
|
#endif
|
|
emit textShown();
|
|
} else if (notes.size() > 1) {
|
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 2, 0)
|
|
emit checkMultipleNotesSelected(QVariant(true));
|
|
#endif
|
|
m_currentNotes = notes;
|
|
m_tagListView->setVisible(false);
|
|
m_textEdit->blockSignals(true);
|
|
auto verticalScrollBarValueToRestore = m_textEdit->verticalScrollBar()->value();
|
|
m_textEdit->clear();
|
|
auto padding = m_currentAdaptableEditorPadding > m_currentMinimumEditorPadding
|
|
? m_currentAdaptableEditorPadding
|
|
: m_currentMinimumEditorPadding;
|
|
QPixmap sep(QSize{ m_textEdit->width() - padding * 2 - 12, 4 });
|
|
sep.fill(Qt::transparent);
|
|
QPainter painter(&sep);
|
|
painter.setPen(m_spacerColor);
|
|
painter.drawRect(0, 1, sep.width(), 1);
|
|
m_textEdit->document()->addResource(QTextDocument::ImageResource, QUrl("mydata://sep.png"),
|
|
sep);
|
|
for (int i = 0; i < notes.size(); ++i) {
|
|
auto cursor = m_textEdit->textCursor();
|
|
cursor.movePosition(QTextCursor::End);
|
|
if (!notes[i].content().endsWith("\n")) {
|
|
if (i != 0) {
|
|
cursor.insertText("\n" + notes[i].content() + "\n");
|
|
} else {
|
|
cursor.insertText(notes[i].content() + "\n");
|
|
}
|
|
} else {
|
|
cursor.insertText(notes[i].content());
|
|
}
|
|
if (i != notes.size() - 1) {
|
|
cursor.movePosition(QTextCursor::End);
|
|
cursor.insertText("\n");
|
|
cursor.insertImage("mydata://sep.png");
|
|
cursor.insertText("\n");
|
|
}
|
|
}
|
|
m_textEdit->verticalScrollBar()->setValue(verticalScrollBarValueToRestore);
|
|
m_textEdit->blockSignals(false);
|
|
m_textEdit->setReadOnly(true);
|
|
m_textEdit->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
|
m_textEdit->setFocusPolicy(Qt::NoFocus);
|
|
highlightSearch();
|
|
}
|
|
}
|
|
// 文本编辑器内容变化时的处理
|
|
void NoteEditorLogic::onTextEditTextChanged()
|
|
{
|
|
if (currentEditingNoteId() != SpecialNodeID::InvalidNodeId) {
|
|
m_textEdit->blockSignals(true);
|
|
QString content = m_currentNotes[0].content();
|
|
if (m_textEdit->toPlainText() != content) {
|
|
// move note to the top of the list
|
|
emit moveNoteToListViewTop(m_currentNotes[0]);
|
|
|
|
// Get the new data
|
|
QString firstline = getFirstLine(m_textEdit->toPlainText());
|
|
QDateTime dateTime = QDateTime::currentDateTime();
|
|
QString noteDate = dateTime.toString(Qt::ISODate);
|
|
m_editorDateLabel->setText(NoteEditorLogic::getNoteDateEditor(noteDate));
|
|
// update note data
|
|
m_currentNotes[0].setContent(m_textEdit->toPlainText());
|
|
m_currentNotes[0].setFullTitle(firstline);
|
|
m_currentNotes[0].setLastModificationDateTime(dateTime);
|
|
m_currentNotes[0].setIsTempNote(false);
|
|
m_currentNotes[0].setScrollBarPosition(m_textEdit->verticalScrollBar()->value());
|
|
// update note data in list view
|
|
emit updateNoteDataInList(m_currentNotes[0]);
|
|
m_isContentModified = true;
|
|
m_autoSaveTimer.start();
|
|
emit setVisibilityOfFrameRightWidgets(false);
|
|
}
|
|
m_textEdit->blockSignals(false);
|
|
} else {
|
|
qDebug() << "NoteEditorLogic::onTextEditTextChanged() : m_currentNote is not valid";
|
|
}
|
|
}
|
|
|
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 2, 0)
|
|
// 重新排列文本编辑器中的任务
|
|
void NoteEditorLogic::rearrangeTasksInTextEditor(int startLinePosition, int endLinePosition,
|
|
int newLinePosition)
|
|
{
|
|
QTextDocument *document = m_textEdit->document();
|
|
QTextCursor cursor(document);
|
|
cursor.setPosition(document->findBlockByNumber(startLinePosition).position());
|
|
cursor.setPosition(document->findBlockByNumber(endLinePosition).position(),
|
|
QTextCursor::KeepAnchor);
|
|
cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
|
|
if (document->findBlockByNumber(endLinePosition + 1).isValid()) {
|
|
cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
|
|
}
|
|
QString selectedText = cursor.selectedText();
|
|
cursor.removeSelectedText();
|
|
|
|
if (newLinePosition <= startLinePosition) {
|
|
cursor.setPosition(document->findBlockByLineNumber(newLinePosition).position());
|
|
} else {
|
|
int newPositionBecauseOfRemoval =
|
|
newLinePosition - (endLinePosition - startLinePosition + 1);
|
|
if (newPositionBecauseOfRemoval == document->lineCount()) {
|
|
cursor.setPosition(
|
|
document->findBlockByLineNumber(newPositionBecauseOfRemoval - 1).position());
|
|
cursor.movePosition(QTextCursor::EndOfBlock);
|
|
selectedText = "\n" + selectedText;
|
|
} else {
|
|
cursor.setPosition(
|
|
document->findBlockByLineNumber(newPositionBecauseOfRemoval).position());
|
|
}
|
|
}
|
|
cursor.insertText(selectedText);
|
|
|
|
checkForTasksInEditor();
|
|
}
|
|
// 重新排列文本编辑器中的列
|
|
void NoteEditorLogic::rearrangeColumnsInTextEditor(int startLinePosition, int endLinePosition,
|
|
int newLinePosition)
|
|
{
|
|
QTextDocument *document = m_textEdit->document();
|
|
QTextCursor cursor(document);
|
|
cursor.setPosition(document->findBlockByNumber(startLinePosition).position());
|
|
cursor.setPosition(document->findBlockByNumber(endLinePosition).position(),
|
|
QTextCursor::KeepAnchor);
|
|
cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
|
|
if (document->findBlockByNumber(endLinePosition + 1).isValid()) {
|
|
cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
|
|
}
|
|
QString selectedText = cursor.selectedText();
|
|
cursor.removeSelectedText();
|
|
cursor.setPosition(document->findBlockByNumber(startLinePosition).position());
|
|
if (document->findBlockByNumber(startLinePosition + 1).isValid()) {
|
|
cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
|
|
cursor.removeSelectedText();
|
|
}
|
|
|
|
if (startLinePosition < newLinePosition) {
|
|
// Goes down
|
|
int newPositionBecauseOfRemoval =
|
|
newLinePosition - (endLinePosition - startLinePosition + 1);
|
|
cursor.setPosition(document->findBlockByLineNumber(newPositionBecauseOfRemoval).position());
|
|
cursor.movePosition(QTextCursor::EndOfBlock);
|
|
cursor.insertText("\n" + selectedText);
|
|
} else {
|
|
// Goes up
|
|
cursor.setPosition(document->findBlockByLineNumber(newLinePosition).position());
|
|
cursor.insertText(selectedText + "\n");
|
|
}
|
|
|
|
checkForTasksInEditor();
|
|
}
|
|
// 获取某行的任务数据
|
|
QMap<QString, int> NoteEditorLogic::getTaskDataInLine(const QString &line)
|
|
{
|
|
QStringList taskExpressions = { "- [ ]", "- [x]", "* [ ]", "* [x]", "- [X]", "* [X]" };
|
|
QMap<QString, int> taskMatchLineData;
|
|
taskMatchLineData["taskMatchIndex"] = -1;
|
|
|
|
int taskMatchIndex = -1;
|
|
for (int j = 0; j < taskExpressions.size(); j++) {
|
|
taskMatchIndex = line.indexOf(taskExpressions[j]);
|
|
if (taskMatchIndex != -1) {
|
|
taskMatchLineData["taskMatchIndex"] = taskMatchIndex;
|
|
taskMatchLineData["taskExpressionSize"] = taskExpressions[j].size();
|
|
taskMatchLineData["taskChecked"] = taskExpressions[j][3] == 'x' ? 1 : 0;
|
|
return taskMatchLineData;
|
|
}
|
|
}
|
|
|
|
return taskMatchLineData;
|
|
}
|
|
// 检查某行的任务
|
|
void NoteEditorLogic::checkTaskInLine(int lineNumber)
|
|
{
|
|
QTextDocument *document = m_textEdit->document();
|
|
QTextBlock block = document->findBlockByLineNumber(lineNumber);
|
|
|
|
if (block.isValid()) {
|
|
int indexOfTaskInLine = getTaskDataInLine(block.text())["taskMatchIndex"];
|
|
if (indexOfTaskInLine == -1)
|
|
return;
|
|
QTextCursor cursor(block);
|
|
cursor.setPosition(block.position() + indexOfTaskInLine + 3, QTextCursor::MoveAnchor);
|
|
|
|
// Remove the old character and insert the new one
|
|
cursor.deleteChar();
|
|
cursor.insertText("x");
|
|
}
|
|
}
|
|
// 取消检查某行的任务
|
|
void NoteEditorLogic::uncheckTaskInLine(int lineNumber)
|
|
{
|
|
QTextDocument *document = m_textEdit->document();
|
|
QTextBlock block = document->findBlockByLineNumber(lineNumber);
|
|
|
|
if (block.isValid()) {
|
|
int indexOfTaskInLine = getTaskDataInLine(block.text())["taskMatchIndex"];
|
|
if (indexOfTaskInLine == -1)
|
|
return;
|
|
QTextCursor cursor(block);
|
|
cursor.setPosition(block.position() + indexOfTaskInLine + 3, QTextCursor::MoveAnchor);
|
|
|
|
// Remove the old character and insert the new one
|
|
cursor.deleteChar();
|
|
cursor.insertText(" ");
|
|
}
|
|
}
|
|
// 替换某行之间的文本
|
|
void NoteEditorLogic::replaceTextBetweenLines(int startLinePosition, int endLinePosition,
|
|
QString &newText)
|
|
{
|
|
QTextDocument *document = m_textEdit->document();
|
|
QTextBlock startBlock = document->findBlockByLineNumber(startLinePosition);
|
|
QTextBlock endBlock = document->findBlockByLineNumber(endLinePosition);
|
|
QTextCursor cursor(startBlock);
|
|
cursor.setPosition(endBlock.position() + endBlock.length() - 1, QTextCursor::KeepAnchor);
|
|
cursor.removeSelectedText();
|
|
cursor.insertText(newText);
|
|
}
|
|
// 更新任务文本
|
|
void NoteEditorLogic::updateTaskText(int startLinePosition, int endLinePosition,
|
|
const QString &newText)
|
|
{
|
|
QTextDocument *document = m_textEdit->document();
|
|
QTextBlock block = document->findBlockByLineNumber(startLinePosition);
|
|
if (block.isValid()) {
|
|
QMap<QString, int> taskData = getTaskDataInLine(block.text());
|
|
int indexOfTaskInLine = taskData["taskMatchIndex"];
|
|
if (indexOfTaskInLine == -1)
|
|
return;
|
|
QString taskExpressionText =
|
|
block.text().mid(0, taskData["taskMatchIndex"] + taskData["taskExpressionSize"]);
|
|
|
|
QString newTextModified = newText;
|
|
newTextModified.replace("\n\n", "\n");
|
|
newTextModified.replace("~~", "");
|
|
QStringList taskExpressions = { "- [ ]", "- [x]", "* [ ]", "* [x]", "- [X]", "* [X]" };
|
|
for (const auto &taskExpression : taskExpressions) {
|
|
newTextModified.replace(taskExpression, "");
|
|
}
|
|
|
|
// We must allow hashtags solely for the first line, otherwise it will mess up
|
|
// the parser - interprate the task's description as columns
|
|
if (newTextModified.count('\n') > 1) {
|
|
QStringList newTextModifiedSplitted = newTextModified.split('\n');
|
|
|
|
if (newTextModifiedSplitted.size() > 1) {
|
|
for (int i = 1; i < newTextModifiedSplitted.size(); i++) {
|
|
// Skipping the first line
|
|
newTextModifiedSplitted[i].replace("# ", "");
|
|
newTextModifiedSplitted[i].replace("#", "");
|
|
}
|
|
|
|
newTextModified = newTextModifiedSplitted.join('\n');
|
|
}
|
|
}
|
|
|
|
QString newTaskText = taskExpressionText + " " + newTextModified;
|
|
if (newTaskText.size() > 0 && newTaskText[newTaskText.size() - 1] == '\n') {
|
|
newTaskText.remove(newTaskText.size() - 1, 1);
|
|
}
|
|
replaceTextBetweenLines(startLinePosition, endLinePosition, newTaskText);
|
|
checkForTasksInEditor();
|
|
}
|
|
}
|
|
// 添加新任务
|
|
void NoteEditorLogic::addNewTask(int startLinePosition, const QString newTaskText)
|
|
{
|
|
QString newText = "\n- [ ] " + newTaskText;
|
|
QTextDocument *document = m_textEdit->document();
|
|
QTextBlock startBlock = document->findBlockByLineNumber(startLinePosition);
|
|
|
|
if (!startBlock.isValid())
|
|
return;
|
|
|
|
QTextCursor cursor(startBlock);
|
|
cursor.movePosition(QTextCursor::EndOfBlock);
|
|
cursor.insertText(newText);
|
|
|
|
checkForTasksInEditor();
|
|
}
|
|
// 移除某行之间的文本
|
|
void NoteEditorLogic::removeTextBetweenLines(int startLinePosition, int endLinePosition)
|
|
{
|
|
if (startLinePosition < 0 || endLinePosition < startLinePosition) {
|
|
return;
|
|
}
|
|
|
|
QTextDocument *document = m_textEdit->document();
|
|
QTextCursor cursor(document);
|
|
cursor.setPosition(document->findBlockByNumber(startLinePosition).position());
|
|
cursor.setPosition(document->findBlockByNumber(endLinePosition).position(),
|
|
QTextCursor::KeepAnchor);
|
|
cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
|
|
if (document->findBlockByNumber(endLinePosition + 1).isValid()) {
|
|
cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
|
|
}
|
|
cursor.removeSelectedText();
|
|
}
|
|
// 移除任务
|
|
void NoteEditorLogic::removeTask(int startLinePosition, int endLinePosition)
|
|
{
|
|
removeTextBetweenLines(startLinePosition, endLinePosition);
|
|
checkForTasksInEditor();
|
|
}
|
|
// 添加新列
|
|
void NoteEditorLogic::addNewColumn(int startLinePosition, const QString &columnTitle)
|
|
{
|
|
if (startLinePosition < 0)
|
|
return;
|
|
|
|
QTextDocument *document = m_textEdit->document();
|
|
QTextBlock block = document->findBlockByNumber(startLinePosition);
|
|
|
|
if (block.isValid()) {
|
|
QTextCursor cursor(block);
|
|
if (startLinePosition == 0) {
|
|
cursor.movePosition(QTextCursor::StartOfBlock);
|
|
} else {
|
|
cursor.movePosition(QTextCursor::EndOfBlock);
|
|
}
|
|
cursor.insertText(columnTitle);
|
|
m_textEdit->setTextCursor(cursor);
|
|
} else {
|
|
m_textEdit->append(columnTitle);
|
|
}
|
|
|
|
checkForTasksInEditor();
|
|
}
|
|
// 移除列
|
|
void NoteEditorLogic::removeColumn(int startLinePosition, int endLinePosition)
|
|
{
|
|
removeTextBetweenLines(startLinePosition, endLinePosition);
|
|
|
|
if (startLinePosition < 0 || endLinePosition < startLinePosition)
|
|
return;
|
|
QTextDocument *document = m_textEdit->document();
|
|
QTextCursor cursor(document);
|
|
cursor.setPosition(document->findBlockByNumber(startLinePosition).position());
|
|
if (cursor.block().isValid() && cursor.block().text().isEmpty()) {
|
|
cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
|
|
cursor.removeSelectedText();
|
|
}
|
|
|
|
checkForTasksInEditor();
|
|
}
|
|
// 更新列标题
|
|
void NoteEditorLogic::updateColumnTitle(int lineNumber, const QString &newText)
|
|
{
|
|
QTextDocument *document = m_textEdit->document();
|
|
QTextBlock block = document->findBlockByLineNumber(lineNumber);
|
|
|
|
if (block.isValid()) {
|
|
// Header by hashtag
|
|
int lastIndexOfHashTag = block.text().lastIndexOf("#");
|
|
if (lastIndexOfHashTag != -1) {
|
|
QTextCursor cursor(block);
|
|
cursor.setPosition(block.position() + lastIndexOfHashTag + 1, QTextCursor::MoveAnchor);
|
|
cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
|
|
cursor.removeSelectedText();
|
|
cursor.setPosition(block.position() + lastIndexOfHashTag + 1);
|
|
cursor.insertText(" " + newText);
|
|
|
|
} else {
|
|
int lastIndexofColon = block.text().lastIndexOf("::");
|
|
if (lastIndexofColon != -1) {
|
|
// Header by double colons
|
|
QTextCursor cursor(block);
|
|
cursor.setPosition(block.position(), QTextCursor::MoveAnchor);
|
|
cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
|
|
cursor.removeSelectedText();
|
|
cursor.setPosition(block.position());
|
|
cursor.insertText(newText + "::");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// 向文本编辑器添加无标题列
|
|
void NoteEditorLogic::addUntitledColumnToTextEditor(int startLinePosition)
|
|
{
|
|
QString columnTitle = "# Untitled\n\n";
|
|
QTextDocument *document = m_textEdit->document();
|
|
QTextBlock block = document->findBlockByNumber(startLinePosition);
|
|
|
|
if (block.isValid()) {
|
|
QTextCursor cursor(block);
|
|
cursor.movePosition(QTextCursor::StartOfBlock);
|
|
cursor.insertText(columnTitle);
|
|
}
|
|
}
|
|
// 向 JSON 数据添加新列
|
|
void NoteEditorLogic::appendNewColumn(QJsonArray &data, QJsonObject ¤tColumn,
|
|
QString ¤tTitle, QJsonArray &tasks)
|
|
{
|
|
if (!tasks.isEmpty()) {
|
|
currentColumn["title"] = currentTitle;
|
|
currentColumn["tasks"] = tasks;
|
|
currentColumn["columnEndLine"] = tasks.last()["taskEndLine"];
|
|
data.append(currentColumn);
|
|
currentColumn = QJsonObject();
|
|
tasks = QJsonArray();
|
|
}
|
|
}
|
|
|
|
// Check if there are any tasks in the current note.
|
|
// If there are, sends the data to the kanban view.
|
|
// Structure:
|
|
// QJsonArray([
|
|
// {
|
|
// "title":"TODO",
|
|
// "columnStartLine": 1
|
|
// "columnEndLine": 4
|
|
// "tasks":[
|
|
// {"checked":false,"text":"todo 1", "taskStartine": 3, "taskEndLine": 3},
|
|
// {"checked":false,"text":"todo 2", "taskStartine": 4, "taskEndLine": 4}}]
|
|
// },
|
|
// ])
|
|
bool NoteEditorLogic::checkForTasksInEditor()// 检查编辑器中的任务
|
|
{
|
|
QStringList lines = m_textEdit->toPlainText().split("\n");
|
|
QJsonArray data;
|
|
QJsonObject currentColumn;
|
|
QJsonArray tasks;
|
|
QString currentTitle = "";
|
|
bool isPreviousLineATask = false;
|
|
|
|
for (int i = 0; i < lines.size(); i++) {
|
|
QString line = lines[i];
|
|
QString lineTrimmed = line.trimmed();
|
|
|
|
// Header title
|
|
if (lineTrimmed.startsWith("#")) {
|
|
if (!tasks.isEmpty() && currentTitle.isEmpty()) {
|
|
// If we have only tasks without a header we insert one and call this function again
|
|
addUntitledColumnToTextEditor(tasks.first()["taskStartLine"].toInt());
|
|
return true;
|
|
}
|
|
appendNewColumn(data, currentColumn, currentTitle, tasks);
|
|
currentColumn["columnStartLine"] = i;
|
|
int countOfHashTags = lineTrimmed.count('#');
|
|
currentTitle = lineTrimmed.mid(countOfHashTags);
|
|
isPreviousLineATask = false;
|
|
}
|
|
// Non-header text with double colons
|
|
else if (lineTrimmed.endsWith("::") && getTaskDataInLine(line)["taskMatchIndex"] == -1) {
|
|
if (!tasks.isEmpty() && currentTitle.isEmpty()) {
|
|
// If we have only tasks without a header we insert one and call this function again
|
|
addUntitledColumnToTextEditor(tasks.first()["taskStartLine"].toInt());
|
|
return true;
|
|
}
|
|
appendNewColumn(data, currentColumn, currentTitle, tasks);
|
|
currentColumn["columnStartLine"] = i;
|
|
QStringList parts = line.split("::");
|
|
currentTitle = parts[0].trimmed();
|
|
isPreviousLineATask = false;
|
|
}
|
|
// Todo item
|
|
else {
|
|
QMap<QString, int> taskDataInLine = getTaskDataInLine(line);
|
|
int indexOfTaskInLine = taskDataInLine["taskMatchIndex"];
|
|
|
|
if (indexOfTaskInLine != -1) {
|
|
QJsonObject taskObject;
|
|
QString taskText =
|
|
line.mid(indexOfTaskInLine + taskDataInLine["taskExpressionSize"])
|
|
.trimmed();
|
|
taskObject["text"] = taskText;
|
|
taskObject["checked"] = taskDataInLine["taskChecked"] == 1;
|
|
taskObject["taskStartLine"] = i;
|
|
taskObject["taskEndLine"] = i;
|
|
tasks.append(taskObject);
|
|
isPreviousLineATask = true;
|
|
}
|
|
// If it's a continues description of the task push current line's text to the last task
|
|
else if (!line.isEmpty() && isPreviousLineATask) {
|
|
if (tasks.size() > 0) {
|
|
QJsonObject newTask = tasks[tasks.size() - 1].toObject();
|
|
QString newTaskText = newTask["text"].toString() + " \n"
|
|
+ lineTrimmed; // For markdown rendering a line break needs two white
|
|
// spaces
|
|
newTask["text"] = newTaskText;
|
|
newTask["taskEndLine"] = i;
|
|
tasks[tasks.size() - 1] = newTask;
|
|
}
|
|
} else {
|
|
isPreviousLineATask = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!tasks.isEmpty() && currentTitle.isEmpty()) {
|
|
// If we have only tasks without a header we insert one and call this function again
|
|
addUntitledColumnToTextEditor(tasks.first()["taskStartLine"].toInt());
|
|
return true;
|
|
}
|
|
|
|
appendNewColumn(data, currentColumn, currentTitle, tasks);
|
|
|
|
emit tasksFoundInEditor(QVariant(data));
|
|
|
|
return false;
|
|
}
|
|
#endif
|
|
// 将字符串转换为 QDateTime
|
|
QDateTime NoteEditorLogic::getQDateTime(const QString &date)
|
|
{
|
|
QDateTime dateTime = QDateTime::fromString(date, Qt::ISODate);
|
|
return dateTime;
|
|
}
|
|
// 显示当前笔记的标签列表
|
|
void NoteEditorLogic::showTagListForCurrentNote()
|
|
{
|
|
if (currentEditingNoteId() != SpecialNodeID::InvalidNodeId) {
|
|
auto tagIds = m_currentNotes[0].tagIds();
|
|
if (tagIds.count() > 0) {
|
|
m_tagListView->setVisible(true);
|
|
m_tagListModel->setModelData(tagIds);
|
|
return;
|
|
}
|
|
m_tagListModel->setModelData(tagIds);
|
|
}
|
|
m_tagListView->setVisible(false);
|
|
}
|
|
|
|
bool NoteEditorLogic::isInEditMode() const// 获取是否处于编辑模式
|
|
{
|
|
if (m_currentNotes.size() == 1) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
int NoteEditorLogic::currentMinimumEditorPadding() const// 获取当前最小编辑器填充
|
|
{
|
|
return m_currentMinimumEditorPadding;
|
|
}
|
|
// 设置当前最小编辑器填充
|
|
void NoteEditorLogic::setCurrentMinimumEditorPadding(int newCurrentMinimumEditorPadding)
|
|
{
|
|
m_currentMinimumEditorPadding = newCurrentMinimumEditorPadding;
|
|
}
|
|
// 获取当前可适应的编辑器填充
|
|
int NoteEditorLogic::currentAdaptableEditorPadding() const
|
|
{
|
|
return m_currentAdaptableEditorPadding;
|
|
}
|
|
// 设置当前可适应的编辑器填充
|
|
void NoteEditorLogic::setCurrentAdaptableEditorPadding(int newCurrentAdaptableEditorPadding)
|
|
{
|
|
m_currentAdaptableEditorPadding = newCurrentAdaptableEditorPadding;
|
|
}
|
|
// 获取当前编辑的笔记 ID
|
|
int NoteEditorLogic::currentEditingNoteId() const
|
|
{
|
|
if (isInEditMode()) {
|
|
return m_currentNotes[0].id();
|
|
}
|
|
return SpecialNodeID::InvalidNodeId;
|
|
}
|
|
// 将笔记保存到数据库
|
|
void NoteEditorLogic::saveNoteToDB()
|
|
{
|
|
if (currentEditingNoteId() != SpecialNodeID::InvalidNodeId && m_isContentModified
|
|
&& !m_currentNotes[0].isTempNote()) {
|
|
emit requestCreateUpdateNote(m_currentNotes[0]);
|
|
m_isContentModified = false;
|
|
}
|
|
}
|
|
// 关闭编辑器
|
|
void NoteEditorLogic::closeEditor()
|
|
{
|
|
if (currentEditingNoteId() != SpecialNodeID::InvalidNodeId) {
|
|
saveNoteToDB();
|
|
emit noteEditClosed(m_currentNotes[0], false);
|
|
}
|
|
m_currentNotes.clear();
|
|
|
|
m_textEdit->blockSignals(true);
|
|
m_textEdit->clear();
|
|
m_textEdit->clearFocus();
|
|
m_textEdit->blockSignals(false);
|
|
m_tagListModel->setModelData({});
|
|
}
|
|
// 笔记标签列表发生变化时的处理
|
|
void NoteEditorLogic::onNoteTagListChanged(int noteId, const QSet<int> &tagIds)
|
|
{
|
|
if (currentEditingNoteId() == noteId) {
|
|
m_currentNotes[0].setTagIds(tagIds);
|
|
showTagListForCurrentNote();
|
|
}
|
|
}
|
|
// 删除当前笔记
|
|
void NoteEditorLogic::deleteCurrentNote()
|
|
{
|
|
if (isTempNote()) {
|
|
auto noteNeedDeleted = m_currentNotes[0];
|
|
m_currentNotes.clear();
|
|
m_textEdit->blockSignals(true);
|
|
m_textEdit->clear();
|
|
m_textEdit->clearFocus();
|
|
m_textEdit->blockSignals(false);
|
|
emit noteEditClosed(noteNeedDeleted, true);
|
|
} else if (currentEditingNoteId() != SpecialNodeID::InvalidNodeId) {
|
|
auto noteNeedDeleted = m_currentNotes[0];
|
|
saveNoteToDB();
|
|
m_currentNotes.clear();
|
|
m_textEdit->blockSignals(true);
|
|
m_textEdit->clear();
|
|
m_textEdit->clearFocus();
|
|
m_textEdit->blockSignals(false);
|
|
emit noteEditClosed(noteNeedDeleted, false);
|
|
emit deleteNoteRequested(noteNeedDeleted);
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* \brief NoteEditorLogic::getFirstLine
|
|
* Get a string 'str' and return only the first line of it
|
|
* If the string contain no text, return "New Note"
|
|
* TODO: We might make it more efficient by not loading the entire string into the memory
|
|
* \param str
|
|
* \return
|
|
*/
|
|
QString NoteEditorLogic::getFirstLine(const QString &str)// 获取文本的首行
|
|
{
|
|
QString text = str.trimmed();
|
|
if (text.isEmpty()) {
|
|
return "New Note";
|
|
}
|
|
QTextStream ts(&text);
|
|
return ts.readLine(FIRST_LINE_MAX);
|
|
}
|
|
|
|
QString NoteEditorLogic::getSecondLine(const QString &str)// 获取文本的第二行
|
|
{
|
|
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
|
|
auto sl = str.split("\n", QString::SkipEmptyParts);
|
|
#else
|
|
auto sl = str.split("\n", Qt::SkipEmptyParts);
|
|
#endif
|
|
if (sl.size() < 2) {
|
|
return getFirstLine(str);
|
|
}
|
|
int i = 1;
|
|
QString text;
|
|
do {
|
|
if (i >= sl.size()) {
|
|
return getFirstLine(str);
|
|
}
|
|
text = sl[i].trimmed();
|
|
++i;
|
|
} while (text.isEmpty());
|
|
QTextStream ts(&text);
|
|
return ts.readLine(FIRST_LINE_MAX);
|
|
}
|
|
// 设置主题
|
|
void NoteEditorLogic::setTheme(Theme::Value theme, QColor textColor, qreal fontSize)
|
|
{
|
|
m_tagListDelegate->setTheme(theme);
|
|
m_highlighter->setTheme(theme, textColor, fontSize);
|
|
switch (theme) {
|
|
case Theme::Light: {
|
|
m_spacerColor = QColor(191, 191, 191);
|
|
break;
|
|
}
|
|
case Theme::Dark: {
|
|
m_spacerColor = QColor(212, 212, 212);
|
|
break;
|
|
}
|
|
case Theme::Sepia: {
|
|
m_spacerColor = QColor(191, 191, 191);
|
|
break;
|
|
}
|
|
}
|
|
if (currentEditingNoteId() != SpecialNodeID::InvalidNodeId) {
|
|
int verticalScrollBarValueToRestore = m_textEdit->verticalScrollBar()->value();
|
|
m_textEdit->setText(
|
|
m_textEdit->toPlainText()); // TODO: Update the text color without setting the text
|
|
m_textEdit->verticalScrollBar()->setValue(verticalScrollBarValueToRestore);
|
|
} else {
|
|
int verticalScrollBarValueToRestore = m_textEdit->verticalScrollBar()->value();
|
|
showNotesInEditor(m_currentNotes);
|
|
m_textEdit->verticalScrollBar()->setValue(verticalScrollBarValueToRestore);
|
|
}
|
|
}
|
|
// 获取笔记编辑日期
|
|
QString NoteEditorLogic::getNoteDateEditor(const QString &dateEdited)
|
|
{
|
|
QDateTime dateTimeEdited(getQDateTime(dateEdited));
|
|
QLocale usLocale(QLocale(QStringLiteral("en_US")));
|
|
|
|
return usLocale.toString(dateTimeEdited, QStringLiteral("MMMM d, yyyy, h:mm A"));
|
|
}
|
|
// 高亮搜索结果
|
|
void NoteEditorLogic::highlightSearch() const
|
|
{
|
|
QString searchString = m_searchEdit->text();
|
|
|
|
if (searchString.isEmpty())
|
|
return;
|
|
|
|
m_textEdit->moveCursor(QTextCursor::Start);
|
|
|
|
QList<QTextEdit::ExtraSelection> extraSelections;
|
|
QTextCharFormat highlightFormat;
|
|
highlightFormat.setBackground(Qt::yellow);
|
|
|
|
while (m_textEdit->find(searchString))
|
|
extraSelections.append({ m_textEdit->textCursor(), highlightFormat });
|
|
|
|
if (!extraSelections.isEmpty()) {
|
|
m_textEdit->setTextCursor(extraSelections.first().cursor);
|
|
m_textEdit->setExtraSelections(extraSelections);
|
|
}
|
|
}
|
|
// 获取是否是临时笔记
|
|
bool NoteEditorLogic::isTempNote() const
|
|
{
|
|
if (currentEditingNoteId() != SpecialNodeID::InvalidNodeId && m_currentNotes[0].isTempNote()) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|