diff --git a/src/dialog/vinserttabledialog.cpp b/src/dialog/vinserttabledialog.cpp new file mode 100644 index 00000000..0d20612c --- /dev/null +++ b/src/dialog/vinserttabledialog.cpp @@ -0,0 +1,95 @@ +#include "vinserttabledialog.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +VInsertTableDialog::VInsertTableDialog(QWidget *p_parent) + : QDialog(p_parent), + m_alignment(VTable::None) +{ + setupUI(); +} + +void VInsertTableDialog::setupUI() +{ + m_rowCount = new QSpinBox(this); + m_rowCount->setToolTip(tr("Number of rows of the table body")); + m_rowCount->setMaximum(1000); + m_rowCount->setMinimum(0); + + m_colCount = new QSpinBox(this); + m_colCount->setToolTip(tr("Number of columns of the table")); + m_colCount->setMaximum(1000); + m_colCount->setMinimum(1); + + QRadioButton *noneBtn = new QRadioButton(tr("None"), this); + QRadioButton *leftBtn = new QRadioButton(tr("Left"), this); + QRadioButton *centerBtn = new QRadioButton(tr("Center"), this); + QRadioButton *rightBtn = new QRadioButton(tr("Right"), this); + QHBoxLayout *alignLayout = new QHBoxLayout(); + alignLayout->addWidget(noneBtn); + alignLayout->addWidget(leftBtn); + alignLayout->addWidget(centerBtn); + alignLayout->addWidget(rightBtn); + alignLayout->addStretch(); + + noneBtn->setChecked(true); + + QButtonGroup *bg = new QButtonGroup(this); + bg->addButton(noneBtn, VTable::None); + bg->addButton(leftBtn, VTable::Left); + bg->addButton(centerBtn, VTable::Center); + bg->addButton(rightBtn, VTable::Right); + connect(bg, static_cast(&QButtonGroup::buttonToggled), + this, [this](int p_id, bool p_checked){ + if (p_checked) { + m_alignment = static_cast(p_id); + } + }); + + QGridLayout *topLayout = new QGridLayout(); + topLayout->addWidget(new QLabel(tr("Row:")), 0, 0, 1, 1); + topLayout->addWidget(m_rowCount, 0, 1, 1, 1); + topLayout->addWidget(new QLabel(tr("Column:")), 0, 2, 1, 1); + topLayout->addWidget(m_colCount, 0, 3, 1, 1); + + topLayout->addWidget(new QLabel(tr("Alignment:")), 1, 0, 1, 1); + topLayout->addLayout(alignLayout, 1, 1, 1, 3); + + // Ok is the default button. + m_btnBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + connect(m_btnBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(m_btnBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + + QPushButton *okBtn = m_btnBox->button(QDialogButtonBox::Ok); + okBtn->setProperty("SpecialBtn", true); + + QVBoxLayout *mainLayout = new QVBoxLayout(); + mainLayout->addLayout(topLayout); + mainLayout->addWidget(m_btnBox); + + setLayout(mainLayout); + setWindowTitle(tr("Insert Table")); +} + +int VInsertTableDialog::getRowCount() const +{ + return m_rowCount->value(); +} + +int VInsertTableDialog::getColumnCount() const +{ + return m_colCount->value(); +} + +VTable::Alignment VInsertTableDialog::getAlignment() const +{ + return m_alignment; +} diff --git a/src/dialog/vinserttabledialog.h b/src/dialog/vinserttabledialog.h new file mode 100644 index 00000000..6b4fa9d0 --- /dev/null +++ b/src/dialog/vinserttabledialog.h @@ -0,0 +1,32 @@ +#ifndef VINSERTTABLEDIALOG_H +#define VINSERTTABLEDIALOG_H + +#include + +#include "../vtable.h" + +class QDialogButtonBox; +class QSpinBox; + +class VInsertTableDialog : public QDialog +{ + Q_OBJECT +public: + explicit VInsertTableDialog(QWidget *p_parent = nullptr); + + int getRowCount() const; + int getColumnCount() const; + VTable::Alignment getAlignment() const; + +private: + void setupUI(); + + QSpinBox *m_rowCount; + QSpinBox *m_colCount; + + QDialogButtonBox *m_btnBox; + + VTable::Alignment m_alignment; +}; + +#endif // VINSERTTABLEDIALOG_H diff --git a/src/resources/docs/shortcuts_en.md b/src/resources/docs/shortcuts_en.md index 682bb619..647ab54a 100644 --- a/src/resources/docs/shortcuts_en.md +++ b/src/resources/docs/shortcuts_en.md @@ -96,6 +96,8 @@ Insert inline code. Press `Ctrl+;` again to exit. Current selected text will be Insert fenced code block. Press `Ctrl+M` again to exit. Current selected text will be wrapped into a code block if exists. - `Ctrl+L` Insert link. +- `Ctrl+.` +Insert table. - `Ctrl+'` Insert image. - `Ctrl+H` diff --git a/src/resources/docs/shortcuts_zh.md b/src/resources/docs/shortcuts_zh.md index c0d05a75..ecf006e6 100644 --- a/src/resources/docs/shortcuts_zh.md +++ b/src/resources/docs/shortcuts_zh.md @@ -96,6 +96,8 @@ 插入代码块;再次按`Ctrl+M`退出。如果已经选择文本,则将当前选择文本嵌入到代码块中。 - `Ctrl+L` 插入链接。 +- `Ctrl+.` +插入表格。 - `Ctrl+'` 插入图片。 - `Ctrl+H` diff --git a/src/resources/icons/table.svg b/src/resources/icons/table.svg new file mode 100644 index 00000000..d2a38552 --- /dev/null +++ b/src/resources/icons/table.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/src/src.pro b/src/src.pro index 4a03034a..14638b32 100644 --- a/src/src.pro +++ b/src/src.pro @@ -159,7 +159,8 @@ SOURCES += main.cpp\ vfilelistwidget.cpp \ widgets/vcombobox.cpp \ vtablehelper.cpp \ - vtable.cpp + vtable.cpp \ + dialog/vinserttabledialog.cpp HEADERS += vmainwindow.h \ vdirectorytree.h \ @@ -304,7 +305,8 @@ HEADERS += vmainwindow.h \ vfilelistwidget.h \ widgets/vcombobox.h \ vtablehelper.h \ - vtable.h + vtable.h \ + dialog/vinserttabledialog.h RESOURCES += \ vnote.qrc \ diff --git a/src/vdirectorytree.cpp b/src/vdirectorytree.cpp index 0071880a..8abfad49 100644 --- a/src/vdirectorytree.cpp +++ b/src/vdirectorytree.cpp @@ -419,7 +419,7 @@ void VDirectoryTree::contextMenuRequested(QPoint pos) menu.addSeparator(); - QAction *reloadAct = new QAction(tr("&Reload From Disk"), &menu); + QAction *reloadAct = new QAction(tr("Reload From Disk"), &menu); reloadAct->setToolTip(tr("Reload the content of this folder (or notebook) from disk")); connect(reloadAct, &QAction::triggered, this, &VDirectoryTree::reloadFromDisk); diff --git a/src/veditor.h b/src/veditor.h index 0c2d5268..ebcd7bb1 100644 --- a/src/veditor.h +++ b/src/veditor.h @@ -72,6 +72,9 @@ public: // User requests to insert a link. void insertLink(); + // User requests to insert a table. + virtual void insertTable() = 0; + // Used for incremental search. // User has enter the content to search, but does not enter the "find" button yet. bool peekText(const QString &p_text, uint p_options, bool p_forward = true); diff --git a/src/vedittab.cpp b/src/vedittab.cpp index 1e033074..e9f572ea 100644 --- a/src/vedittab.cpp +++ b/src/vedittab.cpp @@ -136,6 +136,10 @@ void VEditTab::insertLink() { } +void VEditTab::insertTable() +{ +} + void VEditTab::applySnippet(const VSnippet *p_snippet) { Q_UNUSED(p_snippet); diff --git a/src/vedittab.h b/src/vedittab.h index d266de42..ca045d1a 100644 --- a/src/vedittab.h +++ b/src/vedittab.h @@ -56,6 +56,9 @@ public: // User requests to insert link. virtual void insertLink(); + // User requests to table. + virtual void insertTable(); + // Search @p_text in current note. virtual void findText(const QString &p_text, uint p_options, bool p_peek, bool p_forward = true) = 0; diff --git a/src/vmainwindow.cpp b/src/vmainwindow.cpp index d6e2c18e..1d195564 100644 --- a/src/vmainwindow.cpp +++ b/src/vmainwindow.cpp @@ -573,6 +573,19 @@ QToolBar *VMainWindow::initEditToolBar(QSize p_iconSize) m_editToolBar->addAction(codeBlockAct); + QAction *tableAct = new QAction(VIconUtils::toolButtonIcon(":/resources/icons/table.svg"), + tr("Table\t%1").arg(VUtils::getShortcutText("Ctrl+.")), + this); + tableAct->setStatusTip(tr("Insert a table")); + connect(tableAct, &QAction::triggered, + this, [this](){ + if (m_curTab) { + m_curTab->insertTable(); + } + }); + + m_editToolBar->addAction(tableAct); + // Insert link. QAction *insetLinkAct = new QAction(VIconUtils::toolButtonIcon(":/resources/icons/link.svg"), tr("Insert Link\t%1").arg(VUtils::getShortcutText("Ctrl+L")), diff --git a/src/vmdeditoperations.cpp b/src/vmdeditoperations.cpp index a2dccf0a..d8e36eb0 100644 --- a/src/vmdeditoperations.cpp +++ b/src/vmdeditoperations.cpp @@ -543,6 +543,17 @@ bool VMdEditOperations::handleKeyPressEvent(QKeyEvent *p_event) break; } + case Qt::Key_Period: + { + if (modifiers == Qt::ControlModifier) { + m_editor->insertTable(); + p_event->accept(); + ret = true; + } + + break; + } + default: break; } diff --git a/src/vmdeditor.cpp b/src/vmdeditor.cpp index b670ad69..dc554ae3 100644 --- a/src/vmdeditor.cpp +++ b/src/vmdeditor.cpp @@ -33,6 +33,7 @@ #include "vmdtab.h" #include "vdownloader.h" #include "vtablehelper.h" +#include "dialog/vinserttabledialog.h" extern VWebUtils *g_webUtils; @@ -2211,3 +2212,40 @@ void VMdEditor::handleLinkToAttachmentAction(QAction *p_act) m_editOps->insertLink(linkText, linkUrl); } } + +void VMdEditor::insertTable() +{ + // Get the dialog info. + VInsertTableDialog td(this); + if (td.exec() != QDialog::Accepted) { + return; + } + + int rowCount = td.getRowCount(); + int colCount = td.getColumnCount(); + VTable::Alignment alignment = td.getAlignment(); + + QTextCursor cursor = textCursorW(); + if (cursor.hasSelection()) { + cursor.clearSelection(); + setTextCursorW(cursor); + } + + bool newBlock = !cursor.atBlockEnd(); + if (!newBlock && !cursor.atBlockStart()) { + QString text = cursor.block().text().trimmed(); + if (!text.isEmpty() && text != ">") { + // Insert a new block before inserting table. + newBlock = true; + } + } + + if (newBlock) { + VEditUtils::insertBlock(cursor, false); + VEditUtils::indentBlockAsBlock(cursor, false); + setTextCursorW(cursor); + } + + // Insert table right at cursor. + m_tableHelper->insertTable(rowCount, colCount, alignment); +} diff --git a/src/vmdeditor.h b/src/vmdeditor.h index 9c3cb07d..15f9c43a 100644 --- a/src/vmdeditor.h +++ b/src/vmdeditor.h @@ -84,6 +84,8 @@ public: void updateFontAndPalette() Q_DECL_OVERRIDE; + void insertTable() Q_DECL_OVERRIDE; + public slots: bool jumpTitle(bool p_forward, int p_relativeLevel, int p_repeat) Q_DECL_OVERRIDE; diff --git a/src/vmdtab.cpp b/src/vmdtab.cpp index c7e7f657..e75bf7fb 100644 --- a/src/vmdtab.cpp +++ b/src/vmdtab.cpp @@ -696,6 +696,16 @@ void VMdTab::insertLink() m_editor->insertLink(); } +void VMdTab::insertTable() +{ + if (!m_isEditMode) { + return; + } + + Q_ASSERT(m_editor); + m_editor->insertTable(); +} + void VMdTab::findText(const QString &p_text, uint p_options, bool p_peek, bool p_forward) { diff --git a/src/vmdtab.h b/src/vmdtab.h index 5549839b..7de0d442 100644 --- a/src/vmdtab.h +++ b/src/vmdtab.h @@ -44,6 +44,8 @@ public: void insertImage() Q_DECL_OVERRIDE; + void insertTable() Q_DECL_OVERRIDE; + void insertLink() Q_DECL_OVERRIDE; // Search @p_text in current note. diff --git a/src/vnote.qrc b/src/vnote.qrc index d945c203..9b2efaa9 100644 --- a/src/vnote.qrc +++ b/src/vnote.qrc @@ -277,5 +277,6 @@ resources/docs/welcome_zh.md resources/icons/256x256/vnote.png utils/markdown-it/markdown-it-container.min.js + resources/icons/table.svg diff --git a/src/vtable.cpp b/src/vtable.cpp index 28d09b62..d440353e 100644 --- a/src/vtable.cpp +++ b/src/vtable.cpp @@ -7,14 +7,73 @@ const QString VTable::c_defaultDelimiter = "---"; +const QChar VTable::c_borderChar = '|'; + enum { HeaderRowIndex = 0, DelimiterRowIndex = 1 }; VTable::VTable(VEditor *p_editor, const VTableBlock &p_block) - : m_editor(p_editor) + : m_editor(p_editor), + m_exist(true), + m_spaceWidth(10), + m_minusWidth(10), + m_colonWidth(10), + m_defaultDelimiterWidth(10) { parseFromTableBlock(p_block); } +VTable::VTable(VEditor *p_editor, int p_nrBodyRow, int p_nrCol, VTable::Alignment p_alignment) + : m_editor(p_editor), + m_exist(false), + m_spaceWidth(10), + m_minusWidth(10), + m_colonWidth(10), + m_defaultDelimiterWidth(10) +{ + Q_ASSERT(p_nrBodyRow >= 0 && p_nrCol > 0); + m_rows.resize(p_nrBodyRow + 2); + + // PreText for each row. + QString preText; + QTextCursor cursor = m_editor->textCursorW(); + Q_ASSERT(cursor.atBlockEnd()); + if (!cursor.atBlockStart()) { + preText = cursor.block().text(); + } + + QString core(c_defaultDelimiter); + switch (p_alignment) { + case Alignment::Left: + core[0] = ':'; + break; + + case Alignment::Center: + core[0] = ':'; + core[core.size() - 1] = ':'; + break; + + case Alignment::Right: + core[core.size() - 1] = ':'; + break; + + default: + break; + } + const QString delimiterCell = generateFormattedText(core, 0); + const QString contentCell = generateFormattedText(QString(c_defaultDelimiter.size(), ' '), 0); + + for (int rowIdx = 0; rowIdx < m_rows.size(); ++rowIdx) { + auto & row = m_rows[rowIdx]; + row.m_preText = preText; + row.m_cells.resize(p_nrCol); + + const QString &content = isDelimiterRow(rowIdx) ? delimiterCell : contentCell; + for (auto & cell : row.m_cells) { + cell.m_text = content; + } + } +} + bool VTable::isValid() const { return header() && header()->isValid() @@ -82,6 +141,13 @@ bool VTable::parseOneRow(const QTextBlock &p_block, 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) { @@ -89,7 +155,7 @@ bool VTable::parseOneRow(const QTextBlock &p_block, } int offset = border - startPos; - if (text[offset] != '|') { + if (text[offset] != c_borderChar) { return false; } @@ -101,7 +167,7 @@ bool VTable::parseOneRow(const QTextBlock &p_block, } int nextOffset = p_borders[nextIdx] - startPos; - if (text[nextOffset] != '|') { + if (text[nextOffset] != c_borderChar) { return false; } @@ -186,7 +252,7 @@ void VTable::formatOneColumn(int p_idx, int p_cursorRowIdx, int p_cursorPib) fetchCellInfoOfColumn(p_idx, cells, targetWidth); // Get the alignment of this column. - const VTable::Alignment align = getColumnAlignment(p_idx); + const Alignment align = getColumnAlignment(p_idx); // Calculate the formatted text of each cell. for (int rowIdx = 0; rowIdx < cells.size(); ++rowIdx) { @@ -246,10 +312,7 @@ void VTable::formatOneColumn(int p_idx, int p_cursorRowIdx, int p_cursorPib) break; } - Alignment fakeAlign = align == Alignment::None ? Alignment::Left : align; - cell.m_formattedText = generateFormattedText(core, - 0, - fakeAlign); + cell.m_formattedText = generateFormattedText(core, 0); } } else { Alignment fakeAlign = align; @@ -385,9 +448,10 @@ QString VTable::generateFormattedText(const QString &p_core, rightSpaces = 0; } - return QString("| %1%2%3 ").arg(QString(leftSpaces, ' ')) - .arg(p_core) - .arg(QString(rightSpaces, ' ')); + return QString("%1 %2%3%4 ").arg(c_borderChar) + .arg(QString(leftSpaces, ' ')) + .arg(p_core) + .arg(QString(rightSpaces, ' ')); } VTable::Alignment VTable::getColumnAlignment(int p_idx) const @@ -450,7 +514,7 @@ bool VTable::isCellWellFormatted(const Row &p_row, const Cell &p_cell, const CellInfo &p_info, int p_targetWidth, - VTable::Alignment p_align) const + Alignment p_align) const { Q_ASSERT(p_align != Alignment::None); @@ -507,6 +571,15 @@ bool VTable::isCellWellFormatted(const Row &p_row, } void VTable::write() +{ + if (m_exist) { + writeExist(); + } else { + writeNonExist(); + } +} + +void VTable::writeExist() { bool changed = false; QTextCursor cursor = m_editor->textCursorW(); @@ -535,14 +608,7 @@ void VTable::write() } // Construct the block text. - QString newBlockText; - int firstOffset = row.m_cells.first().m_offset; - if (firstOffset > 0) { - // Get the prefix text. - QString text = row.m_block.text(); - newBlockText = text.left(firstOffset); - } - + QString newBlockText(row.m_preText); for (auto & cell : row.m_cells) { int pos = newBlockText.size(); if (cell.m_formattedText.isEmpty()) { @@ -560,7 +626,7 @@ void VTable::write() } } - newBlockText += "|"; + newBlockText += c_borderChar; // Replace the whole block. cursor.setPosition(row.m_block.position()); @@ -583,3 +649,30 @@ void VTable::write() } } } + +void VTable::writeNonExist() +{ + // 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 (auto & cell : row.m_cells) { + tableText += cell.m_text; + } + + tableText += c_borderChar; + + if (rowIdx < m_rows.size() - 1) { + tableText += '\n'; + } + } + + QTextCursor cursor = m_editor->textCursorW(); + int pos = cursor.position() + 2; + cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::MoveAnchor); + cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor); + cursor.insertText(tableText); + cursor.setPosition(pos); + m_editor->setTextCursorW(cursor); +} diff --git a/src/vtable.h b/src/vtable.h index cd52ac31..60234c4e 100644 --- a/src/vtable.h +++ b/src/vtable.h @@ -10,6 +10,25 @@ class VEditor; class VTable { public: + enum Alignment { + None, + Left, + Center, + Right + }; + + VTable(VEditor *p_editor, const VTableBlock &p_block); + + VTable(VEditor *p_editor, int p_nrBodyRow, int p_nrCol, VTable::Alignment p_alignment); + + bool isValid() const; + + void format(); + + // Write a formatted table. + void write(); + +private: struct Cell { Cell() @@ -53,12 +72,13 @@ public: bool isValid() const { - return m_block.isValid(); + return !m_cells.isEmpty(); } void clear() { m_block = QTextBlock(); + m_preText.clear(); m_cells.clear(); } @@ -75,31 +95,11 @@ public: } QTextBlock m_block; + // Text before table row. + QString m_preText; QVector m_cells; }; - enum Alignment - { - None, - Left, - Center, - Right - }; - - VTable(VEditor *p_editor, const VTableBlock &p_block); - - bool isValid() const; - - void format(); - - // Write a formatted table. - void write(); - - VTable::Row *header() const; - - VTable::Row *delimiter() const; - -private: // Used to hold info about a cell when formatting a column. struct CellInfo { @@ -163,10 +163,21 @@ private: const Cell &p_cell, const CellInfo &p_info, int p_targetWidth, - VTable::Alignment p_align) const; + Alignment p_align) const; + + void writeExist(); + + void writeNonExist(); + + VTable::Row *header() const; + + VTable::Row *delimiter() const; VEditor *m_editor; + // Whether this table exist already. + bool m_exist; + // Header, delimiter, and body. QVector m_rows; @@ -176,6 +187,8 @@ private: int m_defaultDelimiterWidth; static const QString c_defaultDelimiter; + + static const QChar c_borderChar; }; #endif // VTABLE_H diff --git a/src/vtablehelper.cpp b/src/vtablehelper.cpp index 75e8ce74..0e5a7505 100644 --- a/src/vtablehelper.cpp +++ b/src/vtablehelper.cpp @@ -1,7 +1,6 @@ #include "vtablehelper.h" #include "veditor.h" -#include "vtable.h" VTableHelper::VTableHelper(VEditor *p_editor, QObject *p_parent) : QObject(p_parent), @@ -52,3 +51,13 @@ int VTableHelper::currentCursorTableBlock(const QVector &p_blocks) return -1; } + +void VTableHelper::insertTable(int p_nrRow, int p_nrCol, VTable::Alignment p_alignment) +{ + VTable table(m_editor, p_nrRow, p_nrCol, p_alignment); + if (!table.isValid()) { + return; + } + + table.write(); +} diff --git a/src/vtablehelper.h b/src/vtablehelper.h index b034fd30..ddc41484 100644 --- a/src/vtablehelper.h +++ b/src/vtablehelper.h @@ -4,6 +4,7 @@ #include #include "markdownhighlighterdata.h" +#include "vtable.h" class VEditor; @@ -13,6 +14,9 @@ class VTableHelper : public QObject public: explicit VTableHelper(VEditor *p_editor, QObject *p_parent = nullptr); + // Insert table right at current cursor. + void insertTable(int p_nrRow, int p_nrCol, VTable::Alignment p_alignment); + public slots: void updateTableBlocks(const QVector &p_blocks);