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 5225c0e5..57886a6e 100644
Binary files a/src/data/core/translations/qtbase_zh_CN.qm and b/src/data/core/translations/qtbase_zh_CN.qm differ
diff --git a/src/data/core/translations/qtbase_zh_CN.ts b/src/data/core/translations/qtbase_zh_CN.ts
index 353a9d00..6206cecf 100644
--- a/src/data/core/translations/qtbase_zh_CN.ts
+++ b/src/data/core/translations/qtbase_zh_CN.ts
@@ -1644,11 +1644,11 @@ Do you want to delete it anyway?
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 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 \
|