diff --git a/CHANGELOG.md b/CHANGELOG.md index 496abda..55687c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## 1.2.91 - 2020-09-28 + +### Added +- You can now search inside your notes with the new search bar (Pavol Oresky) +- Move selected lines up and down (Aurelien Gateau) +- macOS dmg (Aurelien Gateau) +- Windows installer (Aurelien Gateau) + +## Changed +- Reorganized context menu: added "Edit" and "View" submenus (Aurelien Gateau) + ## 1.2.0 - 2019-05-11 ### Added @@ -26,16 +37,16 @@ ## 1.0.1 - 2019-01-12 -### Fixed -- Fixed indentation and make it respect indentation columns. -- Made it possible to indent/unindent selected lines with Tab/Shift+Tab. -- Update welcome text to reflect current shortcuts. - ### Added - Added unit-tests. - Added Travis integration. - Added rpm and deb packages generated using CPack. +### Fixed +- Fixed indentation and make it respect indentation columns. +- Made it possible to indent/unindent selected lines with Tab/Shift+Tab. +- Update welcome text to reflect current shortcuts. + ## 1.0.0 - 2018-12-30 First release diff --git a/CMakeLists.txt b/CMakeLists.txt index 65f032e..327ffaf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.17) project(nanonote - VERSION 1.2.0 + VERSION 1.2.91 DESCRIPTION "Minimalist note taking application for short-lived notes" HOMEPAGE_URL "https://github.com/agateau/nanonote" ) diff --git a/docs/release-check-list.md b/docs/release-check-list.md index 9c62644..b0b876d 100644 --- a/docs/release-check-list.md +++ b/docs/release-check-list.md @@ -1,23 +1,57 @@ +# Pre-release + Check working tree is up to date and clean: - git checkout dev - git pull - git merge origin/master - git status + rlt-updateworkbranch + +Update .ts files: + + ninja -C $BDIR lupdate + git add src/translations + git commit -m "Update translations" + +Update version: + + rlt-version --set-next $version Update CHANGELOG.md: - r!git log --pretty=format:'- \%s (\%an)' x.y.z-1..HEAD + r!rlt-changelog -Bump version number in CMakeLists.txt +Tag pre-release + + rlt-tag Commit and push -Build packages: + git add . + git commit + git push + git push --tags + +Smoke test binary packages generated by CI + +Ask translators to update their translations - ci/docker-build-app +# Release -Smoke test binary packages +Check working tree is up to date and clean: + + rlt-updateworkbranch + +Update CHANGELOG.md: + + r!rlt-changelog + +Bump version number in CMakeLists.txt + +Commit and push + + git add . + git commit + git push + +Smoke test binary packages generated by CI - Test welcome text is OK - Test screenshot matches @@ -29,15 +63,17 @@ Merge dev in master: git pull git merge --no-ff origin/dev -Create "x.y.z" tag: +Create tag: - git tag -a x.y.z + rlt-tag Push: git push git push --tags -Publish packages on GitHub +Publish generated packages on GitHub + +# Spread Write blog post diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 9184c1e..e90700f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -21,7 +21,7 @@ set(APPLIB_SRCS TextEdit.cpp WheelZoomExtension.cpp SearchWidget.cpp - SearchForm.ui + SearchWidget.ui ) qt5_add_resources(APPLIB_RESOURCES_SRCS app.qrc) diff --git a/src/IndentExtension.cpp b/src/IndentExtension.cpp index 8149576..f4d72be 100644 --- a/src/IndentExtension.cpp +++ b/src/IndentExtension.cpp @@ -63,7 +63,7 @@ IndentExtension::IndentExtension(TextEdit* textEdit) mTextEdit->addAction(mUnindentAction); } -void IndentExtension::aboutToShowContextMenu(QMenu* menu, const QPoint& /*pos*/) { +void IndentExtension::aboutToShowEditContextMenu(QMenu* menu, const QPoint& /*pos*/) { menu->addAction(mIndentAction); menu->addAction(mUnindentAction); menu->addSeparator(); diff --git a/src/IndentExtension.h b/src/IndentExtension.h index 81425ff..f61777e 100644 --- a/src/IndentExtension.h +++ b/src/IndentExtension.h @@ -10,7 +10,7 @@ class IndentExtension : public TextEditExtension { public: explicit IndentExtension(TextEdit* textEdit); - void aboutToShowContextMenu(QMenu* menu, const QPoint& pos) override; + void aboutToShowEditContextMenu(QMenu* menu, const QPoint& pos) override; bool keyPress(QKeyEvent* event) override; private: diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 9df76ca..b8a7cc0 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -40,12 +40,16 @@ MainWindowExtension::MainWindowExtension(MainWindow* window) void MainWindowExtension::aboutToShowContextMenu(QMenu* menu, const QPoint&) { menu->addAction(mWindow->mSearchAction); + menu->addSeparator(); + menu->addAction(mWindow->mSettingsAction); +} + +void MainWindowExtension::aboutToShowViewContextMenu(QMenu* menu, const QPoint&) { menu->addAction(mWindow->mIncreaseFontAction); menu->addAction(mWindow->mDecreaseFontAction); menu->addAction(mWindow->mResetFontAction); - menu->addAction(mWindow->mAlwaysOnTopAction); menu->addSeparator(); - menu->addAction(mWindow->mSettingsAction); + menu->addAction(mWindow->mAlwaysOnTopAction); } //- MainWindow ----------------------------------------- @@ -54,6 +58,8 @@ MainWindow::MainWindow(QWidget* parent) , mSettings(new Settings(this)) , mTextEdit(new TextEdit(this)) , mAutoSaveTimer(new QTimer(this)) + , mSearchWidget(new SearchWidget(mTextEdit, this)) + , mSearchToolBar(new QToolBar(this)) , mIncreaseFontAction(new QAction(this)) , mDecreaseFontAction(new QAction(this)) , mResetFontAction(new QAction(this)) @@ -65,9 +71,8 @@ MainWindow::MainWindow(QWidget* parent) setCentralWidget(mTextEdit); - loadSearchWidget(); - setupTextEdit(); + setupSearchBar(); setupAutoSaveTimer(); setupActions(); loadNotes(); @@ -126,17 +131,16 @@ void MainWindow::setupActions() { connect(mAlwaysOnTopAction, &QAction::toggled, this, &MainWindow::setAlwaysOnTop); addAction(mAlwaysOnTopAction); - mSettingsAction->setText(tr("Settings...")); + mSettingsAction->setText(tr("Settings | About...")); connect(mSettingsAction, &QAction::triggered, this, &MainWindow::showSettingsDialog); addAction(mSettingsAction); // Add find shortcut - mSearchAction->setText(tr("Find in text")); + mSearchAction->setText(tr("Find")); mSearchAction->setShortcut(QKeySequence::Find); connect(mSearchAction, &QAction::triggered, this, &MainWindow::showSearchBar); addAction(mSearchAction); - mCloseSearchAction->setText(tr("Close search tab")); mCloseSearchAction->setShortcut(Qt::Key_Escape); connect(mCloseSearchAction, &QAction::triggered, this, &MainWindow::hideSearchBar); addAction(mCloseSearchAction); @@ -278,18 +282,13 @@ void MainWindow::showSettingsDialog() { mSettingsDialog->show(); } -void MainWindow::loadSearchWidget() { - if (!mSearchWidget) { - mSearchWidget = new SearchWidget(mTextEdit, this); - connect(mSearchWidget, &SearchWidget::closeClicked, this, &MainWindow::hideSearchBar); - } - if (!mSearchToolBar) { - mSearchToolBar = new QToolBar(this); - mSearchToolBar->addWidget(mSearchWidget); - mSearchToolBar->setVisible(false); - mSearchToolBar->setMovable(false); - addToolBar(Qt::BottomToolBarArea, mSearchToolBar); - } +void MainWindow::setupSearchBar() { + connect(mSearchWidget, &SearchWidget::closeClicked, this, &MainWindow::hideSearchBar); + + mSearchToolBar->addWidget(mSearchWidget); + mSearchToolBar->setVisible(false); + mSearchToolBar->setMovable(false); + addToolBar(Qt::BottomToolBarArea, mSearchToolBar); } void MainWindow::showSearchBar() { diff --git a/src/MainWindow.h b/src/MainWindow.h index c8e6267..6d8b7ce 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -23,6 +23,7 @@ class MainWindowExtension : public TextEditExtension { explicit MainWindowExtension(MainWindow* window); void aboutToShowContextMenu(QMenu* menu, const QPoint& /*pos*/) override; + void aboutToShowViewContextMenu(QMenu* menu, const QPoint& /*pos*/) override; private: MainWindow* mWindow; @@ -37,6 +38,7 @@ class MainWindow : public QMainWindow { private: void setupTextEdit(); + void setupSearchBar(); void setupAutoSaveTimer(); void setupActions(); void loadNotes(); @@ -47,23 +49,23 @@ class MainWindow : public QMainWindow { void resetFontSize(); void setAlwaysOnTop(bool onTop); void showSettingsDialog(); - void loadSearchWidget(); void showSearchBar(); void hideSearchBar(); - Settings* mSettings; - TextEdit* mTextEdit; - QTimer* mAutoSaveTimer; - SearchWidget* mSearchWidget = nullptr; - QToolBar* mSearchToolBar = nullptr; - - QAction* mIncreaseFontAction; - QAction* mDecreaseFontAction; - QAction* mResetFontAction; - QAction* mAlwaysOnTopAction; - QAction* mSettingsAction; - QAction* mSearchAction; - QAction* mCloseSearchAction = nullptr; + Settings* const mSettings; + TextEdit* const mTextEdit; + QTimer* const mAutoSaveTimer; + SearchWidget* const mSearchWidget; + QToolBar* const mSearchToolBar; + + QAction* const mIncreaseFontAction; + QAction* const mDecreaseFontAction; + QAction* const mResetFontAction; + QAction* const mAlwaysOnTopAction; + QAction* const mSettingsAction; + QAction* const mSearchAction; + QAction* const mCloseSearchAction; + QPointer mSettingsDialog; friend class MainWindowExtension; diff --git a/src/MoveLinesExtension.cpp b/src/MoveLinesExtension.cpp index 4f5ed7f..f8248bc 100644 --- a/src/MoveLinesExtension.cpp +++ b/src/MoveLinesExtension.cpp @@ -1,37 +1,67 @@ #include "MoveLinesExtension.h" +#include #include +#include #include -static constexpr Qt::KeyboardModifiers MODIFIERS = Qt::ShiftModifier | Qt::AltModifier; +MoveLinesExtension::MoveLinesExtension(TextEdit* textEdit) + : TextEditExtension(textEdit) + , mMoveUpAction(std::make_unique()) + , mMoveDownAction(std::make_unique()) { + mMoveUpAction->setText(tr("Move selected lines up")); + mMoveUpAction->setShortcut(Qt::SHIFT | Qt::ALT | Qt::Key_Up); + connect(mMoveUpAction.get(), &QAction::triggered, this, &MoveLinesExtension::moveUp); + mTextEdit->addAction(mMoveUpAction.get()); -MoveLinesExtension::MoveLinesExtension(TextEdit* textEdit) : TextEditExtension(textEdit) { + mMoveDownAction->setText(tr("Move selected lines down")); + mMoveDownAction->setShortcut(Qt::SHIFT | Qt::ALT | Qt::Key_Down); + connect(mMoveDownAction.get(), &QAction::triggered, this, &MoveLinesExtension::moveDown); + mTextEdit->addAction(mMoveDownAction.get()); } -bool MoveLinesExtension::keyPress(QKeyEvent* event) { - if (event->modifiers() == MODIFIERS) { - if (event->key() == Qt::Key_Down) { - moveSelectedLines(1); - return true; - } else if (event->key() == Qt::Key_Up) { - moveSelectedLines(-1); - return true; - } - } - return false; +MoveLinesExtension::~MoveLinesExtension() { +} + +void MoveLinesExtension::aboutToShowEditContextMenu(QMenu* menu, const QPoint& /*pos*/) { + menu->addAction(mMoveUpAction.get()); + menu->addAction(mMoveDownAction.get()); +} + +void MoveLinesExtension::moveUp() { + moveSelectedLines(-1); +} + +void MoveLinesExtension::moveDown() { + moveSelectedLines(1); } /** * Add a final \\n if needed. Returns true if it added one. */ -static bool addFinalNewLine(TextEdit* textEdit) { +static bool addFinalNewLine(TextEdit* textEdit, QTextCursor* cursor) { if (textEdit->document()->lastBlock().text().isEmpty()) { return false; } - // Use our own cursor to avoid altering the current one - auto cursor = textEdit->textCursor(); - cursor.movePosition(QTextCursor::End); - cursor.insertBlock(); + + // The `cursor` from `moveSelectedLines()` must stay at the same position. A previous version of + // this function created its own cursor using QTextEdit::textCursor(), but if the cursor is at + // the very end of the document, then `moveSelectedLines()` cursor is moved when we insert the + // \n. I assue this is because the cursor is considered to be *after* the insertion position + // of the \n, so Qt maintains its position. + // To avoid that, we save the cursor state manually before inserting the \n, and restore the + // state before leaving this function. + int oldStart = cursor->selectionStart(); + int oldEnd = cursor->selectionEnd(); + if (oldStart == cursor->position()) { + std::swap(oldStart, oldEnd); + } + + cursor->movePosition(QTextCursor::End); + cursor->insertBlock(); + + cursor->setPosition(oldStart); + cursor->setPosition(oldEnd, QTextCursor::KeepAnchor); return true; } @@ -48,7 +78,7 @@ void MoveLinesExtension::moveSelectedLines(int delta) { // To avoid dealing with the special-case of the last line not ending with // a \n, we add one if there is none and remove it at the end - bool addedFinalNewLine = addFinalNewLine(mTextEdit); + bool addedFinalNewLine = addFinalNewLine(mTextEdit, &cursor); // Find start and end of lines to move QTextBlock startBlock, endBlock; diff --git a/src/MoveLinesExtension.h b/src/MoveLinesExtension.h index 3a39cad..1a9043f 100644 --- a/src/MoveLinesExtension.h +++ b/src/MoveLinesExtension.h @@ -3,15 +3,26 @@ #include "TextEdit.h" +#include + +class Action; + class MoveLinesExtension : public TextEditExtension { Q_OBJECT public: explicit MoveLinesExtension(TextEdit* textEdit); + ~MoveLinesExtension(); - bool keyPress(QKeyEvent* event) override; + void aboutToShowEditContextMenu(QMenu* menu, const QPoint& pos) override; + + void moveUp(); + void moveDown(); private: void moveSelectedLines(int delta); + + const std::unique_ptr mMoveUpAction; + const std::unique_ptr mMoveDownAction; }; #endif // MOVELINEEXTENSION_H diff --git a/src/SearchWidget.cpp b/src/SearchWidget.cpp index 8fe7848..c50fc6b 100644 --- a/src/SearchWidget.cpp +++ b/src/SearchWidget.cpp @@ -1,32 +1,38 @@ #include "SearchWidget.h" -#include "ui_SearchForm.h" +#include "ui_SearchWidget.h" #include #include "TextEdit.h" SearchWidget::SearchWidget(TextEdit* textEdit, QWidget* parent) - : QWidget(parent), mUi(new Ui::SearchForm), mTextEdit(textEdit) { + : QWidget(parent), mUi(new Ui::SearchWidget), mTextEdit(textEdit) { mUi->setupUi(this); layout()->setContentsMargins(0, 0, 0, 0); - setFocusProxy(mUi->searchLine); + setFocusProxy(mUi->lineEdit); mUi->countLabel->hide(); + mUi->closeButton->setToolTip(tr("Close search bar")); + connect(mUi->nextButton, &QToolButton::clicked, this, &SearchWidget::selectNextMatch); connect(mUi->previousButton, &QToolButton::clicked, this, &SearchWidget::selectPreviousMatch); connect(mTextEdit, &TextEdit::textChanged, this, &SearchWidget::onDocumentChanged); - connect(mUi->searchLine, &QLineEdit::textChanged, this, &SearchWidget::onSearchLineChanged); + connect(mUi->lineEdit, &QLineEdit::textChanged, this, &SearchWidget::onLineEditChanged); connect(mUi->closeButton, &QToolButton::clicked, this, &SearchWidget::closeClicked); - connect(mUi->searchLine, &QLineEdit::returnPressed, this, &SearchWidget::selectNextMatch); + connect(mUi->lineEdit, &QLineEdit::returnPressed, this, &SearchWidget::selectNextMatch); } SearchWidget::~SearchWidget() { } void SearchWidget::initialize(const QString& text) { - mUi->searchLine->setFocus(); - mUi->searchLine->setText(text); + mUi->lineEdit->setFocus(); + bool textChanged = mUi->lineEdit->text() != text; + mUi->lineEdit->setText(text); + if (!textChanged) { + search(); + } } void SearchWidget::uninitialize() { @@ -34,7 +40,7 @@ void SearchWidget::uninitialize() { } void SearchWidget::search() { - mTextDocument = mTextEdit->toPlainText(); + mPreviousText = mTextEdit->toPlainText(); QTextCursor cursor(mTextEdit->document()); cursor.beginEditBlock(); @@ -44,6 +50,7 @@ void SearchWidget::search() { highlightMatches(); cursor.endEditBlock(); + updateLineEdit(); updateCountLabel(); } @@ -51,7 +58,11 @@ void SearchWidget::selectNextMatch() { if (mMatchPositions.empty()) { return; } - mCurrentMatch = (mCurrentMatch.value() + 1) % mMatchPositions.size(); + int minPosition = mTextEdit->textCursor().selectionStart(); + auto it = std::find_if(mMatchPositions.begin(), + mMatchPositions.end(), + [minPosition](int position) { return position > minPosition; }); + mCurrentMatch = it != mMatchPositions.end() ? std::distance(mMatchPositions.begin(), it) : 0; selectCurrentMatch(); } @@ -59,10 +70,17 @@ void SearchWidget::selectPreviousMatch() { if (mMatchPositions.empty()) { return; } - if (mCurrentMatch != 0) { - mCurrentMatch = mCurrentMatch.value() - 1; - } else { + int maxPosition = mTextEdit->textCursor().selectionStart(); + auto it = std::find_if(mMatchPositions.rbegin(), + mMatchPositions.rend(), + [maxPosition](int position) { return position < maxPosition; }); + + if (it == mMatchPositions.rend()) { mCurrentMatch = mMatchPositions.size() - 1; + } else { + // rlast is the first element of mMatchPosition + auto rlast = std::prev(mMatchPositions.rend()); + mCurrentMatch = std::distance(it, rlast); } selectCurrentMatch(); } @@ -73,7 +91,7 @@ void SearchWidget::highlightMatches() { for (int position : mMatchPositions) { QTextCursor cursor = mTextEdit->textCursor(); cursor.setPosition(position, QTextCursor::MoveAnchor); - cursor.setPosition(position + mUi->searchLine->text().size(), QTextCursor::KeepAnchor); + cursor.setPosition(position + mUi->lineEdit->text().size(), QTextCursor::KeepAnchor); QTextEdit::ExtraSelection currentWord; currentWord.format.setBackground(highlightColor); @@ -91,13 +109,17 @@ void SearchWidget::onDocumentChanged() { if (!isVisible()) { return; } - if (mTextDocument == mTextEdit->toPlainText()) { + // When we highlight the search matches, documentChanged() is emitted. Compare current text with + // the previous content and do not restart a search in this case, to prevent endless recursions. + // This is not optimal, it would probably be better to use a syntax highlighter for matches, but + // this is good enough for now. + if (mPreviousText == mTextEdit->toPlainText()) { return; } search(); } -void SearchWidget::onSearchLineChanged() { +void SearchWidget::onLineEditChanged() { search(); if (mCurrentMatch.has_value()) { selectCurrentMatch(); @@ -106,7 +128,7 @@ void SearchWidget::onSearchLineChanged() { void SearchWidget::updateMatchPositions() { auto* document = mTextEdit->document(); - QString searchString = mUi->searchLine->text(); + QString searchString = mUi->lineEdit->text(); mMatchPositions.clear(); QTextCursor cursor(document); @@ -129,7 +151,7 @@ void SearchWidget::selectCurrentMatch() { cursor.beginEditBlock(); int startPosition = mMatchPositions.at(mCurrentMatch.value()); cursor.setPosition(startPosition, QTextCursor::MoveAnchor); - cursor.setPosition(startPosition + mUi->searchLine->text().size(), QTextCursor::KeepAnchor); + cursor.setPosition(startPosition + mUi->lineEdit->text().size(), QTextCursor::KeepAnchor); mTextEdit->setTextCursor(cursor); cursor.endEditBlock(); updateCountLabel(); @@ -144,3 +166,20 @@ void SearchWidget::updateCountLabel() { mUi->countLabel->hide(); } } + +static QColor mixColors(const QColor& c1, const QColor& c2, qreal k) { + return QColor::fromRgbF(c1.redF() * (1 - k) + c2.redF() * k, + c1.greenF() * (1 - k) + c2.greenF() * k, + c1.blueF() * (1 - k) + c2.blueF() * k); +} + +void SearchWidget::updateLineEdit() { + static QPalette noMatchPalette = [this] { + auto palette = mUi->lineEdit->palette(); + auto baseColor = palette.color(QPalette::Base); + palette.setColor(QPalette::Base, mixColors(baseColor, Qt::red, 0.3)); + return palette; + }(); + bool noMatch = mMatchPositions.empty() && !mUi->lineEdit->text().isEmpty(); + mUi->lineEdit->setPalette(noMatch ? noMatchPalette : palette()); +} diff --git a/src/SearchWidget.h b/src/SearchWidget.h index 7a5afa1..e03c8d5 100644 --- a/src/SearchWidget.h +++ b/src/SearchWidget.h @@ -11,7 +11,7 @@ class TextEdit; namespace Ui { -class SearchForm; +class SearchWidget; } class SearchWidget : public QWidget { @@ -35,14 +35,16 @@ class SearchWidget : public QWidget { void updateCountLabel(); void highlightMatches(); void removeHighlights(); - void onSearchLineChanged(); + void onLineEditChanged(); void search(); void updateMatchPositions(); + void updateLineEdit(); - const std::unique_ptr mUi; + const std::unique_ptr mUi; TextEdit* const mTextEdit; std::vector mMatchPositions; - QString mTextDocument; + // The content of the TextEdit last time we did a search + QString mPreviousText; std::optional mCurrentMatch; }; diff --git a/src/SearchForm.ui b/src/SearchWidget.ui similarity index 88% rename from src/SearchForm.ui rename to src/SearchWidget.ui index e09a7cd..a2071de 100644 --- a/src/SearchForm.ui +++ b/src/SearchWidget.ui @@ -1,7 +1,7 @@ - SearchForm - + SearchWidget + 0 @@ -10,19 +10,16 @@ 48 - - Form - - - / - + [count] - + @@ -75,7 +72,7 @@ - searchLine + lineEdit previousButton nextButton closeButton diff --git a/src/SettingsDialog.cpp b/src/SettingsDialog.cpp index 88e1f43..ae3bfe6 100644 --- a/src/SettingsDialog.cpp +++ b/src/SettingsDialog.cpp @@ -5,11 +5,13 @@ #include "Settings.h" -static const char PROJECT_URL[] = "https://github.com/agateau/nanonote"; +static constexpr char PROJECT_URL[] = "https://github.com/agateau/nanonote/"; +static constexpr char SUPPORT_URL[] = "https://agateau.com/support/"; SettingsDialog::SettingsDialog(Settings* settings, QWidget* parent) : QDialog(parent), ui(new Ui::SettingsDialog), mSettings(settings) { ui->setupUi(this); + setupConfigTab(); setupAboutTab(); ui->tabWidget->setCurrentIndex(0); @@ -32,17 +34,29 @@ SettingsDialog::~SettingsDialog() { delete ui; } -void SettingsDialog::setupAboutTab() { - auto projectLink = QString("%1").arg(PROJECT_URL); - auto noteLink = QString("%1").arg(Settings::notePath()); - auto text = tr("

Nanonote %1

" - "

A minimalist note taking application.
" - "%2

" - "

Your notes are stored in %3.

", - "%1=version %2=projectLink %3=noteLink") - .arg(qApp->applicationVersion(), projectLink, noteLink); +void SettingsDialog::setupConfigTab() { + auto noteLink = QString("%1").arg(Settings::notePath()); + ui->noteLocationLabel->setText(noteLink); +} +void SettingsDialog::setupAboutTab() { + auto text = tr(R"(

Nanonote %1

+

A minimalist note taking application.
+%2

)", + "%1: version, %2: project url") + .arg(qApp->applicationVersion(), PROJECT_URL); ui->aboutLabel->setText(text); + + text = tr(R"(

Hi,

+

I hope you enjoy Nanonote!

+

If you do, it would be lovely if you could support my work on free and open source software.

+

― Aurélien

)", + "%1: support url") + .arg(SUPPORT_URL); + auto font = ui->supportLabel->font(); + font.setItalic(true); + ui->supportLabel->setFont(font); + ui->supportLabel->setText(text); } void SettingsDialog::updateFontFromSettings() { diff --git a/src/SettingsDialog.h b/src/SettingsDialog.h index 9d4c6a8..6032088 100644 --- a/src/SettingsDialog.h +++ b/src/SettingsDialog.h @@ -17,6 +17,7 @@ class SettingsDialog : public QDialog { ~SettingsDialog(); private: + void setupConfigTab(); void setupAboutTab(); void updateFontFromSettings(); diff --git a/src/SettingsDialog.ui b/src/SettingsDialog.ui index 151fca5..6070289 100644 --- a/src/SettingsDialog.ui +++ b/src/SettingsDialog.ui @@ -6,22 +6,32 @@ 0 0 - 464 - 272 + 435 + 290 Settings - - + + + + + Qt::Horizontal + + + QDialogButtonBox::Close + + + + - 1 + 0 - Appearance + Configuration @@ -29,6 +39,9 @@ Font family: + + fontComboBox + @@ -53,7 +66,7 @@ - 40 + 0 20 @@ -61,39 +74,75 @@ - -
- - - About - - - - + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 24 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + - + [note location] - - :/icons/sc-apps-nanonote.svg + + true - - false + + true - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + Qt::TextBrowserInteraction + + + + + + + Your notes are stored here: - + + + + + About + + + - + 0 0 - + [about] Qt::RichText @@ -112,24 +161,68 @@ + + + + + 0 + 0 + + + + [support] + + + true + + + true + + + Qt::TextBrowserInteraction + + + + + + + + + + :/icons/sc-apps-nanonote.svg + + + false + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 12 + + + + - - - - Qt::Horizontal - - - QDialogButtonBox::Close - - - - + @@ -139,8 +232,8 @@ accept() - 248 - 254 + 257 + 280 157 diff --git a/src/TextEdit.cpp b/src/TextEdit.cpp index 8f3164e..7931cad 100644 --- a/src/TextEdit.cpp +++ b/src/TextEdit.cpp @@ -15,6 +15,12 @@ TextEditExtension::TextEditExtension(TextEdit* textEdit) : QObject(textEdit), mT void TextEditExtension::aboutToShowContextMenu(QMenu* /*menu*/, const QPoint& /*pos*/) { } +void TextEditExtension::aboutToShowEditContextMenu(QMenu* /*menu*/, const QPoint& /*pos*/) { +} + +void TextEditExtension::aboutToShowViewContextMenu(QMenu* /*menu*/, const QPoint& /*pos*/) { +} + bool TextEditExtension::keyPress(QKeyEvent* /*event*/) { return false; } @@ -40,6 +46,18 @@ void TextEdit::contextMenuEvent(QContextMenuEvent* event) { auto pos = event->pos(); std::unique_ptr menu(createStandardContextMenu(pos)); menu->addSeparator(); + auto editMenu = menu->addMenu(tr("Edit")); + connect(editMenu, &QMenu::aboutToShow, this, [this, editMenu, pos] { + for (auto extension : mExtensions) { + extension->aboutToShowEditContextMenu(editMenu, pos); + } + }); + auto viewMenu = menu->addMenu(tr("View")); + connect(viewMenu, &QMenu::aboutToShow, this, [this, viewMenu, pos] { + for (auto extension : mExtensions) { + extension->aboutToShowViewContextMenu(viewMenu, pos); + } + }); for (auto extension : mExtensions) { extension->aboutToShowContextMenu(menu.get(), pos); } diff --git a/src/TextEdit.h b/src/TextEdit.h index 2899e5a..1c5e059 100644 --- a/src/TextEdit.h +++ b/src/TextEdit.h @@ -17,6 +17,10 @@ class TextEditExtension : public QObject { virtual void aboutToShowContextMenu(QMenu* menu, const QPoint& pos); + virtual void aboutToShowEditContextMenu(QMenu* menu, const QPoint& pos); + + virtual void aboutToShowViewContextMenu(QMenu* menu, const QPoint& pos); + virtual bool keyPress(QKeyEvent* event); virtual bool keyRelease(QKeyEvent* event); @@ -30,6 +34,7 @@ class TextEditExtension : public QObject { }; class TextEdit : public QPlainTextEdit { + Q_OBJECT public: TextEdit(QWidget* parent = nullptr); diff --git a/src/translations/app_de.ts b/src/translations/app_de.ts index 42c904e..be59102 100644 --- a/src/translations/app_de.ts +++ b/src/translations/app_de.ts @@ -39,7 +39,7 @@ Settings... - Konfiguration... + Konfiguration... Welcome to Nanonote! @@ -142,24 +142,27 @@ Das ist alles, was es zu sagen gibt, jetzt kannst du diesen Text löschen und an - Find in text + Settings | About... - Close search tab + Find - SearchForm + MoveLinesExtension - Form + Move selected lines up - - / - + Move selected lines down + + + SearchWidget Previous @@ -168,6 +171,10 @@ Das ist alles, was es zu sagen gibt, jetzt kannst du diesen Text löschen und an Next + + Close search bar + + SettingsDialog @@ -177,7 +184,7 @@ Das ist alles, was es zu sagen gibt, jetzt kannst du diesen Text löschen und an Appearance - Erscheinungsbild + Erscheinungsbild Font family: @@ -194,7 +201,41 @@ Das ist alles, was es zu sagen gibt, jetzt kannst du diesen Text löschen und an <h2>Nanonote %1</h2><p>A minimalist note taking application.<br>%2</p><p>Your notes are stored in %3.</p> %1=version %2=projectLink %3=noteLink - <h2>Nanonote %1</h2><p>Eine minimalistische Notizapplikation.<br>%2</p><p>Deine Notizen werden gespeichert in %3.</p> + <h2>Nanonote %1</h2><p>Eine minimalistische Notizapplikation.<br>%2</p><p>Deine Notizen werden gespeichert in %3.</p> + + + Configuration + + + + <h2>Nanonote %1</h2> +<p>A minimalist note taking application.<br> +<a href='%2'>%2</a></p> + %1: version, %2: project url + + + + <p>Hi,</p> +<p>I hope you enjoy Nanonote!</p> +<p>If you do, it would be lovely if you could <a href='%1'>support my work</a> on free and open source software.</p> +<p align="right">― Aurélien</p> + %1: support url + + + + Your notes are stored here: + + + + + TextEdit + + Edit + + + + View + diff --git a/src/translations/app_es.ts b/src/translations/app_es.ts index 7971e2c..00eb5c8 100644 --- a/src/translations/app_es.ts +++ b/src/translations/app_es.ts @@ -39,7 +39,7 @@ Settings... - Ajustes... + Ajustes... Welcome to Nanonote! @@ -94,24 +94,27 @@ Y esto es todo lo que hay que decir, ahora puedes eliminar este texto ¡y empeza Restablecer tamaño de la fuente - Find in text + Settings | About... - Close search tab + Find - SearchForm + MoveLinesExtension - Form + Move selected lines up - - / - + Move selected lines down + + + SearchWidget Previous @@ -120,6 +123,10 @@ Y esto es todo lo que hay que decir, ahora puedes eliminar este texto ¡y empeza Next + + Close search bar + + SettingsDialog @@ -129,7 +136,7 @@ Y esto es todo lo que hay que decir, ahora puedes eliminar este texto ¡y empeza Appearance - Apariencia + Apariencia Font family: @@ -146,7 +153,41 @@ Y esto es todo lo que hay que decir, ahora puedes eliminar este texto ¡y empeza <h2>Nanonote %1</h2><p>A minimalist note taking application.<br>%2</p><p>Your notes are stored in %3.</p> %1=version %2=projectLink %3=noteLink - <h2>Nanonote %1</h2><p>Una aplicación minimalista para tomar notas.<br>%2</p><p>Tus notas son almacenadas en %3.</p> + <h2>Nanonote %1</h2><p>Una aplicación minimalista para tomar notas.<br>%2</p><p>Tus notas son almacenadas en %3.</p> + + + Configuration + + + + <h2>Nanonote %1</h2> +<p>A minimalist note taking application.<br> +<a href='%2'>%2</a></p> + %1: version, %2: project url + + + + <p>Hi,</p> +<p>I hope you enjoy Nanonote!</p> +<p>If you do, it would be lovely if you could <a href='%1'>support my work</a> on free and open source software.</p> +<p align="right">― Aurélien</p> + %1: support url + + + + Your notes are stored here: + + + + + TextEdit + + Edit + + + + View + diff --git a/src/translations/app_fr.ts b/src/translations/app_fr.ts index 231e588..e5286be 100644 --- a/src/translations/app_fr.ts +++ b/src/translations/app_fr.ts @@ -39,7 +39,7 @@ Settings... - Configuration... + Configuration... Welcome to Nanonote! @@ -94,31 +94,38 @@ C'est tout ce qu'il y a dire, maintenant vous pouvez effacer ce texte Revenir à la taille de texte par défaut - Find in text - + Settings | About... + Configuration | À propos... - Close search tab - + Find + Chercher - SearchForm + MoveLinesExtension - Form - + Move selected lines up + Déplacer les lignes sélectionnées vers le haut - - / - - + Move selected lines down + Déplacer les lignes sélectionnées vers le bas + + + SearchWidget Previous - + Précédent Next - + Suivant + + + Close search bar + Fermer la barre de recherche @@ -129,7 +136,7 @@ C'est tout ce qu'il y a dire, maintenant vous pouvez effacer ce texte Appearance - Apparence + Apparence Font family: @@ -146,7 +153,46 @@ C'est tout ce qu'il y a dire, maintenant vous pouvez effacer ce texte <h2>Nanonote %1</h2><p>A minimalist note taking application.<br>%2</p><p>Your notes are stored in %3.</p> %1=version %2=projectLink %3=noteLink - <h2>Nanonote %1</h2><p>Une application de prise de note minimaliste.<br>%2</p><p>Vos notes sont stockées ici : %3.</p> + <h2>Nanonote %1</h2><p>Une application de prise de note minimaliste.<br>%2</p><p>Vos notes sont stockées ici : %3.</p> + + + Configuration + Configuration + + + <h2>Nanonote %1</h2> +<p>A minimalist note taking application.<br> +<a href='%2'>%2</a></p> + %1: version, %2: project url + <h2>Nanonote %1</h2> +<p>Une application de prise de notes minimaliste.<br> +<a href='%2'>%2</a></p> + + + <p>Hi,</p> +<p>I hope you enjoy Nanonote!</p> +<p>If you do, it would be lovely if you could <a href='%1'>support my work</a> on free and open source software.</p> +<p align="right">― Aurélien</p> + %1: support url + <p>Bonjour,</p> +<p>J'espère que vous appréciez Nanonote !</p> +<p>Si c'est le cas, ce serait super si vous pouviez <a href='%1'>soutenir mon travail</a> de création de logiciels libres.</p> +<p align="right">― Aurélien</p> + + + Your notes are stored here: + Vos notes sont stockées ici : + + + + TextEdit + + Edit + Édition + + + View + Affichage diff --git a/tests/MoveLinesExtensionTest.cpp b/tests/MoveLinesExtensionTest.cpp index 1a25b24..de3b366 100644 --- a/tests/MoveLinesExtensionTest.cpp +++ b/tests/MoveLinesExtensionTest.cpp @@ -8,15 +8,17 @@ #include "TextUtils.h" -static constexpr Qt::KeyboardModifiers MODIFIERS = Qt::ShiftModifier | Qt::AltModifier; - SCENARIO("movelines") { QMainWindow window; TextEdit* edit = new TextEdit; window.setCentralWidget(edit); - edit->addExtension(new MoveLinesExtension(edit)); + MoveLinesExtension extension(edit); + edit->addExtension(&extension); // Some tests won't work if the window is not visible (for example word-wrapping tests) window.show(); + + auto moveLinesDown = [&extension] { extension.moveDown(); }; + auto moveLinesUp = [&extension] { extension.moveUp(); }; GIVEN("A cursor at the beginning of a line") { setupTextEditContent(edit, "1\n" @@ -24,7 +26,7 @@ SCENARIO("movelines") { "3\n"); WHEN("I press modifiers+down") { - QTest::keyClick(edit, Qt::Key_Down, MODIFIERS); + moveLinesDown(); THEN("The line is moved down") { REQUIRE(dumpTextEditContent(edit) == QString("1\n" @@ -33,7 +35,7 @@ SCENARIO("movelines") { } } WHEN("I press modifiers+up") { - QTest::keyClick(edit, Qt::Key_Up, MODIFIERS); + moveLinesUp(); THEN("The line is moved up") { REQUIRE(dumpTextEditContent(edit) == QString("|2\n" @@ -48,7 +50,7 @@ SCENARIO("movelines") { "2"); WHEN("I press modifiers+down") { - QTest::keyClick(edit, Qt::Key_Down, MODIFIERS); + moveLinesDown(); THEN("The line is moved down") { REQUIRE(dumpTextEditContent(edit) == QString("2\n" @@ -63,13 +65,13 @@ SCENARIO("movelines") { "3\n"); WHEN("I press modifiers+down") { - QTest::keyClick(edit, Qt::Key_Down, MODIFIERS); + moveLinesDown(); THEN("The line is moved down") { REQUIRE(dumpTextEditContent(edit) == QString("1\n3\n22|22\n")); } } WHEN("I press modifiers+up") { - QTest::keyClick(edit, Qt::Key_Up, MODIFIERS); + moveLinesUp(); THEN("The line is moved up") { REQUIRE(dumpTextEditContent(edit) == QString("22|22\n" @@ -78,6 +80,22 @@ SCENARIO("movelines") { } } } + GIVEN("A cursor after the last character") { + setupTextEditContent(edit, + "1\n" + "2\n" + "3|"); + + WHEN("I press modifiers+up") { + moveLinesUp(); + THEN("The line is moved up") { + REQUIRE(dumpTextEditContent(edit) + == QString("1\n" + "3|\n" + "2")); + } + } + } GIVEN("A multi-line selection") { setupTextEditContent(edit, "1\n" @@ -85,7 +103,7 @@ SCENARIO("movelines") { "333|3\n" "4\n"); WHEN("I press modifiers+down") { - QTest::keyClick(edit, Qt::Key_Down, MODIFIERS); + moveLinesDown(); THEN("The selected lines are moved down") { REQUIRE(dumpTextEditContent(edit) == QString("1\n" @@ -95,7 +113,7 @@ SCENARIO("movelines") { } } WHEN("I press modifiers+up") { - QTest::keyClick(edit, Qt::Key_Up, MODIFIERS); + moveLinesUp(); THEN("The lines are moved up") { REQUIRE(dumpTextEditContent(edit) == QString("22*22\n" @@ -113,7 +131,7 @@ SCENARIO("movelines") { "4\n"); WHEN("I press modifiers+down") { - QTest::keyClick(edit, Qt::Key_Down, MODIFIERS); + moveLinesDown(); THEN("The selected lines are moved down") { REQUIRE(dumpTextEditContent(edit) == QString("1\n" @@ -123,7 +141,7 @@ SCENARIO("movelines") { } } WHEN("I press modifiers+up") { - QTest::keyClick(edit, Qt::Key_Up, MODIFIERS); + moveLinesUp(); THEN("The lines are moved up") { REQUIRE(dumpTextEditContent(edit) == QString("22|22\n" @@ -140,7 +158,7 @@ SCENARIO("movelines") { "4"; setupTextEditContent(edit, initialContent); AND_GIVEN("I moved the line down") { - QTest::keyClick(edit, Qt::Key_Down, MODIFIERS); + moveLinesDown(); WHEN("I undo the changes") { edit->undo(); THEN("The text edit is back to its previous state") { @@ -161,7 +179,7 @@ SCENARIO("movelines") { // TODO: Add a REQUIRE checking the first line is wrapped WHEN("I move the first line down") { - QTest::keyClick(edit, Qt::Key_Down, MODIFIERS); + moveLinesDown(); THEN("The entire wrapped line is moved") { auto expectedContent = stringFill('b') + "\n|" + stringFill('a') + '\n' + stringFill('c'); @@ -186,7 +204,7 @@ SCENARIO("movelines") { // TODO: Add a REQUIRE checking the first line is wrapped WHEN("I move the line up") { - QTest::keyClick(edit, Qt::Key_Up, MODIFIERS); + moveLinesUp(); THEN("The entire wrapped line is moved") { auto expectedContent = stringFill('a') + "\n|" + stringFill('c') + '\n' + stringFill('b'); diff --git a/tests/SearchWidgetTest.cpp b/tests/SearchWidgetTest.cpp index ea6e7b1..c534d71 100644 --- a/tests/SearchWidgetTest.cpp +++ b/tests/SearchWidgetTest.cpp @@ -9,6 +9,21 @@ #include +struct CursorSpan { + CursorSpan(const QTextCursor& cursor) + : start(cursor.selectionStart()), length(cursor.selectionEnd() - start) { + } + CursorSpan(int start, int length) : start(start), length(length) { + } + + const int start; + const int length; +}; + +bool operator==(const CursorSpan& c1, const CursorSpan& c2) { + return c1.start == c2.start && c1.length == c2.length; +} + TEST_CASE("searchwidget") { TextEdit edit; SearchWidget searchWidget(&edit); @@ -73,4 +88,55 @@ TEST_CASE("searchwidget") { searchWidget.initialize("no match"); REQUIRE(!isVisible()); } + + SECTION("highlights") { + edit.setPlainText("a bb bb"); + searchWidget.initialize("bb"); + auto selections = edit.extraSelections(); + CHECK(selections.count() == 2); + CHECK(CursorSpan(selections.at(0).cursor) == CursorSpan{2, 2}); + CHECK(CursorSpan(selections.at(1).cursor) == CursorSpan{5, 2}); + + SECTION("uninitialize must remove highlights") { + searchWidget.uninitialize(); + selections = edit.extraSelections(); + REQUIRE(selections.count() == 0); + + SECTION("initializing again with the same text must bring back highlights") { + searchWidget.initialize("bb"); + selections = edit.extraSelections(); + CHECK(selections.count() == 2); + CHECK(CursorSpan(selections.at(0).cursor) == CursorSpan{2, 2}); + CHECK(CursorSpan(selections.at(1).cursor) == CursorSpan{5, 2}); + } + } + } + + SECTION("search, change selection") { + // GIVEN a search with multiple matches + edit.setPlainText("a bb bb cc bb"); + searchWidget.initialize("bb"); + REQUIRE(dumpTextEditContent(&edit) == "a *bb| bb cc bb"); + + // AND cursor has been moved after the 2nd match + auto cursor = edit.textCursor(); + cursor.setPosition(cursor.position() + 4); + edit.setTextCursor(cursor); + REQUIRE(dumpTextEditContent(&edit) == "a bb bb |cc bb"); + + SECTION("then search forward") { + // WHEN I search for the next match + QTest::mouseClick(nextButton, Qt::LeftButton); + + // THEN the first match *after the cursor* is selected + REQUIRE(dumpTextEditContent(&edit) == "a bb bb cc *bb|"); + } + SECTION("then search backward") { + // WHEN I search for the previous match + QTest::mouseClick(previousButton, Qt::LeftButton); + + // THEN the first match *before the cursor* is selected + REQUIRE(dumpTextEditContent(&edit) == "a bb *bb| cc bb"); + } + } }