From 75dc7c6f28acb92e622b0cd534412474f5485fc5 Mon Sep 17 00:00:00 2001 From: Le Tan Date: Sat, 9 Jan 2021 19:52:08 -0800 Subject: [PATCH] Smart Table and Mark (#1649) * support smart table and mark --- libs/vtextedit | 2 +- src/core/editorconfig.h | 1 + src/core/global.h | 8 + src/core/markdowneditorconfig.cpp | 22 + src/core/markdowneditorconfig.h | 10 + src/data/core/core.qrc | 1 + src/data/core/icons/type_mark_editor.svg | 1 + src/data/core/translations/qtbase_zh_CN.qm | Bin 117813 -> 117813 bytes src/data/core/translations/qtbase_zh_CN.ts | 24 +- src/data/core/vnotex.json | 7 +- .../extra/themes/moonlight/text-editor.theme | 4 +- .../dialogs/settings/markdowneditorpage.cpp | 14 + .../dialogs/settings/markdowneditorpage.h | 2 + src/widgets/dialogs/tableinsertdialog.cpp | 91 ++ src/widgets/dialogs/tableinsertdialog.h | 35 + src/widgets/editors/markdowneditor.cpp | 52 ++ src/widgets/editors/markdowneditor.h | 10 + src/widgets/editors/markdowntable.cpp | 781 ++++++++++++++++++ src/widgets/editors/markdowntable.h | 165 ++++ src/widgets/editors/markdowntablehelper.cpp | 113 +++ src/widgets/editors/markdowntablehelper.h | 47 ++ src/widgets/markdownviewwindow.cpp | 9 + src/widgets/viewwindow.cpp | 4 +- src/widgets/viewwindow.h | 3 +- src/widgets/viewwindowtoolbarhelper.cpp | 6 + src/widgets/viewwindowtoolbarhelper.h | 3 + src/widgets/widgets.pri | 6 + 27 files changed, 1403 insertions(+), 18 deletions(-) create mode 100644 src/data/core/icons/type_mark_editor.svg create mode 100644 src/widgets/dialogs/tableinsertdialog.cpp create mode 100644 src/widgets/dialogs/tableinsertdialog.h create mode 100644 src/widgets/editors/markdowntable.cpp create mode 100644 src/widgets/editors/markdowntable.h create mode 100644 src/widgets/editors/markdowntablehelper.cpp create mode 100644 src/widgets/editors/markdowntablehelper.h diff --git a/libs/vtextedit b/libs/vtextedit index 69bd5765..3e45827a 160000 --- a/libs/vtextedit +++ b/libs/vtextedit @@ -1 +1 @@ -Subproject commit 69bd57656ccac8cf75502506be0c87d80d86e577 +Subproject commit 3e45827ae9a662bdc61da1090f4e51fdff24af85 diff --git a/src/core/editorconfig.h b/src/core/editorconfig.h index 62952ea4..7006d313 100644 --- a/src/core/editorconfig.h +++ b/src/core/editorconfig.h @@ -43,6 +43,7 @@ namespace vnotex TypeLink, TypeImage, TypeTable, + TypeMark, Outline, RichPaste, FindAndReplace, diff --git a/src/core/global.h b/src/core/global.h index 480aa353..f05e12a9 100644 --- a/src/core/global.h +++ b/src/core/global.h @@ -79,6 +79,14 @@ namespace vnotex ForceEnable = 1, ForceDisable = 2 }; + + enum class Alignment + { + None, + Left, + Center, + Right + }; } // ns vnotex Q_DECLARE_OPERATORS_FOR_FLAGS(vnotex::FindOptions); diff --git a/src/core/markdowneditorconfig.cpp b/src/core/markdowneditorconfig.cpp index 5bbd7665..cfa86fc1 100644 --- a/src/core/markdowneditorconfig.cpp +++ b/src/core/markdowneditorconfig.cpp @@ -43,11 +43,15 @@ void MarkdownEditorConfig::init(const QJsonObject &p_app, const QJsonObject &p_u m_constrainInPlacePreviewWidthEnabled = READBOOL(QStringLiteral("constrain_inplace_preview_width")); m_zoomFactorInReadMode = READREAL(QStringLiteral("zoom_factor_in_read_mode")); m_fetchImagesInParseAndPaste = READBOOL(QStringLiteral("fetch_images_in_parse_and_paste")); + m_protectFromXss = READBOOL(QStringLiteral("protect_from_xss")); m_htmlTagEnabled = READBOOL(QStringLiteral("html_tag")); m_autoBreakEnabled = READBOOL(QStringLiteral("auto_break")); m_linkifyEnabled = READBOOL(QStringLiteral("linkify")); m_indentFirstLineEnabled = READBOOL(QStringLiteral("indent_first_line")); + + m_smartTableEnabled = READBOOL(QStringLiteral("smart_table")); + m_smartTableInterval = READINT(QStringLiteral("smart_table_interval")); } QJsonObject MarkdownEditorConfig::toJson() const @@ -73,6 +77,8 @@ QJsonObject MarkdownEditorConfig::toJson() const obj[QStringLiteral("auto_break")] = m_autoBreakEnabled; obj[QStringLiteral("linkify")] = m_linkifyEnabled; obj[QStringLiteral("indent_first_line")] = m_indentFirstLineEnabled; + obj[QStringLiteral("smart_table")] = m_smartTableEnabled; + obj[QStringLiteral("smart_table_interval")] = m_smartTableInterval; return obj; } @@ -319,3 +325,19 @@ void MarkdownEditorConfig::setSectionNumberStyle(SectionNumberStyle p_style) { updateConfig(m_sectionNumberStyle, p_style, this); } + +bool MarkdownEditorConfig::getSmartTableEnabled() const +{ + return m_smartTableEnabled; +} + +void MarkdownEditorConfig::setSmartTableEnabled(bool p_enabled) +{ + updateConfig(m_smartTableEnabled, p_enabled, this); +} + +int MarkdownEditorConfig::getSmartTableInterval() const +{ + return m_smartTableInterval; +} + diff --git a/src/core/markdowneditorconfig.h b/src/core/markdowneditorconfig.h index fcc884fe..b9cde71a 100644 --- a/src/core/markdowneditorconfig.h +++ b/src/core/markdowneditorconfig.h @@ -95,6 +95,11 @@ namespace vnotex bool getIndentFirstLineEnabled() const; void setIndentFirstLineEnabled(bool p_enabled); + bool getSmartTableEnabled() const; + void setSmartTableEnabled(bool p_enabled); + + int getSmartTableInterval() const; + private: QString sectionNumberModeToString(SectionNumberMode p_mode) const; SectionNumberMode stringToSectionNumberMode(const QString &p_str) const; @@ -154,6 +159,11 @@ namespace vnotex // Whether indent the first line of a paragraph. bool m_indentFirstLineEnabled = false; + + bool m_smartTableEnabled = true; + + // Interval time to do smart table format. + int m_smartTableInterval = 2000; }; } diff --git a/src/data/core/core.qrc b/src/data/core/core.qrc index 282ccf3e..8d26618e 100644 --- a/src/data/core/core.qrc +++ b/src/data/core/core.qrc @@ -57,6 +57,7 @@ icons/type_quote_editor.svg icons/type_link_editor.svg icons/type_image_editor.svg + icons/type_mark_editor.svg icons/type_table_editor.svg icons/add.svg icons/clear.svg diff --git a/src/data/core/icons/type_mark_editor.svg b/src/data/core/icons/type_mark_editor.svg new file mode 100644 index 00000000..e5b56983 --- /dev/null +++ b/src/data/core/icons/type_mark_editor.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/data/core/translations/qtbase_zh_CN.qm b/src/data/core/translations/qtbase_zh_CN.qm index 5225c0e56997897d8ac533da55bdaeb0cb82e801..57886a6e36eed41a723ddba7c40adeedfe93cd50 100644 GIT binary patch delta 127 zcmdlwgMI4^_6^VbSOSZuzMcHKPoFVwvUI;Li~rr|h{*=~L?$QnOLBqa8hHG8{qJsW z?iaNJs`cxeJm;i7>x9;y|{cJPuDFbt`QPUk688w-e;+d!CGBO@u b%$u&q#Q2iQ|L$~2W=35gz1@SE@$Cr!d2}+Y delta 124 zcmV-?0E7Rvm QGnomeTheme &OK - 確定(&O) + 确定(&O) &Save - 儲存(&S) + 保存(&S) &Cancel @@ -1656,11 +1656,11 @@ Do you want to delete it anyway? &Close - 關閉(&C) + 关闭(&C) Close without Saving - 關閉而不儲存 + 关闭而不保存 @@ -2438,11 +2438,11 @@ Do you want to delete it anyway? QMessageBox Show Details... - 顯示詳情... + 显示详情... Hide Details... - 隱藏詳情... + 隐藏详情... <h3>About Qt</h3><p>This program uses Qt version %1.</p> @@ -2455,7 +2455,7 @@ Do you want to delete it anyway? About Qt - 關於 Qt + 关于 Qt <p>Qt is a C++ toolkit for cross-platform application development.</p><p>Qt provides single-source portability across all major desktop operating systems. It is also available for embedded Linux and other embedded and mobile operating systems.</p><p>Qt is available under three different licensing options designed to accommodate the needs of our various users.</p><p>Qt licensed under our commercial license agreement is appropriate for development of proprietary/commercial software where you do not want to share any source code with third parties or otherwise cannot comply with the terms of the GNU LGPL version 3.</p><p>Qt licensed under the GNU LGPL version 3 is appropriate for the development of Qt&nbsp;applications provided you can comply with the terms and conditions of the GNU LGPL version 3.</p><p>Please see <a href="http://%2/">%2</a> for an overview of Qt licensing.</p><p>Copyright (C) %1 The Qt Company Ltd and other contributors.</p><p>Qt and the Qt logo are trademarks of The Qt Company Ltd.</p><p>Qt is The Qt Company Ltd product developed as an open source project. See <a href="http://%3/">%3</a> for more information.</p> @@ -3540,15 +3540,15 @@ Do you want to delete it anyway? QPlatformTheme OK - 確定 + 确定 Save - 儲存 + 保存 Save All - 全部儲存 + 全部保存 Open @@ -3584,7 +3584,7 @@ Do you want to delete it anyway? Close - 關閉 + 关闭 Cancel @@ -3592,7 +3592,7 @@ Do you want to delete it anyway? Discard - 丟棄 + 丢弃 Help diff --git a/src/data/core/vnotex.json b/src/data/core/vnotex.json index ed7fe38a..96942087 100644 --- a/src/data/core/vnotex.json +++ b/src/data/core/vnotex.json @@ -56,6 +56,7 @@ "TypeMath" : "Ctrl+,", "TypeMathBlock" : "Ctrl+.", "TypeTable" : "Ctrl+/", + "TypeMark" : "Ctrl+G, M", "Outline" : "Ctrl+G, O", "RichPaste" : "Ctrl+Shift+V", "FindAndReplace" : "Ctrl+F", @@ -250,7 +251,11 @@ "//comment" : "Whether convert URL-like text to links", "linkify" : true, "//comment" : "Whether add indentation to the first line of paragraph", - "indent_first_line" : false + "indent_first_line" : false, + "//comment" : "Whether enable smart table (formation)", + "smart_table" : true, + "//comment" : "Time interval (milliseconds) to do smart table formation", + "smart_table_interval" : 2000 } }, "widget" : { diff --git a/src/data/extra/themes/moonlight/text-editor.theme b/src/data/extra/themes/moonlight/text-editor.theme index 74d17b76..086d81ce 100644 --- a/src/data/extra/themes/moonlight/text-editor.theme +++ b/src/data/extra/themes/moonlight/text-editor.theme @@ -185,8 +185,8 @@ "font-family" : "YaHei Consolas Hybrid, Consolas, Monaco, Andale Mono, Monospace, Courier New" }, "MARK" : { - "text-color" : "#ccd1d8", - "background-color" : "#551560" + "text-color" : "#d7dae0", + "background-color" : "#898900" }, "TABLE" : { "font-family" : "YaHei Consolas Hybrid, Consolas, Monaco, Andale Mono, Monospace, Courier New" diff --git a/src/widgets/dialogs/settings/markdowneditorpage.cpp b/src/widgets/dialogs/settings/markdowneditorpage.cpp index 1148da5c..600bd40c 100644 --- a/src/widgets/dialogs/settings/markdowneditorpage.cpp +++ b/src/widgets/dialogs/settings/markdowneditorpage.cpp @@ -72,6 +72,8 @@ void MarkdownEditorPage::loadInternal() m_linkifyCheckBox->setChecked(markdownConfig.getLinkifyEnabled()); m_indentFirstLineCheckBox->setChecked(markdownConfig.getIndentFirstLineEnabled()); + + m_smartTableCheckBox->setChecked(markdownConfig.getSmartTableEnabled()); } void MarkdownEditorPage::saveInternal() @@ -110,6 +112,8 @@ void MarkdownEditorPage::saveInternal() markdownConfig.setIndentFirstLineEnabled(m_indentFirstLineCheckBox->isChecked()); + markdownConfig.setSmartTableEnabled(m_smartTableCheckBox->isChecked()); + EditorPage::notifyEditorConfigChange(); } @@ -225,6 +229,16 @@ QGroupBox *MarkdownEditorPage::setupEditGroup() this, &MarkdownEditorPage::pageIsChanged); } + { + const QString label(tr("Smart table")); + m_smartTableCheckBox = WidgetsFactory::createCheckBox(label, box); + m_smartTableCheckBox->setToolTip(tr("Smart table formation")); + layout->addRow(m_smartTableCheckBox); + addSearchItem(label, m_smartTableCheckBox->toolTip(), m_smartTableCheckBox); + connect(m_smartTableCheckBox, &QCheckBox::stateChanged, + this, &MarkdownEditorPage::pageIsChanged); + } + return box; } diff --git a/src/widgets/dialogs/settings/markdowneditorpage.h b/src/widgets/dialogs/settings/markdowneditorpage.h index 48b8d7d8..40f89756 100644 --- a/src/widgets/dialogs/settings/markdowneditorpage.h +++ b/src/widgets/dialogs/settings/markdowneditorpage.h @@ -56,6 +56,8 @@ namespace vnotex QSpinBox *m_sectionNumberBaseLevelSpinBox = nullptr; QComboBox *m_sectionNumberStyleComboBox = nullptr; + + QCheckBox *m_smartTableCheckBox = nullptr; }; } diff --git a/src/widgets/dialogs/tableinsertdialog.cpp b/src/widgets/dialogs/tableinsertdialog.cpp new file mode 100644 index 00000000..92ac28df --- /dev/null +++ b/src/widgets/dialogs/tableinsertdialog.cpp @@ -0,0 +1,91 @@ +#include "tableinsertdialog.h" + +#include +#include +#include +#include +#include +#include + +#include + +using namespace vnotex; + +TableInsertDialog::TableInsertDialog(const QString &p_title, QWidget *p_parent) + : ScrollDialog(p_parent) +{ + setupUI(p_title); +} + +void TableInsertDialog::setupUI(const QString &p_title) +{ + auto mainWidget = new QWidget(this); + setCentralWidget(mainWidget); + + auto mainLayout = new QGridLayout(mainWidget); + + m_rowCountSpinBox = WidgetsFactory::createSpinBox(mainWidget); + m_rowCountSpinBox->setToolTip(tr("Row count of the table body")); + m_rowCountSpinBox->setMaximum(1000); + m_rowCountSpinBox->setMinimum(0); + + mainLayout->addWidget(new QLabel(tr("Row:")), 0, 0, 1, 1); + mainLayout->addWidget(m_rowCountSpinBox, 0, 1, 1, 1); + + m_colCountSpinBox = WidgetsFactory::createSpinBox(mainWidget); + m_colCountSpinBox->setToolTip(tr("Column count of the table")); + m_colCountSpinBox->setMaximum(1000); + m_colCountSpinBox->setMinimum(1); + + mainLayout->addWidget(new QLabel(tr("Column:")), 0, 2, 1, 1); + mainLayout->addWidget(m_colCountSpinBox, 0, 3, 1, 1); + + { + auto noneBtn = new QRadioButton(tr("None"), mainWidget); + auto leftBtn = new QRadioButton(tr("Left"), mainWidget); + auto centerBtn = new QRadioButton(tr("Center"), mainWidget); + auto rightBtn = new QRadioButton(tr("Right"), mainWidget); + + auto alignLayout = new QHBoxLayout(); + alignLayout->addWidget(noneBtn); + alignLayout->addWidget(leftBtn); + alignLayout->addWidget(centerBtn); + alignLayout->addWidget(rightBtn); + alignLayout->addStretch(); + + mainLayout->addWidget(new QLabel(tr("Alignment:")), 1, 0, 1, 1); + mainLayout->addLayout(alignLayout, 1, 1, 1, 3); + + auto buttonGroup = new QButtonGroup(mainWidget); + buttonGroup->addButton(noneBtn, static_cast(Alignment::None)); + buttonGroup->addButton(leftBtn, static_cast(Alignment::Left)); + buttonGroup->addButton(centerBtn, static_cast(Alignment::Center)); + buttonGroup->addButton(rightBtn, static_cast(Alignment::Right)); + + noneBtn->setChecked(true); + connect(buttonGroup, static_cast(&QButtonGroup::buttonToggled), + this, [this](int p_id, bool p_checked){ + if (p_checked) { + m_alignment = static_cast(p_id); + } + }); + } + + setDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + setWindowTitle(p_title); +} + +int TableInsertDialog::getRowCount() const +{ + return m_rowCountSpinBox->value(); +} + +int TableInsertDialog::getColumnCount() const +{ + return m_colCountSpinBox->value(); +} + +Alignment TableInsertDialog::getAlignment() const +{ + return m_alignment; +} diff --git a/src/widgets/dialogs/tableinsertdialog.h b/src/widgets/dialogs/tableinsertdialog.h new file mode 100644 index 00000000..7d434727 --- /dev/null +++ b/src/widgets/dialogs/tableinsertdialog.h @@ -0,0 +1,35 @@ +#ifndef TABLEINSERTDIALOG_H +#define TABLEINSERTDIALOG_H + +#include "scrolldialog.h" + +#include + +class QSpinBox; + +namespace vnotex +{ + class TableInsertDialog : public ScrollDialog + { + Q_OBJECT + public: + TableInsertDialog(const QString &p_title, QWidget *p_parent = nullptr); + + int getRowCount() const; + + int getColumnCount() const; + + Alignment getAlignment() const; + + private: + void setupUI(const QString &p_title); + + QSpinBox *m_rowCountSpinBox = nullptr; + + QSpinBox *m_colCountSpinBox = nullptr; + + Alignment m_alignment = Alignment::None; + }; +} + +#endif // TABLEINSERTDIALOG_H diff --git a/src/widgets/editors/markdowneditor.cpp b/src/widgets/editors/markdowneditor.cpp index 71a6a613..f8f5722c 100644 --- a/src/widgets/editors/markdowneditor.cpp +++ b/src/widgets/editors/markdowneditor.cpp @@ -24,6 +24,7 @@ #include #include +#include #include #include @@ -43,6 +44,7 @@ #include "previewhelper.h" #include "../outlineprovider.h" +#include "markdowntablehelper.h" using namespace vnotex; @@ -78,6 +80,8 @@ MarkdownEditor::MarkdownEditor(const MarkdownEditorConfig &p_config, connect(getHighlighter(), &vte::PegMarkdownHighlighter::headersUpdated, this, &MarkdownEditor::updateHeadings); + setupTableHelper(); + m_headingTimer = new QTimer(this); m_headingTimer->setInterval(500); m_headingTimer->setSingleShot(true); @@ -143,6 +147,12 @@ void MarkdownEditor::typeStrikethrough() vte::MarkdownUtils::typeStrikethrough(m_textEdit); } +void MarkdownEditor::typeMark() +{ + enterInsertModeIfApplicable(); + vte::MarkdownUtils::typeMark(m_textEdit); +} + void MarkdownEditor::typeUnorderedList() { enterInsertModeIfApplicable(); @@ -293,6 +303,41 @@ void MarkdownEditor::typeImage() } } +void MarkdownEditor::typeTable() +{ + TableInsertDialog dialog(tr("Insert Table"), this); + if (dialog.exec() != QDialog::Accepted) { + return; + } + + auto cursor = m_textEdit->textCursor(); + cursor.beginEditBlock(); + if (cursor.hasSelection()) { + cursor.setPosition(qMax(cursor.selectionStart(), cursor.selectionEnd())); + } + + bool newBlock = !cursor.atBlockEnd(); + if (!newBlock && !cursor.atBlockStart()) { + QString text = cursor.block().text().trimmed(); + if (!text.isEmpty() && text != QStringLiteral(">")) { + // Insert a new block before inserting table. + newBlock = true; + } + } + + if (newBlock) { + auto indentationStr = vte::TextEditUtils::fetchIndentationSpaces(cursor.block()); + vte::TextEditUtils::insertBlock(cursor, false); + cursor.insertText(indentationStr); + } + + cursor.endEditBlock(); + m_textEdit->setTextCursor(cursor); + + // Insert table. + m_tableHelper->insertTable(dialog.getRowCount(), dialog.getColumnCount(), dialog.getAlignment()); +} + void MarkdownEditor::setBuffer(Buffer *p_buffer) { m_buffer = p_buffer; @@ -1214,3 +1259,10 @@ void MarkdownEditor::updateFromConfig(bool p_initialized) getHighlighter()->updateHighlight(); } } + +void MarkdownEditor::setupTableHelper() +{ + m_tableHelper = new MarkdownTableHelper(this, this); + connect(getHighlighter(), &vte::PegMarkdownHighlighter::tableBlocksUpdated, + m_tableHelper, &MarkdownTableHelper::updateTableBlocks); +} diff --git a/src/widgets/editors/markdowneditor.h b/src/widgets/editors/markdowneditor.h index f9be76ff..8720e6e4 100644 --- a/src/widgets/editors/markdowneditor.h +++ b/src/widgets/editors/markdowneditor.h @@ -22,6 +22,7 @@ namespace vnotex class PreviewHelper; class Buffer; class MarkdownEditorConfig; + class MarkdownTableHelper; class MarkdownEditor : public vte::VMarkdownEditor { @@ -65,6 +66,8 @@ namespace vnotex void typeStrikethrough(); + void typeMark(); + void typeUnorderedList(); void typeOrderedList(); @@ -85,6 +88,8 @@ namespace vnotex void typeImage(); + void typeTable(); + const QVector &getHeadings() const; int getCurrentHeadingIndex() const; @@ -171,6 +176,8 @@ namespace vnotex // Return true if there is change. bool updateSectionNumber(const QVector &p_headings); + void setupTableHelper(); + static QString generateImageFileNameToInsertAs(const QString &p_title, const QString &p_suffix); const MarkdownEditorConfig &m_config; @@ -190,6 +197,9 @@ namespace vnotex bool m_sectionNumberEnabled = false; OverrideState m_overriddenSectionNumber = OverrideState::NoOverride; + + // Managed by QObject. + MarkdownTableHelper *m_tableHelper = nullptr; }; } diff --git a/src/widgets/editors/markdowntable.cpp b/src/widgets/editors/markdowntable.cpp new file mode 100644 index 00000000..c789b750 --- /dev/null +++ b/src/widgets/editors/markdowntable.cpp @@ -0,0 +1,781 @@ +#include "markdowntable.h" + +#include +#include + +using namespace vnotex; + +void MarkdownTable::Cell::clear() +{ + m_offset = -1; + m_length = 0; + m_text.clear(); + m_formattedText.clear(); + m_cursorCoreOffset = -1; + m_deleted = false; +} + +bool MarkdownTable::Row::isValid() const +{ + return !m_cells.isEmpty(); +} + +void MarkdownTable::Row::clear() +{ + m_block = QTextBlock(); + m_preText.clear(); + m_cells.clear(); +} + +QString MarkdownTable::Row::toString() const +{ + QString cells; + for (auto & cell : m_cells) { + cells += QString(" (%1, %2 [%3])").arg(cell.m_offset) + .arg(cell.m_length) + .arg(cell.m_text); + } + + return QString("row %1 %2").arg(m_block.blockNumber()).arg(cells); +} + +qreal MarkdownTable::s_spaceWidth = -1; + +qreal MarkdownTable::s_minusWidth = -1; + +qreal MarkdownTable::s_colonWidth = -1; + +qreal MarkdownTable::s_defaultDelimiterWidth = -1; + +const QString MarkdownTable::c_defaultDelimiter = "---"; + +const QChar MarkdownTable::c_borderChar = '|'; + +enum +{ + HeaderRowIndex = 0, + DelimiterRowIndex = 1 +}; + +MarkdownTable::MarkdownTable(QTextEdit *p_textEdit, const vte::peg::TableBlock &p_block) + : m_textEdit(p_textEdit) +{ + parseTableBlock(p_block); +} + +MarkdownTable::MarkdownTable(QTextEdit *p_textEdit, int p_bodyRow, int p_col, Alignment p_alignment) + : m_textEdit(p_textEdit), + m_isNew(true) +{ + Q_ASSERT(p_bodyRow >= 0 && p_col > 0); + m_rows.resize(p_bodyRow + 2); + + // PreText for each row. + QString preText; + const QTextCursor cursor = m_textEdit->textCursor(); + Q_ASSERT(cursor.atBlockEnd()); + if (!cursor.atBlockStart()) { + preText = cursor.block().text(); + } + + QString delimiterCore(c_defaultDelimiter); + switch (p_alignment) { + case Alignment::Left: + delimiterCore[0] = ':'; + break; + + case Alignment::Center: + delimiterCore[0] = ':'; + delimiterCore[delimiterCore.size() - 1] = ':'; + break; + + case Alignment::Right: + delimiterCore[delimiterCore.size() - 1] = ':'; + break; + + default: + break; + } + const QString delimiterCell = generateFormattedText(delimiterCore, 0); + const QString contentCell = generateFormattedText(QString(c_defaultDelimiter.size(), QLatin1Char(' ')), 0); + + for (int rowIdx = 0; rowIdx < m_rows.size(); ++rowIdx) { + auto &row = m_rows[rowIdx]; + row.m_preText = preText; + row.m_cells.resize(p_col); + + const QString &content = isDelimiterRow(rowIdx) ? delimiterCell : contentCell; + for (auto &cell : row.m_cells) { + cell.m_text = content; + } + } +} + +bool MarkdownTable::isValid() const +{ + return header() && header()->isValid() && delimiter() && delimiter()->isValid(); +} + +void MarkdownTable::format() +{ + if (!isValid()) { + return; + } + + const QTextCursor cursor = m_textEdit->textCursor(); + int curRowIdx = cursor.blockNumber() - m_rows[0].m_block.blockNumber(); + int curPib = -1; + if (curRowIdx < 0 || curRowIdx >= m_rows.size()) { + curRowIdx = -1; + } else { + curPib = cursor.positionInBlock(); + } + + int nrCols = calculateColumnCount(); + pruneColumns(nrCols); + + for (int i = 0; i < nrCols; ++i) { + formatColumn(i, curRowIdx, curPib); + } +} + +void MarkdownTable::write() +{ + if (m_isNew) { + writeNewTable(); + } else { + writeTable(); + } +} + +void MarkdownTable::parseTableBlock(const vte::peg::TableBlock &p_block) +{ + auto doc = m_textEdit->document(); + + QTextBlock block = doc->findBlock(p_block.m_startPos); + if (!block.isValid()) { + return; + } + + int lastBlockNumber = doc->findBlock(p_block.m_endPos - 1).blockNumber(); + if (lastBlockNumber == -1) { + return; + } + + const QVector &borders = p_block.m_borders; + if (borders.isEmpty()) { + return; + } + + int numRows = lastBlockNumber - block.blockNumber() + 1; + if (numRows <= DelimiterRowIndex) { + return; + } + + initWidths(block, borders[0]); + + int borderIdx = 0; + m_rows.reserve(numRows); + for (int i = 0; i < numRows; ++i) { + m_rows.append(Row()); + if (!parseRow(block, borders, borderIdx, m_rows.last())) { + clear(); + return; + } + + qDebug() << "row" << i << m_rows.last().toString(); + + block = block.next(); + } +} + +void MarkdownTable::clear() +{ + m_rows.clear(); +} + +void MarkdownTable::initWidths(const QTextBlock &p_block, int p_borderPos) +{ + if (s_spaceWidth != -1) { + return; + } + + QFont font = m_textEdit->font(); + int pib = p_borderPos - p_block.position(); + auto fmts = p_block.layout()->formats(); + for (const auto &fmt : fmts) { + if (fmt.start <= pib && fmt.start + fmt.length > pib) { + // Hit. + if (!fmt.format.fontFamily().isEmpty()) { + font = fmt.format.font(); + break; + } + } + } + + QFontMetricsF fmf(font); + s_spaceWidth = fmf.width(' '); + s_minusWidth = fmf.width('-'); + s_colonWidth = fmf.width(':'); + s_defaultDelimiterWidth = fmf.width(c_defaultDelimiter); + + qDebug() << "smart table widths" << font.family() << s_spaceWidth << s_minusWidth << s_colonWidth << s_defaultDelimiterWidth; +} + +bool MarkdownTable::parseRow(const QTextBlock &p_block, + const QVector &p_borders, + int &p_borderIdx, + Row &p_row) const +{ + if (!p_block.isValid() || p_borderIdx >= p_borders.size()) { + return false; + } + + p_row.m_block = p_block; + + QString text = p_block.text(); + int startPos = p_block.position(); + int endPos = startPos + text.length(); + + if (p_borders[p_borderIdx] < startPos + || p_borders[p_borderIdx] >= endPos) { + return false; + } + + // Get pre text. + int firstCellOffset = p_borders[p_borderIdx] - startPos; + if (text[firstCellOffset] != c_borderChar) { + return false; + } + p_row.m_preText = text.left(firstCellOffset); + + for (; p_borderIdx < p_borders.size(); ++p_borderIdx) { + int border = p_borders[p_borderIdx]; + if (border >= endPos) { + break; + } + + int offset = border - startPos; + if (text[offset] != c_borderChar) { + return false; + } + + int nextIdx = p_borderIdx + 1; + if (nextIdx >= p_borders.size() || p_borders[nextIdx] >= endPos) { + // The last border of this row. + ++p_borderIdx; + break; + } + + int nextOffset = p_borders[nextIdx] - startPos; + if (text[nextOffset] != c_borderChar) { + return false; + } + + // Got one cell. + Cell cell; + cell.m_offset = offset; + cell.m_length = nextOffset - offset; + cell.m_text = text.mid(cell.m_offset, cell.m_length); + + p_row.m_cells.append(cell); + } + + return true; +} + +const MarkdownTable::Row *MarkdownTable::header() const +{ + if (m_rows.size() <= HeaderRowIndex) { + return nullptr; + } + + return &m_rows[HeaderRowIndex]; +} + +const MarkdownTable::Row *MarkdownTable::delimiter() const +{ + if (m_rows.size() <= DelimiterRowIndex) { + return nullptr; + } + + return &m_rows[DelimiterRowIndex]; +} + +int MarkdownTable::calculateColumnCount() const +{ + // We use the width of the header as the width of the table. + // With this, we could add or remove one column by just changing the header row. + return header()->m_cells.size(); +} + +void MarkdownTable::pruneColumns(int p_nrCols) +{ + for (auto &row : m_rows) { + for (int i = p_nrCols; i < row.m_cells.size(); ++i) { + row.m_cells[i].m_deleted = true; + } + } +} + +void MarkdownTable::formatColumn(int p_idx, int p_cursorRowIdx, int p_cursorPib) +{ + QVector cells; + // Target width of this column. + qreal targetWidth = 0; + fetchCellInfoOfColumn(p_idx, p_cursorRowIdx, p_cursorPib, cells, targetWidth); + + // Get the alignment of this column. + const auto align = getColumnAlignment(p_idx); + + // Calculate the formatted text of each cell. + for (int rowIdx = 0; rowIdx < cells.size(); ++rowIdx) { + const auto &info = cells[rowIdx]; + auto &row = m_rows[rowIdx]; + if (row.m_cells.size() <= p_idx) { + row.m_cells.resize(p_idx + 1); + } + auto &cell = row.m_cells[p_idx]; + Q_ASSERT(cell.m_formattedText.isEmpty()); + Q_ASSERT(cell.m_cursorCoreOffset == -1); + + // Record the cursor position. + if (rowIdx == p_cursorRowIdx) { + if (cell.m_offset <= p_cursorPib && cell.m_offset + cell.m_length > p_cursorPib) { + // Cursor in this cell. + int offset = p_cursorPib - cell.m_offset; + offset = offset - info.m_coreOffset; + if (offset > info.m_coreLength) { + offset = info.m_coreLength; + } else if (offset < 0) { + offset = 0; + } + + cell.m_cursorCoreOffset = offset; + } + } + + if (isDelimiterRow(rowIdx)) { + if (!isDelimiterCellWellFormatted(cell, info, targetWidth)) { + QString core; + int delta = s_minusWidth - 1; + switch (align) { + case Alignment::None: + { + int coreLength = static_cast((targetWidth + delta) / s_minusWidth); + core = QString(coreLength, '-'); + break; + } + + case Alignment::Left: + { + int coreLength = static_cast((targetWidth - s_colonWidth + delta) / s_minusWidth); + core = QStringLiteral(":"); + core += QString(coreLength, '-'); + break; + } + + case Alignment::Center: + { + int coreLength = static_cast((targetWidth - 2 * s_colonWidth + delta) / s_minusWidth); + core = QStringLiteral(":"); + core += QString(coreLength, '-'); + core += QStringLiteral(":"); + break; + } + + case Alignment::Right: + { + int coreLength = static_cast((targetWidth - s_colonWidth + delta) / s_minusWidth); + core = QString(coreLength, '-'); + core += QStringLiteral(":"); + break; + } + + default: + Q_ASSERT(false); + break; + } + + cell.m_formattedText = generateFormattedText(core, 0); + if (cell.m_text == cell.m_formattedText) { + // Avoid infinite change. + cell.m_formattedText.clear(); + } + } + } else { + Alignment fakeAlign = align; + if (fakeAlign == Alignment::None) { + // For Alignment::None, we make the header align center while + // content cells align left. + if (isHeaderRow(rowIdx)) { + fakeAlign = Alignment::Center; + } else { + fakeAlign = Alignment::Left; + } + } + + if (!isCellWellFormatted(row, cell, info, targetWidth, fakeAlign)) { + QString core = cell.m_text.mid(info.m_coreOffset, info.m_coreLength); + int nr = static_cast((targetWidth - info.m_coreWidth + s_spaceWidth - 1) / s_spaceWidth); + cell.m_formattedText = generateFormattedText(core, nr, fakeAlign); + + // For cells crossing lines and having spaces at the end of one line, + // Qt will collapse those spaces, which make it not well formatted. + if (cell.m_text == cell.m_formattedText) { + cell.m_formattedText.clear(); + } + } + } + } +} + +void MarkdownTable::fetchCellInfoOfColumn(int p_idx, + int p_cursorRowIdx, + int p_cursorPib, + QVector &p_cellsInfo, + qreal &p_targetWidth) const +{ + p_targetWidth = s_defaultDelimiterWidth; + p_cellsInfo.resize(m_rows.size()); + + // Fetch the trimmed core content and its width. + for (int i = 0; i < m_rows.size(); ++i) { + const auto &row = m_rows[i]; + auto &info = p_cellsInfo[i]; + + if (row.m_cells.size() <= p_idx) { + // Need to add a new cell later. + continue; + } + + // Get the info of this cell. + const auto &cell = row.m_cells[p_idx]; + int first = fetchCoreOffset(cell.m_text); + if (first == -1) { + // Empty cell. + continue; + } + info.m_coreOffset = first; + + // If the cursor is in this cell, then we should treat the core length at least not + // less than the cursor position even if there is trailing spaces before the cursor. + int last = cell.m_length - 1; + for (; last >= first; --last) { + if ((p_cursorRowIdx == i && p_cursorPib - cell.m_offset - 1 == last) || cell.m_text[last] != ' ') { + // Found the last of core content. + info.m_coreLength = last - first + 1; + break; + } + } + + // Calculate the core width. + info.m_coreWidth = calculateTextWidth(row.m_block, + cell.m_offset + info.m_coreOffset, + info.m_coreLength); + // Delimiter row's width should not be considered. + if (info.m_coreWidth > p_targetWidth && !isDelimiterRow(i)) { + p_targetWidth = info.m_coreWidth; + } + } +} + +bool MarkdownTable::isDelimiterRow(int p_idx) const +{ + return p_idx == DelimiterRowIndex; +} + +qreal MarkdownTable::calculateTextWidth(const QTextBlock &p_block, int p_pib, int p_length) const +{ + // The block may cross multiple lines. + qreal textWidth = 0; + QTextLayout *layout = p_block.layout(); + QTextLine line = layout->lineForTextPosition(p_pib); + while (line.isValid()) { + int lineEnd = line.textStart() + line.textLength(); + if (lineEnd >= p_pib + p_length) { + // The last line. + textWidth += line.cursorToX(p_pib + p_length) - line.cursorToX(p_pib); + break; + } else { + // Cross lines. + textWidth += line.cursorToX(lineEnd) - line.cursorToX(p_pib); + + // Move to next line. + p_length = p_length - (lineEnd - p_pib); + p_pib = lineEnd; + line = layout->lineForTextPosition(p_pib + 1); + } + } + + return textWidth > 0 ? textWidth : -1; +} + +Alignment MarkdownTable::getColumnAlignment(int p_idx) const +{ + auto row = delimiter(); + if (row->m_cells.size() <= p_idx) { + return Alignment::None; + } + + QString core = row->m_cells[p_idx].m_text.mid(1).trimmed(); + Q_ASSERT(!core.isEmpty()); + bool leftColon = core[0] == ':'; + bool rightColon = core[core.size() - 1] == ':'; + if (leftColon) { + if (rightColon) { + return Alignment::Center; + } else { + return Alignment::Left; + } + } else { + if (rightColon) { + return Alignment::Right; + } else { + return Alignment::None; + } + } +} + +static bool equalWidth(int p_a, int p_b, int p_margin = 5) +{ + return qAbs(p_a - p_b) <= p_margin; +} + +bool MarkdownTable::isDelimiterCellWellFormatted(const Cell &p_cell, + const CellInfo &p_info, + qreal p_targetWidth) const +{ + // We could use core width here for delimiter cell. + if (!equalWidth(p_info.m_coreWidth, p_targetWidth, s_minusWidth / 2)) { + return false; + } + + const QString &text = p_cell.m_text; + if (text.size() < 4) { + return false; + } + + if (text[1] != ' ' || text[text.size() - 1] != ' ') { + return false; + } + + if (text[2] == ' ' || text[text.size() - 2] == ' ') { + return false; + } + + return true; +} + +QString MarkdownTable::generateFormattedText(const QString &p_core, + int p_nrSpaces, + Alignment p_align) const +{ + Q_ASSERT(p_align != Alignment::None); + + // Align left. + int leftSpaces = 0; + int rightSpaces = p_nrSpaces; + + if (p_align == Alignment::Center) { + leftSpaces = p_nrSpaces / 2; + rightSpaces = p_nrSpaces - leftSpaces; + } else if (p_align == Alignment::Right) { + leftSpaces = p_nrSpaces; + rightSpaces = 0; + } + + return QString("%1 %2%3%4 ").arg(c_borderChar, + QString(leftSpaces, ' '), + p_core, + QString(rightSpaces, ' ')); +} + +bool MarkdownTable::isHeaderRow(int p_idx) const +{ + return p_idx == HeaderRowIndex; +} + +bool MarkdownTable::isCellWellFormatted(const Row &p_row, + const Cell &p_cell, + const CellInfo &p_info, + int p_targetWidth, + Alignment p_align) const +{ + Q_ASSERT(p_align != Alignment::None); + const QString &text = p_cell.m_text; + if (text.size() < 4) { + return false; + } + + if (text[1] != ' ' || text[text.size() - 1] != ' ') { + return false; + } + + // Skip alignment check of empty cell. + if (p_info.m_coreOffset > 0) { + int leftSpaces = p_info.m_coreOffset - 2; + int rightSpaces = text.size() - p_info.m_coreOffset - p_info.m_coreLength - 1; + switch (p_align) { + case Alignment::Left: + if (leftSpaces > 0) { + return false; + } + + break; + + case Alignment::Center: + if (qAbs(leftSpaces - rightSpaces) > 1) { + return false; + } + + break; + + case Alignment::Right: + if (rightSpaces > 0) { + return false; + } + + break; + + default: + Q_ASSERT(false); + break; + } + } + + // Calculate the width of the text without two spaces around. + int cellWidth = calculateTextWidth(p_row.m_block, + p_cell.m_offset + 2, + p_cell.m_length - 3); + if (!equalWidth(cellWidth, p_targetWidth, s_spaceWidth / 2)) { + return false; + } + + return true; +} + +void MarkdownTable::writeTable() +{ + bool changed = false; + // Use cursor(QTextDocument) to handle the corner case when cursor locates at the end of one row. + QTextCursor cursor(m_textEdit->document()); + int cursorBlock = -1, cursorPib = -1; + bool cursorHit = false; + + // Write the table row by row. + for (const auto &row : m_rows) { + bool needChange = false; + for (const auto &cell : row.m_cells) { + if (!cell.m_formattedText.isEmpty() || cell.m_deleted) { + needChange = true; + break; + } + } + + if (!needChange) { + continue; + } + + if (!changed) { + changed = true; + const QTextCursor curCursor = m_textEdit->textCursor(); + cursorBlock = curCursor.blockNumber(); + cursorPib = curCursor.positionInBlock(); + cursor.beginEditBlock(); + } + + // Construct the block text. + QString newBlockText(row.m_preText); + for (const auto &cell : row.m_cells) { + if (cell.m_deleted) { + continue; + } + + int pos = newBlockText.size(); + if (cell.m_formattedText.isEmpty()) { + newBlockText += cell.m_text; + } else { + newBlockText += cell.m_formattedText; + } + + if (cell.m_cursorCoreOffset > -1) { + // Cursor in this cell. + cursorHit = true; + // We need to calculate the new core offset of this cell. + // For delimiter row, this way won't work, but that is fine. + int coreOffset = fetchCoreOffset(cell.m_formattedText.isEmpty() ? cell.m_text : cell.m_formattedText); + cursorPib = pos + cell.m_cursorCoreOffset + coreOffset; + if (cursorPib >= newBlockText.size()) { + cursorPib = newBlockText.size() - 1; + } + } + } + + newBlockText += c_borderChar; + + // Replace the whole block. + cursor.setPosition(row.m_block.position()); + cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor); + cursor.insertText(newBlockText); + } + + if (changed) { + qDebug() << "write formatted table with cursor block" << cursorBlock; + cursor.endEditBlock(); + + // Restore the cursor. + if (cursorHit) { + QTextBlock block = m_textEdit->document()->findBlockByNumber(cursorBlock); + if (block.isValid()) { + int pos = block.position() + cursorPib; + auto curCursor = m_textEdit->textCursor(); + curCursor.setPosition(pos); + m_textEdit->setTextCursor(curCursor); + } + } + } +} + +void MarkdownTable::writeNewTable() +{ + // Generate the text of the whole table. + QString tableText; + for (int rowIdx = 0; rowIdx < m_rows.size(); ++rowIdx) { + const auto &row = m_rows[rowIdx]; + tableText += row.m_preText; + for (const auto &cell : row.m_cells) { + if (cell.m_deleted) { + continue; + } + + tableText += cell.m_text; + } + + tableText += c_borderChar; + + if (rowIdx < m_rows.size() - 1) { + tableText += '\n'; + } + } + + QTextCursor cursor = m_textEdit->textCursor(); + int pos = cursor.position() + 2; + cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::MoveAnchor); + cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor); + cursor.insertText(tableText); + cursor.setPosition(pos); + m_textEdit->setTextCursor(cursor); +} + +int MarkdownTable::fetchCoreOffset(const QString &p_cellText) +{ + // [0] is the border char. To find the offset of the core content. + for (int i = 1; i < p_cellText.size(); ++i) { + if (p_cellText[i] != ' ') { + return i; + } + } + + return -1; +} diff --git a/src/widgets/editors/markdowntable.h b/src/widgets/editors/markdowntable.h new file mode 100644 index 00000000..092e2b48 --- /dev/null +++ b/src/widgets/editors/markdowntable.h @@ -0,0 +1,165 @@ +#ifndef MARKDOWNTABLE_H +#define MARKDOWNTABLE_H + +#include +#include + +#include + +#include + +class QTextEdit; + +namespace vnotex +{ + class MarkdownTable + { + public: + MarkdownTable(QTextEdit *p_textEdit, const vte::peg::TableBlock &p_block); + + MarkdownTable(QTextEdit *p_textEdit, int p_bodyRow, int p_col, Alignment p_alignment); + + bool isValid() const; + + void format(); + + void write(); + + private: + struct Cell + { + void clear(); + + // Start offset within block, including the starting border |. + int m_offset = -1; + + // Length of this cell, till next border |. + int m_length = 0; + + // Text like "| vnote ". + QString m_text; + + // Formatted text, such as "| vnote ". + // It is empty if it does not need formatted. + QString m_formattedText; + + // If cursor is within this cell, this will not be -1. + int m_cursorCoreOffset = -1; + + // Whether this cell need to be deleted. + bool m_deleted = false; + }; + + struct Row + { + bool isValid() const; + + void clear(); + + QString toString() const; + + QTextBlock m_block; + + // Text before (the first cell of) table row. + QString m_preText; + + QVector m_cells; + }; + + // Used to hold info about a cell when formatting a column. + struct CellInfo + { + // The offset of the core content within the cell. + // Will be 0 if it is an empty cell. + int m_coreOffset = 0; + + // The length of the core content. + // Will be 0 if it is an empty cell. + int m_coreLength = 0; + + // Pixel width of the core content. + qreal m_coreWidth = 0; + }; + + void parseTableBlock(const vte::peg::TableBlock &p_block); + + void clear(); + + void initWidths(const QTextBlock &p_block, int p_borderPos); + + // Parse one row into @p_row and move @p_borderIdx forward. + bool parseRow(const QTextBlock &p_block, + const QVector &p_borders, + int &p_borderIdx, + Row &p_row) const; + + const Row *header() const; + + const Row *delimiter() const; + + int calculateColumnCount() const; + + // Prune columns beyond the header row that should be deleted. + void pruneColumns(int p_nrCols); + + void formatColumn(int p_idx, int p_cursorRowIdx, int p_cursorPib); + + void fetchCellInfoOfColumn(int p_idx, + int p_cursorRowIdx, + int p_cursorPib, + QVector &p_cellsInfo, + qreal &p_targetWidth) const; + + bool isHeaderRow(int p_idx) const; + + bool isDelimiterRow(int p_idx) const; + + qreal calculateTextWidth(const QTextBlock &p_block, int p_pib, int p_length) const; + + Alignment getColumnAlignment(int p_idx) const; + + bool isDelimiterCellWellFormatted(const Cell &p_cell, + const CellInfo &p_info, + qreal p_targetWidth) const; + + // @p_nrSpaces: number of spaces to fill core content. + QString generateFormattedText(const QString &p_core, + int p_nrSpaces, + Alignment p_align = Alignment::Left) const; + + bool isCellWellFormatted(const Row &p_row, + const Cell &p_cell, + const CellInfo &p_info, + int p_targetWidth, + Alignment p_align) const; + + void writeTable(); + + void writeNewTable(); + + // Return -1 if it is an empty cell. + static int fetchCoreOffset(const QString &p_cellText); + + QTextEdit *m_textEdit = nullptr; + + // Whether this table is a new table or not. + bool m_isNew = false; + + // Header, delimiter, and body. + QVector m_rows; + + static qreal s_spaceWidth; + + static qreal s_minusWidth; + + static qreal s_colonWidth; + + static qreal s_defaultDelimiterWidth; + + static const QString c_defaultDelimiter; + + static const QChar c_borderChar; + }; +} + +#endif // MARKDOWNTABLE_H diff --git a/src/widgets/editors/markdowntablehelper.cpp b/src/widgets/editors/markdowntablehelper.cpp new file mode 100644 index 00000000..ab1980af --- /dev/null +++ b/src/widgets/editors/markdowntablehelper.cpp @@ -0,0 +1,113 @@ +#include "markdowntablehelper.h" + +#include +#include +#include + +#include +#include + +#include +#include +#include + +using namespace vnotex; + +MarkdownTableHelper::MarkdownTableHelper(vte::VTextEditor *p_editor, QObject *p_parent) + : QObject(p_parent), + m_editor(p_editor) +{ +} + +bool MarkdownTableHelper::isSmartTableEnabled() const +{ + return ConfigMgr::getInst().getEditorConfig().getMarkdownEditorConfig().getSmartTableEnabled(); +} + +QTimer *MarkdownTableHelper::getTimer() +{ + if (!m_timer) { + m_timer = new QTimer(this); + m_timer->setSingleShot(true); + m_timer->setInterval(ConfigMgr::getInst().getEditorConfig().getMarkdownEditorConfig().getSmartTableInterval()); + connect(m_timer, &QTimer::timeout, + this, &MarkdownTableHelper::formatTable); + } + + return m_timer; +} + +void MarkdownTableHelper::formatTable() +{ + if (!isSmartTableEnabled()) { + return; + } + + if (!m_block.isValid()) { + return; + } + + + MarkdownTable table(m_editor->getTextEdit(), m_block); + if (!table.isValid()) { + return; + } + + table.format(); + + table.write(); +} + +void MarkdownTableHelper::updateTableBlocks(const QVector &p_blocks) +{ + if (!isSmartTableEnabled()) { + return; + } + + getTimer()->stop(); + + if (m_editor->isReadOnly() || !m_editor->isModified()) { + return; + } + + int idx = currentCursorTableBlock(p_blocks); + if (idx == -1) { + return; + } + + m_block = p_blocks[idx]; + getTimer()->start(); +} + +int MarkdownTableHelper::currentCursorTableBlock(const QVector &p_blocks) const +{ + // Binary search. + int curPos = m_editor->getTextEdit()->textCursor().position(); + + int first = 0, last = p_blocks.size() - 1; + while (first <= last) { + int mid = (first + last) / 2; + const auto &block = p_blocks[mid]; + if (block.m_startPos <= curPos && block.m_endPos >= curPos) { + return mid; + } + + if (block.m_startPos > curPos) { + last = mid - 1; + } else { + first = mid + 1; + } + } + + return -1; +} + +void MarkdownTableHelper::insertTable(int p_bodyRow, int p_col, Alignment p_alignment) +{ + MarkdownTable table(m_editor->getTextEdit(), p_bodyRow, p_col, p_alignment); + if (!table.isValid()) { + return; + } + + table.write(); +} diff --git a/src/widgets/editors/markdowntablehelper.h b/src/widgets/editors/markdowntablehelper.h new file mode 100644 index 00000000..efc66e62 --- /dev/null +++ b/src/widgets/editors/markdowntablehelper.h @@ -0,0 +1,47 @@ +#ifndef MARKDOWNTABLEHELPER_H +#define MARKDOWNTABLEHELPER_H + +#include + +#include "markdowntable.h" + +class QTimer; + +namespace vte +{ + class VTextEditor; +} + +namespace vnotex +{ + class MarkdownTableHelper : public QObject + { + Q_OBJECT + public: + MarkdownTableHelper(vte::VTextEditor *p_editor, QObject *p_parent = nullptr); + + void insertTable(int p_bodyRow, int p_col, Alignment p_alignment); + + public slots: + void updateTableBlocks(const QVector &p_blocks); + + private: + // Return the block index which contains the cursor. + int currentCursorTableBlock(const QVector &p_blocks) const; + + void formatTable(); + + bool isSmartTableEnabled() const; + + QTimer *getTimer(); + + vte::VTextEditor *m_editor = nullptr; + + // Use getTimer() to access. + QTimer *m_timer = nullptr; + + vte::peg::TableBlock m_block; + }; +} + +#endif // MARKDOWNTABLEHELPER_H diff --git a/src/widgets/markdownviewwindow.cpp b/src/widgets/markdownviewwindow.cpp index 34825aa6..956d6d6e 100644 --- a/src/widgets/markdownviewwindow.cpp +++ b/src/widgets/markdownviewwindow.cpp @@ -274,6 +274,7 @@ void MarkdownViewWindow::setupToolBar() addAction(toolBar, ViewWindowToolBarHelper::TypeBold); addAction(toolBar, ViewWindowToolBarHelper::TypeItalic); addAction(toolBar, ViewWindowToolBarHelper::TypeStrikethrough); + addAction(toolBar, ViewWindowToolBarHelper::TypeMark); addAction(toolBar, ViewWindowToolBarHelper::TypeUnorderedList); addAction(toolBar, ViewWindowToolBarHelper::TypeOrderedList); addAction(toolBar, ViewWindowToolBarHelper::TypeTodoList); @@ -630,6 +631,10 @@ void MarkdownViewWindow::handleTypeAction(TypeAction p_action) m_editor->typeStrikethrough(); break; + case TypeAction::Mark: + m_editor->typeMark(); + break; + case TypeAction::UnorderedList: m_editor->typeUnorderedList(); break; @@ -674,6 +679,10 @@ void MarkdownViewWindow::handleTypeAction(TypeAction p_action) m_editor->typeImage(); break; + case TypeAction::Table: + m_editor->typeTable(); + break; + default: qWarning() << "TypeAction not handled" << p_action; break; diff --git a/src/widgets/viewwindow.cpp b/src/widgets/viewwindow.cpp index 1b1b533a..0e417a05 100644 --- a/src/widgets/viewwindow.cpp +++ b/src/widgets/viewwindow.cpp @@ -360,6 +360,8 @@ QAction *ViewWindow::addAction(QToolBar *p_toolBar, ViewWindowToolBarHelper::Act case ViewWindowToolBarHelper::TypeImage: Q_FALLTHROUGH(); case ViewWindowToolBarHelper::TypeTable: + Q_FALLTHROUGH(); + case ViewWindowToolBarHelper::TypeMark: { act = ViewWindowToolBarHelper::addAction(p_toolBar, p_action); connect(this, &ViewWindow::modeChanged, @@ -616,7 +618,7 @@ void ViewWindow::handleSectionNumberOverride(OverrideState p_state) ViewWindow::TypeAction ViewWindow::toolBarActionToTypeAction(ViewWindowToolBarHelper::Action p_action) { Q_ASSERT(p_action >= ViewWindowToolBarHelper::Action::TypeBold - && p_action <= ViewWindowToolBarHelper::Action::TypeTable); + && p_action <= ViewWindowToolBarHelper::Action::TypeMax); return static_cast(TypeAction::Bold + (p_action - ViewWindowToolBarHelper::Action::TypeBold)); } diff --git a/src/widgets/viewwindow.h b/src/widgets/viewwindow.h index cece9d86..0c0ab8a1 100644 --- a/src/widgets/viewwindow.h +++ b/src/widgets/viewwindow.h @@ -128,7 +128,8 @@ namespace vnotex Quote, Link, Image, - TypeTable + Table, + Mark }; protected slots: diff --git a/src/widgets/viewwindowtoolbarhelper.cpp b/src/widgets/viewwindowtoolbarhelper.cpp index b61b2804..c71da160 100644 --- a/src/widgets/viewwindowtoolbarhelper.cpp +++ b/src/widgets/viewwindowtoolbarhelper.cpp @@ -268,6 +268,12 @@ QAction *ViewWindowToolBarHelper::addAction(QToolBar *p_tb, Action p_action) addActionShortcut(act, editorConfig.getShortcut(Shortcut::TypeTable), viewWindow); break; + case Action::TypeMark: + act = p_tb->addAction(ToolBarHelper::generateIcon("type_mark_editor.svg"), + ViewWindow::tr("Mark")); + addActionShortcut(act, editorConfig.getShortcut(Shortcut::TypeMark), viewWindow); + break; + case Action::Attachment: { act = p_tb->addAction(ToolBarHelper::generateIcon("attachment_editor.svg"), diff --git a/src/widgets/viewwindowtoolbarhelper.h b/src/widgets/viewwindowtoolbarhelper.h index f37436bb..78f1d172 100644 --- a/src/widgets/viewwindowtoolbarhelper.h +++ b/src/widgets/viewwindowtoolbarhelper.h @@ -36,6 +36,9 @@ namespace vnotex TypeLink, TypeImage, TypeTable, + TypeMark, + // Ending TypeXXX. + TypeMax, Attachment, Outline, diff --git a/src/widgets/widgets.pri b/src/widgets/widgets.pri index 11d8ef54..d6988e6b 100644 --- a/src/widgets/widgets.pri +++ b/src/widgets/widgets.pri @@ -22,9 +22,12 @@ SOURCES += \ $$PWD/dialogs/settings/settingsdialog.cpp \ $$PWD/dialogs/settings/texteditorpage.cpp \ $$PWD/dialogs/settings/themepage.cpp \ + $$PWD/dialogs/tableinsertdialog.cpp \ $$PWD/dragdropareaindicator.cpp \ $$PWD/editors/editormarkdownvieweradapter.cpp \ $$PWD/editors/markdowneditor.cpp \ + $$PWD/editors/markdowntable.cpp \ + $$PWD/editors/markdowntablehelper.cpp \ $$PWD/editors/markdownviewer.cpp \ $$PWD/editors/markdownvieweradapter.cpp \ $$PWD/editors/previewhelper.cpp \ @@ -104,9 +107,12 @@ HEADERS += \ $$PWD/dialogs/settings/settingsdialog.h \ $$PWD/dialogs/settings/texteditorpage.h \ $$PWD/dialogs/settings/themepage.h \ + $$PWD/dialogs/tableinsertdialog.h \ $$PWD/dragdropareaindicator.h \ $$PWD/editors/editormarkdownvieweradapter.h \ $$PWD/editors/markdowneditor.h \ + $$PWD/editors/markdowntable.h \ + $$PWD/editors/markdowntablehelper.h \ $$PWD/editors/markdownviewer.h \ $$PWD/editors/markdownvieweradapter.h \ $$PWD/editors/previewhelper.h \