diff --git a/.gitignore b/.gitignore index b146fe8..647609f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ /build /dist -CMakeLists.txt.user +CMakeLists.txt.user* .*.swp diff --git a/.gitmodules b/.gitmodules index 8ce66d9..78cef8c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,9 @@ [submodule "singleapplication"] - path = singleapplication/src + path = 3rdparty/singleapplication/src url = https://github.com/itay-grudev/SingleApplication [submodule "qpropgen"] - path = qpropgen + path = 3rdparty/qpropgen url = https://github.com/agateau/qpropgen +[submodule "3rdparty/catch2"] + path = 3rdparty/catch2 + url = https://github.com/catchorg/Catch2 diff --git a/3rdparty/catch2 b/3rdparty/catch2 new file mode 160000 index 0000000..6ccd467 --- /dev/null +++ b/3rdparty/catch2 @@ -0,0 +1 @@ +Subproject commit 6ccd467094973824d89efb16cbc553e279f79823 diff --git a/qpropgen b/3rdparty/qpropgen similarity index 100% rename from qpropgen rename to 3rdparty/qpropgen diff --git a/singleapplication/CMakeLists.txt b/3rdparty/singleapplication/CMakeLists.txt similarity index 93% rename from singleapplication/CMakeLists.txt rename to 3rdparty/singleapplication/CMakeLists.txt index 2bc676b..19f5be8 100644 --- a/singleapplication/CMakeLists.txt +++ b/3rdparty/singleapplication/CMakeLists.txt @@ -1,13 +1,12 @@ project(singleapplication) -# Dependencies find_package(Qt5 CONFIG REQUIRED Core Widgets Network) -# Sources +set(CMAKE_AUTOMOC ON) + set(singleapplication_SRCS src/singleapplication.cpp) -# Building add_library(singleapplication STATIC ${singleapplication_SRCS}) target_compile_definitions(singleapplication diff --git a/singleapplication/src b/3rdparty/singleapplication/src similarity index 100% rename from singleapplication/src rename to 3rdparty/singleapplication/src diff --git a/CHANGELOG.md b/CHANGELOG.md index af37ba9..7053017 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## 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. + ## 1.0.0 - 2018-12-30 First release diff --git a/CMakeLists.txt b/CMakeLists.txt index 477ae8d..43bdb62 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,15 +5,17 @@ project(nanonote ) # Dependencies -find_package(Qt5 CONFIG REQUIRED Core Widgets) -include(qpropgen/cmake/qpropgen.cmake) +find_package(Qt5 CONFIG REQUIRED Core Widgets Test) +include(3rdparty/qpropgen/cmake/qpropgen.cmake) +add_subdirectory(3rdparty/singleapplication) +add_subdirectory(3rdparty/catch2) -set(CMAKE_AUTOMOC ON) -set(CMAKE_AUTOUIC ON) -set(CMAKE_INSTALL_RPATH_USE_LINK_PATH ON) +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Woverloaded-virtual") +set(CMAKE_CXX_STANDARD 17) +enable_testing() -add_subdirectory(singleapplication) add_subdirectory(src) +add_subdirectory(tests) # Packaging set(PROJECT_URL "https://github.com/agateau/nanonote") diff --git a/RELEASE_CHECK_LIST.md b/RELEASE_CHECK_LIST.md new file mode 100644 index 0000000..694bf9a --- /dev/null +++ b/RELEASE_CHECK_LIST.md @@ -0,0 +1,41 @@ +Check working tree is up to date and clean: + + git checkout dev + git pull + git status + +Update CHANGELOG.md: + + r!git log --pretty=format:'- \%s (\%an)' x.y.z-1..HEAD + +Bump version number in CMakeLists.txt + +Commit and push + +Build packages: + + docker run -v $PWD:/root/nanonote nanonote:1 /root/nanonote/ci/build-packages + +Smoke test binary packages + +- Test welcome text is OK +- Test screenshot matches + +Merge dev in master: + + git checkout master + git pull + git merge --no-ff origin/dev + +Create "x.y.z" tag: + + git tag -a x.y.z + +Push: + + git push + git push --tags + +Publish packages on GitHub + +Write blog post diff --git a/ci/Dockerfile b/ci/Dockerfile index 8873b15..76efef3 100644 --- a/ci/Dockerfile +++ b/ci/Dockerfile @@ -2,9 +2,19 @@ FROM ubuntu:18.04 RUN apt-get update \ && apt-get install -y -qq --no-install-recommends \ - qtbase5-dev qt5-default g++ make cmake ninja-build \ - python3 python3-pip python3-setuptools \ - file rpm + cmake \ + dpkg-dev \ + file \ + g++ \ + make \ + ninja-build \ + python3 \ + python3-pip \ + python3-setuptools \ + qt5-default \ + qtbase5-dev \ + rpm \ + xvfb COPY requirements.txt /tmp diff --git a/ci/build-packages b/ci/build-packages index c310499..d46aa38 100755 --- a/ci/build-packages +++ b/ci/build-packages @@ -7,16 +7,17 @@ cd $(dirname $0)/.. SRC_DIR=$PWD DIST_DIR=$PWD/dist -setup_work_dir() { +setup_work_dirs() { cd /tmp cd $(mktemp -d cibuild.XXXXXX) WORK_DIR=$PWD + WORK_SRC_PKG_DIR=$PWD/srcpkg + WORK_BIN_PKG_DIR=$PWD/binpkg + mkdir $WORK_SRC_PKG_DIR $WORK_BIN_PKG_DIR } create_source_package() { - cd $WORK_DIR - mkdir srcpkg - cd srcpkg + cd $WORK_SRC_PKG_DIR cmake -G Ninja $SRC_DIR ninja package_source @@ -24,23 +25,31 @@ create_source_package() { mv $SOURCE_PACKAGE $DIST_DIR } -create_binary_packages() { - cd $WORK_DIR - mkdir binpkg - cd binpkg - tar xf $DIST_DIR/$SOURCE_PACKAGE - cd $(echo $SOURCE_PACKAGE | sed s/.tar.bz2$//) +unpack_source_package() { + cd $WORK_BIN_PKG_DIR + tar -xf $DIST_DIR/$SOURCE_PACKAGE --strip-components 1 +} + +build_and_test() { + cd $WORK_BIN_PKG_DIR mkdir build cd build cmake -G Ninja .. ninja + xvfb-run ninja test +} + +create_binary_packages() { + cd $WORK_BIN_PKG_DIR/build ninja package mv *.deb *.rpm $DIST_DIR } rm -rf $DIST_DIR mkdir $DIST_DIR -setup_work_dir +setup_work_dirs create_source_package +unpack_source_package +build_and_test create_binary_packages rm -rf $WORK_DIR diff --git a/ci/docker-build b/ci/docker-build index 37e567f..0fb33ab 100755 --- a/ci/docker-build +++ b/ci/docker-build @@ -1,5 +1,5 @@ #!/bin/sh set -ev cd $(dirname $0) -cp ../qpropgen/requirements.txt . +cp ../3rdparty/qpropgen/requirements.txt . docker build -t nanonote:1 . diff --git a/screenshot.png b/screenshot.png index d74365f..6f7a667 100644 Binary files a/screenshot.png and b/screenshot.png differ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f38ba13..398822a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -4,7 +4,6 @@ configure_file( ${CMAKE_CURRENT_BINARY_DIR}/Config.h) set(nanonote_SRCS - main.cpp IndentExtension.cpp LinkSyntaxHighlighter.cpp LinkExtension.cpp @@ -22,19 +21,26 @@ qpropgen(QPROPGEN_SRCS ) # Building -include_directories( +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_INSTALL_RPATH_USE_LINK_PATH ON) + +# Lib +add_library(nanonotelib STATIC ${nanonote_SRCS} ${nanonote_RESOURCES} ${QPROPGEN_SRCS}) +target_include_directories(nanonotelib PUBLIC ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR} ) +target_link_libraries(nanonotelib + Qt5::Core + Qt5::Widgets +) -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Woverloaded-virtual") -set(CMAKE_CXX_STANDARD 17) -add_executable(nanonote ${nanonote_SRCS} ${nanonote_RESOURCES} ${QPROPGEN_SRCS}) - +# Main +add_executable(nanonote main.cpp) target_link_libraries(nanonote + nanonotelib singleapplication - Qt5::Core - Qt5::Widgets ) # Install diff --git a/src/IndentExtension.cpp b/src/IndentExtension.cpp index 29fef75..b4c53fb 100644 --- a/src/IndentExtension.cpp +++ b/src/IndentExtension.cpp @@ -73,7 +73,11 @@ void IndentExtension::aboutToShowContextMenu(QMenu *menu, const QPoint &/*pos*/) bool IndentExtension::keyPress(QKeyEvent *event) { if (event->key() == Qt::Key_Tab && event->modifiers() == 0) { - insertIndentation(); + onTabPressed(); + return true; + } + if (event->key() == Qt::Key_Backtab) { + processSelection(unindentLine); return true; } if (event->key() == Qt::Key_Enter || event->key() == Qt::Key_Return) { @@ -106,7 +110,8 @@ bool IndentExtension::canRemoveIndentation() const void IndentExtension::insertIndentation() { auto cursor = mTextEdit->textCursor(); - indentLine(cursor); + int count = INDENT_SIZE - (cursor.columnNumber() % INDENT_SIZE); + cursor.insertText(QString(count, ' ')); } void IndentExtension::processSelection(ProcessSelectionCallback callback) @@ -136,6 +141,16 @@ void IndentExtension::processSelection(ProcessSelectionCallback callback) editCursor.endEditBlock(); } +void IndentExtension::onTabPressed() +{ + auto cursor = mTextEdit->textCursor(); + if (cursor.selectedText().contains(QChar::ParagraphSeparator)) { + processSelection(indentLine); + } else { + insertIndentation(); + } +} + void IndentExtension::removeIndentation() { auto cursor = mTextEdit->textCursor(); diff --git a/src/IndentExtension.h b/src/IndentExtension.h index e90c00d..bb6e946 100644 --- a/src/IndentExtension.h +++ b/src/IndentExtension.h @@ -20,6 +20,7 @@ class IndentExtension : public TextEditExtension void removeIndentation(); void insertIndentedLine(); void processSelection(ProcessSelectionCallback callback); + void onTabPressed(); QAction *mIndentAction; QAction *mUnindentAction; diff --git a/src/main.cpp b/src/main.cpp index 88b0318..8ff77cc 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -9,6 +9,7 @@ int main(int argc, char *argv[]) { SingleApplication app(argc, argv); + Q_INIT_RESOURCE(nanonote); app.setOrganizationName("agateau.com"); app.setApplicationName("nanonote"); app.setApplicationVersion(NANONOTE_VERSION); diff --git a/src/welcome.txt b/src/welcome.txt index c412a94..e76fcb9 100644 --- a/src/welcome.txt +++ b/src/welcome.txt @@ -17,6 +17,6 @@ It also has a few handy editing features, like auto-bullet lists: You can also open urls using Control + click. You can try clicking on this one for example: https://github.com/agateau/nanonote. -Finally, you can indent selected lines with Ctrl+I and unindent with Ctrl+U. +Finally, you can indent selected lines with Tab or Ctrl+I and unindent them with Shift+Tab or Ctrl+U. -That's all there is to say, now you can erase this text and start taking notes! \ No newline at end of file +That's all there is to say, now you can erase this text and start taking notes! diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..af4dbd5 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,14 @@ +add_executable(tests + tests.cpp + IndentExtensionTest.cpp + LinkSyntaxHighlighterTest.cpp + TestUtils.cpp +) + +target_link_libraries(tests + nanonotelib + Qt5::Test + Catch2::Catch2 +) + +add_test(NAME tests COMMAND tests) diff --git a/tests/IndentExtensionTest.cpp b/tests/IndentExtensionTest.cpp new file mode 100644 index 0000000..77ce212 --- /dev/null +++ b/tests/IndentExtensionTest.cpp @@ -0,0 +1,46 @@ +#include "IndentExtension.h" + +#include +#include +#include + +#include + +#include "TestUtils.h" + +TEST_CASE("textedit") { + QMainWindow window; + TextEdit *edit = new TextEdit; + window.setCentralWidget(edit); + edit->addExtension(new IndentExtension(edit)); + SECTION("indent insert spaces") { + edit->setPlainText("Hello"); + + SECTION("indent from beginning of line") { + QTest::keyClick(edit, Qt::Key_Tab); + REQUIRE(edit->toPlainText() == QString(" Hello")); + } + + SECTION("indent from middle of word") { + QTest::keyClick(edit, Qt::Key_Right); + QTest::keyClick(edit, Qt::Key_Tab); + REQUIRE(edit->toPlainText() == QString("H ello")); + } + } + + SECTION("indent whole lines") { + edit->setPlainText("1\n2\n3\n"); + edit->moveCursor(QTextCursor::Down, QTextCursor::KeepAnchor); + edit->moveCursor(QTextCursor::Down, QTextCursor::KeepAnchor); + QTest::keyClick(edit, Qt::Key_Tab); + REQUIRE(edit->toPlainText() == QString(" 1\n 2\n3\n")); + } + + SECTION("unindent whole lines") { + edit->setPlainText(" 1\n 2\n3\n"); + edit->moveCursor(QTextCursor::Down, QTextCursor::KeepAnchor); + edit->moveCursor(QTextCursor::Down, QTextCursor::KeepAnchor); + QTest::keyClick(edit, Qt::Key_Backtab); + REQUIRE(edit->toPlainText() == QString("1\n2\n3\n")); + } +} diff --git a/tests/LinkSyntaxHighlighterTest.cpp b/tests/LinkSyntaxHighlighterTest.cpp new file mode 100644 index 0000000..0655fe9 --- /dev/null +++ b/tests/LinkSyntaxHighlighterTest.cpp @@ -0,0 +1,17 @@ +#include "LinkSyntaxHighlighter.h" + +#include + +TEST_CASE("getLinkAt") { + QString text = "link to http://google.fr. The end."; + QUrl expected("http://google.fr"); + SECTION("before link") { + REQUIRE(LinkSyntaxHighlighter::getLinkAt(text, 0) == QUrl()); + } + SECTION("at link") { + REQUIRE(LinkSyntaxHighlighter::getLinkAt(text, 10) == expected); + } + SECTION("after link") { + REQUIRE(LinkSyntaxHighlighter::getLinkAt(text, 25) == QUrl()); + } +} diff --git a/tests/TestUtils.cpp b/tests/TestUtils.cpp new file mode 100644 index 0000000..8f1a820 --- /dev/null +++ b/tests/TestUtils.cpp @@ -0,0 +1,7 @@ +#include "TestUtils.h" + +std::ostream &operator<<(std::ostream &ostr, const QString &str) +{ + ostr << '"' << str.toStdString() << '"'; + return ostr; +} diff --git a/tests/TestUtils.h b/tests/TestUtils.h new file mode 100644 index 0000000..dbf22f3 --- /dev/null +++ b/tests/TestUtils.h @@ -0,0 +1,11 @@ +#ifndef TESTUTILS_H +#define TESTUTILS_H + +#include + +#include + +// Let Catch know how to print a QString +std::ostream &operator<<(std::ostream &ostr, const QString &str); + +#endif // TESTUTILS_H diff --git a/tests/tests.cpp b/tests/tests.cpp new file mode 100644 index 0000000..eeccd96 --- /dev/null +++ b/tests/tests.cpp @@ -0,0 +1,12 @@ +#define CATCH_CONFIG_RUNNER +#include + +#include +#include + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + QTEST_SET_MAIN_SOURCE_PATH + return Catch::Session().run(argc, argv); +}