mirror of
https://gitee.com/vnotex/vnote.git
synced 2025-07-05 13:59:52 +08:00
parent
6503b433e1
commit
75dc7c6f28
@ -1 +1 @@
|
|||||||
Subproject commit 69bd57656ccac8cf75502506be0c87d80d86e577
|
Subproject commit 3e45827ae9a662bdc61da1090f4e51fdff24af85
|
@ -43,6 +43,7 @@ namespace vnotex
|
|||||||
TypeLink,
|
TypeLink,
|
||||||
TypeImage,
|
TypeImage,
|
||||||
TypeTable,
|
TypeTable,
|
||||||
|
TypeMark,
|
||||||
Outline,
|
Outline,
|
||||||
RichPaste,
|
RichPaste,
|
||||||
FindAndReplace,
|
FindAndReplace,
|
||||||
|
@ -79,6 +79,14 @@ namespace vnotex
|
|||||||
ForceEnable = 1,
|
ForceEnable = 1,
|
||||||
ForceDisable = 2
|
ForceDisable = 2
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum class Alignment
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Left,
|
||||||
|
Center,
|
||||||
|
Right
|
||||||
|
};
|
||||||
} // ns vnotex
|
} // ns vnotex
|
||||||
|
|
||||||
Q_DECLARE_OPERATORS_FOR_FLAGS(vnotex::FindOptions);
|
Q_DECLARE_OPERATORS_FOR_FLAGS(vnotex::FindOptions);
|
||||||
|
@ -43,11 +43,15 @@ void MarkdownEditorConfig::init(const QJsonObject &p_app, const QJsonObject &p_u
|
|||||||
m_constrainInPlacePreviewWidthEnabled = READBOOL(QStringLiteral("constrain_inplace_preview_width"));
|
m_constrainInPlacePreviewWidthEnabled = READBOOL(QStringLiteral("constrain_inplace_preview_width"));
|
||||||
m_zoomFactorInReadMode = READREAL(QStringLiteral("zoom_factor_in_read_mode"));
|
m_zoomFactorInReadMode = READREAL(QStringLiteral("zoom_factor_in_read_mode"));
|
||||||
m_fetchImagesInParseAndPaste = READBOOL(QStringLiteral("fetch_images_in_parse_and_paste"));
|
m_fetchImagesInParseAndPaste = READBOOL(QStringLiteral("fetch_images_in_parse_and_paste"));
|
||||||
|
|
||||||
m_protectFromXss = READBOOL(QStringLiteral("protect_from_xss"));
|
m_protectFromXss = READBOOL(QStringLiteral("protect_from_xss"));
|
||||||
m_htmlTagEnabled = READBOOL(QStringLiteral("html_tag"));
|
m_htmlTagEnabled = READBOOL(QStringLiteral("html_tag"));
|
||||||
m_autoBreakEnabled = READBOOL(QStringLiteral("auto_break"));
|
m_autoBreakEnabled = READBOOL(QStringLiteral("auto_break"));
|
||||||
m_linkifyEnabled = READBOOL(QStringLiteral("linkify"));
|
m_linkifyEnabled = READBOOL(QStringLiteral("linkify"));
|
||||||
m_indentFirstLineEnabled = READBOOL(QStringLiteral("indent_first_line"));
|
m_indentFirstLineEnabled = READBOOL(QStringLiteral("indent_first_line"));
|
||||||
|
|
||||||
|
m_smartTableEnabled = READBOOL(QStringLiteral("smart_table"));
|
||||||
|
m_smartTableInterval = READINT(QStringLiteral("smart_table_interval"));
|
||||||
}
|
}
|
||||||
|
|
||||||
QJsonObject MarkdownEditorConfig::toJson() const
|
QJsonObject MarkdownEditorConfig::toJson() const
|
||||||
@ -73,6 +77,8 @@ QJsonObject MarkdownEditorConfig::toJson() const
|
|||||||
obj[QStringLiteral("auto_break")] = m_autoBreakEnabled;
|
obj[QStringLiteral("auto_break")] = m_autoBreakEnabled;
|
||||||
obj[QStringLiteral("linkify")] = m_linkifyEnabled;
|
obj[QStringLiteral("linkify")] = m_linkifyEnabled;
|
||||||
obj[QStringLiteral("indent_first_line")] = m_indentFirstLineEnabled;
|
obj[QStringLiteral("indent_first_line")] = m_indentFirstLineEnabled;
|
||||||
|
obj[QStringLiteral("smart_table")] = m_smartTableEnabled;
|
||||||
|
obj[QStringLiteral("smart_table_interval")] = m_smartTableInterval;
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -319,3 +325,19 @@ void MarkdownEditorConfig::setSectionNumberStyle(SectionNumberStyle p_style)
|
|||||||
{
|
{
|
||||||
updateConfig(m_sectionNumberStyle, p_style, this);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -95,6 +95,11 @@ namespace vnotex
|
|||||||
bool getIndentFirstLineEnabled() const;
|
bool getIndentFirstLineEnabled() const;
|
||||||
void setIndentFirstLineEnabled(bool p_enabled);
|
void setIndentFirstLineEnabled(bool p_enabled);
|
||||||
|
|
||||||
|
bool getSmartTableEnabled() const;
|
||||||
|
void setSmartTableEnabled(bool p_enabled);
|
||||||
|
|
||||||
|
int getSmartTableInterval() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QString sectionNumberModeToString(SectionNumberMode p_mode) const;
|
QString sectionNumberModeToString(SectionNumberMode p_mode) const;
|
||||||
SectionNumberMode stringToSectionNumberMode(const QString &p_str) const;
|
SectionNumberMode stringToSectionNumberMode(const QString &p_str) const;
|
||||||
@ -154,6 +159,11 @@ namespace vnotex
|
|||||||
|
|
||||||
// Whether indent the first line of a paragraph.
|
// Whether indent the first line of a paragraph.
|
||||||
bool m_indentFirstLineEnabled = false;
|
bool m_indentFirstLineEnabled = false;
|
||||||
|
|
||||||
|
bool m_smartTableEnabled = true;
|
||||||
|
|
||||||
|
// Interval time to do smart table format.
|
||||||
|
int m_smartTableInterval = 2000;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,6 +57,7 @@
|
|||||||
<file>icons/type_quote_editor.svg</file>
|
<file>icons/type_quote_editor.svg</file>
|
||||||
<file>icons/type_link_editor.svg</file>
|
<file>icons/type_link_editor.svg</file>
|
||||||
<file>icons/type_image_editor.svg</file>
|
<file>icons/type_image_editor.svg</file>
|
||||||
|
<file>icons/type_mark_editor.svg</file>
|
||||||
<file>icons/type_table_editor.svg</file>
|
<file>icons/type_table_editor.svg</file>
|
||||||
<file>icons/add.svg</file>
|
<file>icons/add.svg</file>
|
||||||
<file>icons/clear.svg</file>
|
<file>icons/clear.svg</file>
|
||||||
|
1
src/data/core/icons/type_mark_editor.svg
Normal file
1
src/data/core/icons/type_mark_editor.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1610071686158" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3107" width="512" height="512" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css"></style></defs><path d="M650.368 192.64l-287.488 287.445333-30.165333 90.496-44.373334 44.416 120.661334 120.704 44.373333-44.416 90.538667-30.165333 287.445333-287.488-180.992-180.992z m271.530667 150.826667a42.666667 42.666667 0 0 1 0 60.330666l-331.904 331.904-90.453334 30.165334-60.373333 60.330666a42.666667 42.666667 0 0 1-60.330667 0l-181.034666-181.034666a42.666667 42.666667 0 0 1 0-60.330667l60.330666-60.330667 30.165334-90.496 331.904-331.904a42.666667 42.666667 0 0 1 60.330666 0l241.365334 241.365334z m-271.530667-30.165334l60.330667 60.330667-211.2 211.2-60.330667-60.330667 211.2-211.2zM182.741333 720.64l120.661334 120.704-60.330667 60.330667-181.034667-60.330667 120.661334-120.704z" p-id="3108" fill="#000000"></path></svg>
|
After Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
@ -1644,11 +1644,11 @@ Do you want to delete it anyway?</source>
|
|||||||
<name>QGnomeTheme</name>
|
<name>QGnomeTheme</name>
|
||||||
<message>
|
<message>
|
||||||
<source>&OK</source>
|
<source>&OK</source>
|
||||||
<translation>確定(&O)</translation>
|
<translation>确定(&O)</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source>&Save</source>
|
<source>&Save</source>
|
||||||
<translation>儲存(&S)</translation>
|
<translation>保存(&S)</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source>&Cancel</source>
|
<source>&Cancel</source>
|
||||||
@ -1656,11 +1656,11 @@ Do you want to delete it anyway?</source>
|
|||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source>&Close</source>
|
<source>&Close</source>
|
||||||
<translation>關閉(&C)</translation>
|
<translation>关闭(&C)</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source>Close without Saving</source>
|
<source>Close without Saving</source>
|
||||||
<translation>關閉而不儲存</translation>
|
<translation>关闭而不保存</translation>
|
||||||
</message>
|
</message>
|
||||||
</context>
|
</context>
|
||||||
<context>
|
<context>
|
||||||
@ -2438,11 +2438,11 @@ Do you want to delete it anyway?</source>
|
|||||||
<name>QMessageBox</name>
|
<name>QMessageBox</name>
|
||||||
<message>
|
<message>
|
||||||
<source>Show Details...</source>
|
<source>Show Details...</source>
|
||||||
<translation>顯示詳情...</translation>
|
<translation>显示详情...</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source>Hide Details...</source>
|
<source>Hide Details...</source>
|
||||||
<translation>隱藏詳情...</translation>
|
<translation>隐藏详情...</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source><h3>About Qt</h3><p>This program uses Qt version %1.</p></source>
|
<source><h3>About Qt</h3><p>This program uses Qt version %1.</p></source>
|
||||||
@ -2455,7 +2455,7 @@ Do you want to delete it anyway?</source>
|
|||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source>About Qt</source>
|
<source>About Qt</source>
|
||||||
<translation>關於 Qt</translation>
|
<translation>关于 Qt</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source><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></source>
|
<source><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></source>
|
||||||
@ -3540,15 +3540,15 @@ Do you want to delete it anyway?</source>
|
|||||||
<name>QPlatformTheme</name>
|
<name>QPlatformTheme</name>
|
||||||
<message>
|
<message>
|
||||||
<source>OK</source>
|
<source>OK</source>
|
||||||
<translation>確定</translation>
|
<translation>确定</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source>Save</source>
|
<source>Save</source>
|
||||||
<translation>儲存</translation>
|
<translation>保存</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source>Save All</source>
|
<source>Save All</source>
|
||||||
<translation>全部儲存</translation>
|
<translation>全部保存</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source>Open</source>
|
<source>Open</source>
|
||||||
@ -3584,7 +3584,7 @@ Do you want to delete it anyway?</source>
|
|||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source>Close</source>
|
<source>Close</source>
|
||||||
<translation>關閉</translation>
|
<translation>关闭</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source>Cancel</source>
|
<source>Cancel</source>
|
||||||
@ -3592,7 +3592,7 @@ Do you want to delete it anyway?</source>
|
|||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source>Discard</source>
|
<source>Discard</source>
|
||||||
<translation>丟棄</translation>
|
<translation>丢弃</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source>Help</source>
|
<source>Help</source>
|
||||||
|
@ -56,6 +56,7 @@
|
|||||||
"TypeMath" : "Ctrl+,",
|
"TypeMath" : "Ctrl+,",
|
||||||
"TypeMathBlock" : "Ctrl+.",
|
"TypeMathBlock" : "Ctrl+.",
|
||||||
"TypeTable" : "Ctrl+/",
|
"TypeTable" : "Ctrl+/",
|
||||||
|
"TypeMark" : "Ctrl+G, M",
|
||||||
"Outline" : "Ctrl+G, O",
|
"Outline" : "Ctrl+G, O",
|
||||||
"RichPaste" : "Ctrl+Shift+V",
|
"RichPaste" : "Ctrl+Shift+V",
|
||||||
"FindAndReplace" : "Ctrl+F",
|
"FindAndReplace" : "Ctrl+F",
|
||||||
@ -250,7 +251,11 @@
|
|||||||
"//comment" : "Whether convert URL-like text to links",
|
"//comment" : "Whether convert URL-like text to links",
|
||||||
"linkify" : true,
|
"linkify" : true,
|
||||||
"//comment" : "Whether add indentation to the first line of paragraph",
|
"//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" : {
|
"widget" : {
|
||||||
|
@ -185,8 +185,8 @@
|
|||||||
"font-family" : "YaHei Consolas Hybrid, Consolas, Monaco, Andale Mono, Monospace, Courier New"
|
"font-family" : "YaHei Consolas Hybrid, Consolas, Monaco, Andale Mono, Monospace, Courier New"
|
||||||
},
|
},
|
||||||
"MARK" : {
|
"MARK" : {
|
||||||
"text-color" : "#ccd1d8",
|
"text-color" : "#d7dae0",
|
||||||
"background-color" : "#551560"
|
"background-color" : "#898900"
|
||||||
},
|
},
|
||||||
"TABLE" : {
|
"TABLE" : {
|
||||||
"font-family" : "YaHei Consolas Hybrid, Consolas, Monaco, Andale Mono, Monospace, Courier New"
|
"font-family" : "YaHei Consolas Hybrid, Consolas, Monaco, Andale Mono, Monospace, Courier New"
|
||||||
|
@ -72,6 +72,8 @@ void MarkdownEditorPage::loadInternal()
|
|||||||
m_linkifyCheckBox->setChecked(markdownConfig.getLinkifyEnabled());
|
m_linkifyCheckBox->setChecked(markdownConfig.getLinkifyEnabled());
|
||||||
|
|
||||||
m_indentFirstLineCheckBox->setChecked(markdownConfig.getIndentFirstLineEnabled());
|
m_indentFirstLineCheckBox->setChecked(markdownConfig.getIndentFirstLineEnabled());
|
||||||
|
|
||||||
|
m_smartTableCheckBox->setChecked(markdownConfig.getSmartTableEnabled());
|
||||||
}
|
}
|
||||||
|
|
||||||
void MarkdownEditorPage::saveInternal()
|
void MarkdownEditorPage::saveInternal()
|
||||||
@ -110,6 +112,8 @@ void MarkdownEditorPage::saveInternal()
|
|||||||
|
|
||||||
markdownConfig.setIndentFirstLineEnabled(m_indentFirstLineCheckBox->isChecked());
|
markdownConfig.setIndentFirstLineEnabled(m_indentFirstLineCheckBox->isChecked());
|
||||||
|
|
||||||
|
markdownConfig.setSmartTableEnabled(m_smartTableCheckBox->isChecked());
|
||||||
|
|
||||||
EditorPage::notifyEditorConfigChange();
|
EditorPage::notifyEditorConfigChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -225,6 +229,16 @@ QGroupBox *MarkdownEditorPage::setupEditGroup()
|
|||||||
this, &MarkdownEditorPage::pageIsChanged);
|
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;
|
return box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,6 +56,8 @@ namespace vnotex
|
|||||||
QSpinBox *m_sectionNumberBaseLevelSpinBox = nullptr;
|
QSpinBox *m_sectionNumberBaseLevelSpinBox = nullptr;
|
||||||
|
|
||||||
QComboBox *m_sectionNumberStyleComboBox = nullptr;
|
QComboBox *m_sectionNumberStyleComboBox = nullptr;
|
||||||
|
|
||||||
|
QCheckBox *m_smartTableCheckBox = nullptr;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
91
src/widgets/dialogs/tableinsertdialog.cpp
Normal file
91
src/widgets/dialogs/tableinsertdialog.cpp
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
#include "tableinsertdialog.h"
|
||||||
|
|
||||||
|
#include <QSpinBox>
|
||||||
|
#include <QRadioButton>
|
||||||
|
#include <QGridLayout>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QButtonGroup>
|
||||||
|
|
||||||
|
#include <widgets/widgetsfactory.h>
|
||||||
|
|
||||||
|
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<int>(Alignment::None));
|
||||||
|
buttonGroup->addButton(leftBtn, static_cast<int>(Alignment::Left));
|
||||||
|
buttonGroup->addButton(centerBtn, static_cast<int>(Alignment::Center));
|
||||||
|
buttonGroup->addButton(rightBtn, static_cast<int>(Alignment::Right));
|
||||||
|
|
||||||
|
noneBtn->setChecked(true);
|
||||||
|
connect(buttonGroup, static_cast<void(QButtonGroup::*)(int, bool)>(&QButtonGroup::buttonToggled),
|
||||||
|
this, [this](int p_id, bool p_checked){
|
||||||
|
if (p_checked) {
|
||||||
|
m_alignment = static_cast<Alignment>(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;
|
||||||
|
}
|
35
src/widgets/dialogs/tableinsertdialog.h
Normal file
35
src/widgets/dialogs/tableinsertdialog.h
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
#ifndef TABLEINSERTDIALOG_H
|
||||||
|
#define TABLEINSERTDIALOG_H
|
||||||
|
|
||||||
|
#include "scrolldialog.h"
|
||||||
|
|
||||||
|
#include <core/global.h>
|
||||||
|
|
||||||
|
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
|
@ -24,6 +24,7 @@
|
|||||||
|
|
||||||
#include <widgets/dialogs/linkinsertdialog.h>
|
#include <widgets/dialogs/linkinsertdialog.h>
|
||||||
#include <widgets/dialogs/imageinsertdialog.h>
|
#include <widgets/dialogs/imageinsertdialog.h>
|
||||||
|
#include <widgets/dialogs/tableinsertdialog.h>
|
||||||
#include <widgets/messageboxhelper.h>
|
#include <widgets/messageboxhelper.h>
|
||||||
|
|
||||||
#include <widgets/dialogs/selectdialog.h>
|
#include <widgets/dialogs/selectdialog.h>
|
||||||
@ -43,6 +44,7 @@
|
|||||||
|
|
||||||
#include "previewhelper.h"
|
#include "previewhelper.h"
|
||||||
#include "../outlineprovider.h"
|
#include "../outlineprovider.h"
|
||||||
|
#include "markdowntablehelper.h"
|
||||||
|
|
||||||
using namespace vnotex;
|
using namespace vnotex;
|
||||||
|
|
||||||
@ -78,6 +80,8 @@ MarkdownEditor::MarkdownEditor(const MarkdownEditorConfig &p_config,
|
|||||||
connect(getHighlighter(), &vte::PegMarkdownHighlighter::headersUpdated,
|
connect(getHighlighter(), &vte::PegMarkdownHighlighter::headersUpdated,
|
||||||
this, &MarkdownEditor::updateHeadings);
|
this, &MarkdownEditor::updateHeadings);
|
||||||
|
|
||||||
|
setupTableHelper();
|
||||||
|
|
||||||
m_headingTimer = new QTimer(this);
|
m_headingTimer = new QTimer(this);
|
||||||
m_headingTimer->setInterval(500);
|
m_headingTimer->setInterval(500);
|
||||||
m_headingTimer->setSingleShot(true);
|
m_headingTimer->setSingleShot(true);
|
||||||
@ -143,6 +147,12 @@ void MarkdownEditor::typeStrikethrough()
|
|||||||
vte::MarkdownUtils::typeStrikethrough(m_textEdit);
|
vte::MarkdownUtils::typeStrikethrough(m_textEdit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MarkdownEditor::typeMark()
|
||||||
|
{
|
||||||
|
enterInsertModeIfApplicable();
|
||||||
|
vte::MarkdownUtils::typeMark(m_textEdit);
|
||||||
|
}
|
||||||
|
|
||||||
void MarkdownEditor::typeUnorderedList()
|
void MarkdownEditor::typeUnorderedList()
|
||||||
{
|
{
|
||||||
enterInsertModeIfApplicable();
|
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)
|
void MarkdownEditor::setBuffer(Buffer *p_buffer)
|
||||||
{
|
{
|
||||||
m_buffer = p_buffer;
|
m_buffer = p_buffer;
|
||||||
@ -1214,3 +1259,10 @@ void MarkdownEditor::updateFromConfig(bool p_initialized)
|
|||||||
getHighlighter()->updateHighlight();
|
getHighlighter()->updateHighlight();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MarkdownEditor::setupTableHelper()
|
||||||
|
{
|
||||||
|
m_tableHelper = new MarkdownTableHelper(this, this);
|
||||||
|
connect(getHighlighter(), &vte::PegMarkdownHighlighter::tableBlocksUpdated,
|
||||||
|
m_tableHelper, &MarkdownTableHelper::updateTableBlocks);
|
||||||
|
}
|
||||||
|
@ -22,6 +22,7 @@ namespace vnotex
|
|||||||
class PreviewHelper;
|
class PreviewHelper;
|
||||||
class Buffer;
|
class Buffer;
|
||||||
class MarkdownEditorConfig;
|
class MarkdownEditorConfig;
|
||||||
|
class MarkdownTableHelper;
|
||||||
|
|
||||||
class MarkdownEditor : public vte::VMarkdownEditor
|
class MarkdownEditor : public vte::VMarkdownEditor
|
||||||
{
|
{
|
||||||
@ -65,6 +66,8 @@ namespace vnotex
|
|||||||
|
|
||||||
void typeStrikethrough();
|
void typeStrikethrough();
|
||||||
|
|
||||||
|
void typeMark();
|
||||||
|
|
||||||
void typeUnorderedList();
|
void typeUnorderedList();
|
||||||
|
|
||||||
void typeOrderedList();
|
void typeOrderedList();
|
||||||
@ -85,6 +88,8 @@ namespace vnotex
|
|||||||
|
|
||||||
void typeImage();
|
void typeImage();
|
||||||
|
|
||||||
|
void typeTable();
|
||||||
|
|
||||||
const QVector<MarkdownEditor::Heading> &getHeadings() const;
|
const QVector<MarkdownEditor::Heading> &getHeadings() const;
|
||||||
int getCurrentHeadingIndex() const;
|
int getCurrentHeadingIndex() const;
|
||||||
|
|
||||||
@ -171,6 +176,8 @@ namespace vnotex
|
|||||||
// Return true if there is change.
|
// Return true if there is change.
|
||||||
bool updateSectionNumber(const QVector<Heading> &p_headings);
|
bool updateSectionNumber(const QVector<Heading> &p_headings);
|
||||||
|
|
||||||
|
void setupTableHelper();
|
||||||
|
|
||||||
static QString generateImageFileNameToInsertAs(const QString &p_title, const QString &p_suffix);
|
static QString generateImageFileNameToInsertAs(const QString &p_title, const QString &p_suffix);
|
||||||
|
|
||||||
const MarkdownEditorConfig &m_config;
|
const MarkdownEditorConfig &m_config;
|
||||||
@ -190,6 +197,9 @@ namespace vnotex
|
|||||||
bool m_sectionNumberEnabled = false;
|
bool m_sectionNumberEnabled = false;
|
||||||
|
|
||||||
OverrideState m_overriddenSectionNumber = OverrideState::NoOverride;
|
OverrideState m_overriddenSectionNumber = OverrideState::NoOverride;
|
||||||
|
|
||||||
|
// Managed by QObject.
|
||||||
|
MarkdownTableHelper *m_tableHelper = nullptr;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
781
src/widgets/editors/markdowntable.cpp
Normal file
781
src/widgets/editors/markdowntable.cpp
Normal file
@ -0,0 +1,781 @@
|
|||||||
|
#include "markdowntable.h"
|
||||||
|
|
||||||
|
#include <QTextEdit>
|
||||||
|
#include <QDebug>
|
||||||
|
|
||||||
|
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<int> &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<int> &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<CellInfo> 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<int>((targetWidth + delta) / s_minusWidth);
|
||||||
|
core = QString(coreLength, '-');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case Alignment::Left:
|
||||||
|
{
|
||||||
|
int coreLength = static_cast<int>((targetWidth - s_colonWidth + delta) / s_minusWidth);
|
||||||
|
core = QStringLiteral(":");
|
||||||
|
core += QString(coreLength, '-');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case Alignment::Center:
|
||||||
|
{
|
||||||
|
int coreLength = static_cast<int>((targetWidth - 2 * s_colonWidth + delta) / s_minusWidth);
|
||||||
|
core = QStringLiteral(":");
|
||||||
|
core += QString(coreLength, '-');
|
||||||
|
core += QStringLiteral(":");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case Alignment::Right:
|
||||||
|
{
|
||||||
|
int coreLength = static_cast<int>((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<int>((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<CellInfo> &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;
|
||||||
|
}
|
165
src/widgets/editors/markdowntable.h
Normal file
165
src/widgets/editors/markdowntable.h
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
#ifndef MARKDOWNTABLE_H
|
||||||
|
#define MARKDOWNTABLE_H
|
||||||
|
|
||||||
|
#include <QTextBlock>
|
||||||
|
#include <QVector>
|
||||||
|
|
||||||
|
#include <vtextedit/pegmarkdownhighlighterdata.h>
|
||||||
|
|
||||||
|
#include <core/global.h>
|
||||||
|
|
||||||
|
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<Cell> 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<int> &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<CellInfo> &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<Row> 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
|
113
src/widgets/editors/markdowntablehelper.cpp
Normal file
113
src/widgets/editors/markdowntablehelper.cpp
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
#include "markdowntablehelper.h"
|
||||||
|
|
||||||
|
#include <QTimer>
|
||||||
|
#include <QTextEdit>
|
||||||
|
#include <QTextCursor>
|
||||||
|
|
||||||
|
#include <vtextedit/vtexteditor.h>
|
||||||
|
#include <vtextedit/vtextedit.h>
|
||||||
|
|
||||||
|
#include <core/configmgr.h>
|
||||||
|
#include <core/markdowneditorconfig.h>
|
||||||
|
#include <core/editorconfig.h>
|
||||||
|
|
||||||
|
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<vte::peg::TableBlock> &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<vte::peg::TableBlock> &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();
|
||||||
|
}
|
47
src/widgets/editors/markdowntablehelper.h
Normal file
47
src/widgets/editors/markdowntablehelper.h
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
#ifndef MARKDOWNTABLEHELPER_H
|
||||||
|
#define MARKDOWNTABLEHELPER_H
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
|
||||||
|
#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<vte::peg::TableBlock> &p_blocks);
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Return the block index which contains the cursor.
|
||||||
|
int currentCursorTableBlock(const QVector<vte::peg::TableBlock> &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
|
@ -274,6 +274,7 @@ void MarkdownViewWindow::setupToolBar()
|
|||||||
addAction(toolBar, ViewWindowToolBarHelper::TypeBold);
|
addAction(toolBar, ViewWindowToolBarHelper::TypeBold);
|
||||||
addAction(toolBar, ViewWindowToolBarHelper::TypeItalic);
|
addAction(toolBar, ViewWindowToolBarHelper::TypeItalic);
|
||||||
addAction(toolBar, ViewWindowToolBarHelper::TypeStrikethrough);
|
addAction(toolBar, ViewWindowToolBarHelper::TypeStrikethrough);
|
||||||
|
addAction(toolBar, ViewWindowToolBarHelper::TypeMark);
|
||||||
addAction(toolBar, ViewWindowToolBarHelper::TypeUnorderedList);
|
addAction(toolBar, ViewWindowToolBarHelper::TypeUnorderedList);
|
||||||
addAction(toolBar, ViewWindowToolBarHelper::TypeOrderedList);
|
addAction(toolBar, ViewWindowToolBarHelper::TypeOrderedList);
|
||||||
addAction(toolBar, ViewWindowToolBarHelper::TypeTodoList);
|
addAction(toolBar, ViewWindowToolBarHelper::TypeTodoList);
|
||||||
@ -630,6 +631,10 @@ void MarkdownViewWindow::handleTypeAction(TypeAction p_action)
|
|||||||
m_editor->typeStrikethrough();
|
m_editor->typeStrikethrough();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case TypeAction::Mark:
|
||||||
|
m_editor->typeMark();
|
||||||
|
break;
|
||||||
|
|
||||||
case TypeAction::UnorderedList:
|
case TypeAction::UnorderedList:
|
||||||
m_editor->typeUnorderedList();
|
m_editor->typeUnorderedList();
|
||||||
break;
|
break;
|
||||||
@ -674,6 +679,10 @@ void MarkdownViewWindow::handleTypeAction(TypeAction p_action)
|
|||||||
m_editor->typeImage();
|
m_editor->typeImage();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case TypeAction::Table:
|
||||||
|
m_editor->typeTable();
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
qWarning() << "TypeAction not handled" << p_action;
|
qWarning() << "TypeAction not handled" << p_action;
|
||||||
break;
|
break;
|
||||||
|
@ -360,6 +360,8 @@ QAction *ViewWindow::addAction(QToolBar *p_toolBar, ViewWindowToolBarHelper::Act
|
|||||||
case ViewWindowToolBarHelper::TypeImage:
|
case ViewWindowToolBarHelper::TypeImage:
|
||||||
Q_FALLTHROUGH();
|
Q_FALLTHROUGH();
|
||||||
case ViewWindowToolBarHelper::TypeTable:
|
case ViewWindowToolBarHelper::TypeTable:
|
||||||
|
Q_FALLTHROUGH();
|
||||||
|
case ViewWindowToolBarHelper::TypeMark:
|
||||||
{
|
{
|
||||||
act = ViewWindowToolBarHelper::addAction(p_toolBar, p_action);
|
act = ViewWindowToolBarHelper::addAction(p_toolBar, p_action);
|
||||||
connect(this, &ViewWindow::modeChanged,
|
connect(this, &ViewWindow::modeChanged,
|
||||||
@ -616,7 +618,7 @@ void ViewWindow::handleSectionNumberOverride(OverrideState p_state)
|
|||||||
ViewWindow::TypeAction ViewWindow::toolBarActionToTypeAction(ViewWindowToolBarHelper::Action p_action)
|
ViewWindow::TypeAction ViewWindow::toolBarActionToTypeAction(ViewWindowToolBarHelper::Action p_action)
|
||||||
{
|
{
|
||||||
Q_ASSERT(p_action >= ViewWindowToolBarHelper::Action::TypeBold
|
Q_ASSERT(p_action >= ViewWindowToolBarHelper::Action::TypeBold
|
||||||
&& p_action <= ViewWindowToolBarHelper::Action::TypeTable);
|
&& p_action <= ViewWindowToolBarHelper::Action::TypeMax);
|
||||||
return static_cast<TypeAction>(TypeAction::Bold + (p_action - ViewWindowToolBarHelper::Action::TypeBold));
|
return static_cast<TypeAction>(TypeAction::Bold + (p_action - ViewWindowToolBarHelper::Action::TypeBold));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -128,7 +128,8 @@ namespace vnotex
|
|||||||
Quote,
|
Quote,
|
||||||
Link,
|
Link,
|
||||||
Image,
|
Image,
|
||||||
TypeTable
|
Table,
|
||||||
|
Mark
|
||||||
};
|
};
|
||||||
|
|
||||||
protected slots:
|
protected slots:
|
||||||
|
@ -268,6 +268,12 @@ QAction *ViewWindowToolBarHelper::addAction(QToolBar *p_tb, Action p_action)
|
|||||||
addActionShortcut(act, editorConfig.getShortcut(Shortcut::TypeTable), viewWindow);
|
addActionShortcut(act, editorConfig.getShortcut(Shortcut::TypeTable), viewWindow);
|
||||||
break;
|
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:
|
case Action::Attachment:
|
||||||
{
|
{
|
||||||
act = p_tb->addAction(ToolBarHelper::generateIcon("attachment_editor.svg"),
|
act = p_tb->addAction(ToolBarHelper::generateIcon("attachment_editor.svg"),
|
||||||
|
@ -36,6 +36,9 @@ namespace vnotex
|
|||||||
TypeLink,
|
TypeLink,
|
||||||
TypeImage,
|
TypeImage,
|
||||||
TypeTable,
|
TypeTable,
|
||||||
|
TypeMark,
|
||||||
|
// Ending TypeXXX.
|
||||||
|
TypeMax,
|
||||||
|
|
||||||
Attachment,
|
Attachment,
|
||||||
Outline,
|
Outline,
|
||||||
|
@ -22,9 +22,12 @@ SOURCES += \
|
|||||||
$$PWD/dialogs/settings/settingsdialog.cpp \
|
$$PWD/dialogs/settings/settingsdialog.cpp \
|
||||||
$$PWD/dialogs/settings/texteditorpage.cpp \
|
$$PWD/dialogs/settings/texteditorpage.cpp \
|
||||||
$$PWD/dialogs/settings/themepage.cpp \
|
$$PWD/dialogs/settings/themepage.cpp \
|
||||||
|
$$PWD/dialogs/tableinsertdialog.cpp \
|
||||||
$$PWD/dragdropareaindicator.cpp \
|
$$PWD/dragdropareaindicator.cpp \
|
||||||
$$PWD/editors/editormarkdownvieweradapter.cpp \
|
$$PWD/editors/editormarkdownvieweradapter.cpp \
|
||||||
$$PWD/editors/markdowneditor.cpp \
|
$$PWD/editors/markdowneditor.cpp \
|
||||||
|
$$PWD/editors/markdowntable.cpp \
|
||||||
|
$$PWD/editors/markdowntablehelper.cpp \
|
||||||
$$PWD/editors/markdownviewer.cpp \
|
$$PWD/editors/markdownviewer.cpp \
|
||||||
$$PWD/editors/markdownvieweradapter.cpp \
|
$$PWD/editors/markdownvieweradapter.cpp \
|
||||||
$$PWD/editors/previewhelper.cpp \
|
$$PWD/editors/previewhelper.cpp \
|
||||||
@ -104,9 +107,12 @@ HEADERS += \
|
|||||||
$$PWD/dialogs/settings/settingsdialog.h \
|
$$PWD/dialogs/settings/settingsdialog.h \
|
||||||
$$PWD/dialogs/settings/texteditorpage.h \
|
$$PWD/dialogs/settings/texteditorpage.h \
|
||||||
$$PWD/dialogs/settings/themepage.h \
|
$$PWD/dialogs/settings/themepage.h \
|
||||||
|
$$PWD/dialogs/tableinsertdialog.h \
|
||||||
$$PWD/dragdropareaindicator.h \
|
$$PWD/dragdropareaindicator.h \
|
||||||
$$PWD/editors/editormarkdownvieweradapter.h \
|
$$PWD/editors/editormarkdownvieweradapter.h \
|
||||||
$$PWD/editors/markdowneditor.h \
|
$$PWD/editors/markdowneditor.h \
|
||||||
|
$$PWD/editors/markdowntable.h \
|
||||||
|
$$PWD/editors/markdowntablehelper.h \
|
||||||
$$PWD/editors/markdownviewer.h \
|
$$PWD/editors/markdownviewer.h \
|
||||||
$$PWD/editors/markdownvieweradapter.h \
|
$$PWD/editors/markdownvieweradapter.h \
|
||||||
$$PWD/editors/previewhelper.h \
|
$$PWD/editors/previewhelper.h \
|
||||||
|
Loading…
x
Reference in New Issue
Block a user