├── .clang-format ├── .github └── workflows │ ├── ci.yml │ ├── linux-release.yml │ ├── source-release.yml │ └── windows-release.yml ├── .gitignore ├── .gitmodules ├── CMakeLists.txt ├── DeepTags.qrc ├── LICENSE ├── README.md ├── Screenshot.png ├── deeptags.png ├── images ├── DeepTags.ico ├── add.png ├── addFile.png ├── addFolder.png ├── all_notes.png ├── arrowdown.png ├── arrowup.png ├── collapse.png ├── color_blue.png ├── color_cyan.png ├── color_green.png ├── color_magenta.png ├── color_orange.png ├── color_red.png ├── color_yellow.png ├── delete.png ├── expand.png ├── favorite.png ├── favpin.png ├── icon128.png ├── icon256.png ├── newFile.png ├── notebook.png ├── pin.png ├── quit.png ├── spinner.gif ├── star.png ├── trash.png └── untagged.png ├── locale ├── DeepTags_fr.qm └── DeepTags_fr.ts ├── packaging ├── archlinux │ └── PKGBUILD ├── linux-build.sh ├── package-deb.sh └── resources │ ├── deeptags.appdata.xml │ ├── deeptags.desktop │ └── icons │ └── hicolor │ ├── 128x128 │ └── apps │ │ └── deeptags.png │ ├── 16x16 │ └── apps │ │ └── deeptags.png │ ├── 192x192 │ └── apps │ │ └── deeptags.png │ ├── 256x256 │ └── apps │ │ └── deeptags.png │ ├── 32x32 │ └── apps │ │ └── deeptags.png │ ├── 384x384 │ └── apps │ │ └── deeptags.png │ ├── 48x48 │ └── apps │ │ └── deeptags.png │ ├── 512x512 │ └── apps │ │ └── deeptags.png │ ├── 64x64 │ └── apps │ │ └── deeptags.png │ ├── 80x80 │ └── apps │ │ └── deeptags.png │ ├── 8x8 │ └── apps │ │ └── deeptags.png │ ├── 96x96 │ └── apps │ │ └── deeptags.png │ └── scalable │ └── apps │ └── deeptags.svg ├── src ├── Benchmark.h ├── DataDirectoryCentralWidget.cpp ├── DataDirectoryCentralWidget.h ├── DataDirectoryCentralWidget.ui ├── DataDirectoryDialog.cpp ├── DataDirectoryDialog.h ├── DataDirectoryDialog.ui ├── Document.cpp ├── Document.h ├── DocumentContentView.cpp ├── DocumentContentView.h ├── DocumentInfoDialog.cpp ├── DocumentInfoDialog.h ├── DocumentInfoDialog.ui ├── DocumentListDelegate.cpp ├── DocumentListDelegate.h ├── DocumentListItem.cpp ├── DocumentListItem.h ├── DocumentListModel.cpp ├── DocumentListModel.h ├── DocumentUtils.cpp ├── DocumentUtils.h ├── DocumentsListView.cpp ├── DocumentsListView.h ├── ExternalReadersCentralWidget.cpp ├── ExternalReadersCentralWidget.h ├── ExternalReadersCentralWidget.ui ├── ExternalReadersDialog.cpp ├── ExternalReadersDialog.h ├── ExternalReadersDialog.ui ├── MainWindow.cpp ├── MainWindow.h ├── MainWindow.ui ├── Settings.cpp ├── Settings.h ├── SettingsDialog.cpp ├── SettingsDialog.h ├── SettingsDialog.ui ├── TagTreeDelegate.cpp ├── TagTreeDelegate.h ├── TagTreeItem.cpp ├── TagTreeItem.h ├── TagTreeModel.cpp ├── TagTreeModel.h ├── TagsTreeView.cpp ├── TagsTreeView.h └── main.cpp ├── tag_hierarchy.png └── tests └── DocumentUtilsTest.cpp /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | BasedOnStyle: Google 4 | AccessModifierOffset: -4 5 | AlignAfterOpenBracket: Align 6 | AlignConsecutiveMacros: true 7 | AlignConsecutiveAssignments: true 8 | AlignConsecutiveDeclarations: false 9 | AlignEscapedNewlines: Left 10 | AlignOperands: true 11 | AlignTrailingComments: true 12 | AllowAllArgumentsOnNextLine: true 13 | AllowAllConstructorInitializersOnNextLine: true 14 | AllowAllParametersOfDeclarationOnNextLine: false 15 | AllowShortBlocksOnASingleLine: false 16 | AllowShortCaseLabelsOnASingleLine: false 17 | AllowShortFunctionsOnASingleLine: Inline 18 | AllowShortLambdasOnASingleLine: All 19 | AllowShortIfStatementsOnASingleLine: true 20 | AllowShortLoopsOnASingleLine: true 21 | AlwaysBreakAfterDefinitionReturnType: None 22 | AlwaysBreakAfterReturnType: None 23 | AlwaysBreakBeforeMultilineStrings: false 24 | AlwaysBreakTemplateDeclarations: Yes 25 | BinPackArguments: true 26 | BinPackParameters: true 27 | BraceWrapping: 28 | AfterCaseLabel: false 29 | AfterClass: true 30 | AfterControlStatement: false 31 | AfterEnum: false 32 | AfterFunction: false 33 | AfterNamespace: false 34 | AfterObjCDeclaration: false 35 | AfterStruct: true 36 | AfterUnion: false 37 | AfterExternBlock: false 38 | BeforeCatch: true 39 | BeforeElse: true 40 | IndentBraces: false 41 | SplitEmptyFunction: true 42 | SplitEmptyRecord: true 43 | SplitEmptyNamespace: true 44 | BreakBeforeBinaryOperators: None 45 | BreakBeforeBraces: Attach 46 | BreakBeforeInheritanceComma: false 47 | BreakInheritanceList: BeforeColon 48 | BreakBeforeTernaryOperators: true 49 | BreakConstructorInitializersBeforeComma: false 50 | BreakConstructorInitializers: BeforeColon 51 | BreakAfterJavaFieldAnnotations: true 52 | BreakStringLiterals: true 53 | ColumnLimit: 100 54 | CommentPragmas: '^ IWYU pragma:' 55 | CompactNamespaces: true 56 | ConstructorInitializerAllOnOneLineOrOnePerLine: false 57 | ConstructorInitializerIndentWidth: 4 58 | ContinuationIndentWidth: 4 59 | Cpp11BracedListStyle: false 60 | DerivePointerAlignment: false 61 | PointerAlignment: Left 62 | DisableFormat: false 63 | ExperimentalAutoDetectBinPacking: false 64 | FixNamespaceComments: true 65 | ForEachMacros: 66 | - foreach 67 | - Q_FOREACH 68 | - BOOST_FOREACH 69 | IncludeBlocks: Merge 70 | IncludeCategories: 71 | - Regex: '^' 72 | Priority: 2 73 | - Regex: '^<.*\.h>' 74 | Priority: 1 75 | - Regex: '^<.*' 76 | Priority: 2 77 | - Regex: '.*' 78 | Priority: 3 79 | IncludeIsMainRegex: '([-_](test|unittest))?$' 80 | IndentCaseLabels: false 81 | IndentPPDirectives: BeforeHash 82 | IndentWidth: 4 83 | IndentWrappedFunctionNames: false 84 | JavaScriptQuotes: Leave 85 | JavaScriptWrapImports: true 86 | KeepEmptyLinesAtTheStartOfBlocks: false 87 | MacroBlockBegin: '' 88 | MacroBlockEnd: '' 89 | MaxEmptyLinesToKeep: 2 90 | NamespaceIndentation: Inner 91 | ObjCBinPackProtocolList: Never 92 | ObjCBlockIndentWidth: 2 93 | ObjCSpaceAfterProperty: false 94 | ObjCSpaceBeforeProtocolList: true 95 | PenaltyBreakAssignment: 2 96 | PenaltyBreakBeforeFirstCallParameter: 1 97 | PenaltyBreakComment: 300 98 | PenaltyBreakFirstLessLess: 120 99 | PenaltyBreakString: 1000 100 | PenaltyBreakTemplateDeclaration: 10 101 | PenaltyExcessCharacter: 1000000 102 | PenaltyReturnTypeOnItsOwnLine: 200 103 | RawStringFormats: 104 | - Language: Cpp 105 | Delimiters: 106 | - cc 107 | - CC 108 | - cpp 109 | - Cpp 110 | - CPP 111 | - 'c++' 112 | - 'C++' 113 | CanonicalDelimiter: '' 114 | BasedOnStyle: google 115 | - Language: TextProto 116 | Delimiters: 117 | - pb 118 | - PB 119 | - proto 120 | - PROTO 121 | EnclosingFunctions: 122 | - EqualsProto 123 | - EquivToProto 124 | - PARSE_PARTIAL_TEXT_PROTO 125 | - PARSE_TEST_PROTO 126 | - PARSE_TEXT_PROTO 127 | - ParseTextOrDie 128 | - ParseTextProtoOrDie 129 | CanonicalDelimiter: '' 130 | BasedOnStyle: google 131 | ReflowComments: true 132 | SortIncludes: true 133 | SortUsingDeclarations: false 134 | SpaceAfterCStyleCast: true 135 | SpaceAfterLogicalNot: false 136 | SpaceAfterTemplateKeyword: true 137 | SpaceBeforeAssignmentOperators: true 138 | SpaceBeforeCpp11BracedList: false 139 | SpaceBeforeCtorInitializerColon: true 140 | SpaceBeforeInheritanceColon: true 141 | SpaceBeforeParens: ControlStatements 142 | SpaceBeforeRangeBasedForLoopColon: true 143 | SpaceInEmptyParentheses: false 144 | SpacesBeforeTrailingComments: 3 145 | SpacesInAngles: false 146 | SpacesInContainerLiterals: true 147 | SpacesInCStyleCastParentheses: false 148 | SpacesInParentheses: false 149 | SpacesInSquareBrackets: false 150 | Standard: Auto 151 | StatementMacros: 152 | - Q_UNUSED 153 | - QT_REQUIRE_VERSION 154 | TabWidth: 4 155 | UseTab: Never 156 | --- 157 | 158 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Running Tests 2 | 3 | 4 | on: 5 | workflow_dispatch: 6 | push: 7 | branches: [ "master" ] 8 | pull_request: 9 | branches: [ "master" ] 10 | 11 | 12 | env: 13 | BUILD_TYPE: Debug 14 | BUILD_DIR: ${{github.workspace}}/build 15 | QT_LOC: ${{github.workspace}}/Qt 16 | QT_VERSION: 5.12.2 17 | QT_ARCH: gcc_64 18 | 19 | 20 | jobs: 21 | build: 22 | runs-on: ubuntu-20.04 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | with: 27 | submodules: recursive 28 | 29 | - name: Set reusable strings 30 | id: strings 31 | shell: bash 32 | run: | 33 | echo "build-output-dir=${{ github.workspace }}/build" >> "$GITHUB_OUTPUT" 34 | 35 | 36 | - name: Cache Qt 37 | id: cache-qt 38 | uses: actions/cache@v4 39 | with: 40 | path: ${{env.QT_LOC}} 41 | key: ${{runner.os}}-${{env.QT_ARCH}}-QtCache-${{env.QT_VERSION}} 42 | 43 | - name: Install Qt 44 | uses: jurplel/install-qt-action@v3 45 | with: 46 | version: 5.15.2 47 | dir: ${{env.QT_LOC}} 48 | arch: gcc_64 49 | aqtversion: '==3.1.11' 50 | setup-python: false 51 | cache: true 52 | 53 | 54 | - name: Configure CMake for testing 55 | run: cmake -B ${{env.BUILD_DIR}} -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DBENCHMARK=ON -DTEST=ON 56 | 57 | 58 | - name: Build for testing 59 | run: | 60 | cmake --build ${{env.BUILD_DIR}} --config ${{env.BUILD_TYPE}} --parallel $(nproc) 61 | 62 | 63 | - name: Re-Configure CMake for regular build 64 | run: cmake -B ${{env.BUILD_DIR}} -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DBENCHMARK=ON -DTEST=OFF 65 | 66 | 67 | - name: Launch a regular build 68 | run: cmake --build ${{env.BUILD_DIR}} --config ${{env.BUILD_TYPE}} --parallel $(nproc) 69 | 70 | -------------------------------------------------------------------------------- /.github/workflows/linux-release.yml: -------------------------------------------------------------------------------- 1 | name: Linux Release 2 | 3 | 4 | on: 5 | workflow_dispatch: 6 | 7 | 8 | env: 9 | BUILD_DIR_PATH: ${{github.workspace}}/build 10 | QT_LOC: ${{github.workspace}}/Qt 11 | QT_VERSION: 5.12.2 12 | QT_ARCH: gcc_64 13 | TAG: ${{github.ref_name}} 14 | DEEPTAGS_DIR_NAME: DeepTags_${{ github.ref_name }} 15 | APPIMAGE_FILENAME: DeepTags_${{ github.ref_name }}-x86_64.AppImage 16 | 17 | 18 | jobs: 19 | build: 20 | runs-on: ubuntu-20.04 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | with: 25 | submodules: recursive 26 | 27 | - name: Set reusable strings 28 | id: strings 29 | shell: bash 30 | run: | 31 | echo "build-output-dir=${{ github.workspace }}/build" >> "$GITHUB_OUTPUT" 32 | 33 | 34 | - name: Cache Qt 35 | id: cache-qt 36 | uses: actions/cache@v4 37 | with: 38 | path: ${{env.QT_LOC}} 39 | key: ${{runner.os}}-${{env.QT_ARCH}}-QtCache-${{env.QT_VERSION}} 40 | 41 | - name: Install Qt 42 | uses: jurplel/install-qt-action@v3 43 | with: 44 | version: 5.15.2 45 | dir: ${{env.QT_LOC}} 46 | arch: gcc_64 47 | aqtversion: '==3.1.11' 48 | setup-python: false 49 | cache: true 50 | 51 | 52 | - name: Configure CMake 53 | run: > 54 | cmake -B build/ 55 | -DCMAKE_CXX_COMPILER=g++ 56 | -DCMAKE_C_COMPILER=gcc 57 | -DCMAKE_BUILD_TYPE=Release 58 | -S ${{ github.workspace }} 59 | 60 | 61 | - name: Building 62 | run: cmake --build ${{ steps.strings.outputs.build-output-dir }} --config Release 63 | 64 | 65 | - name: Create .deb file 66 | run: | 67 | ./packaging/package-deb.sh 68 | mv packaging/*.deb . 69 | 70 | 71 | 72 | - name: Setup linuxdeploy 73 | run: | 74 | curl -fLO --retry 10 https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage 75 | chmod +x linuxdeploy-x86_64.AppImage 76 | 77 | 78 | - name: Setup Qt plugin for linuxdeploy 79 | run: | 80 | curl -fLO --retry 10 https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage 81 | chmod +x linuxdeploy-plugin-qt-x86_64.AppImage 82 | 83 | 84 | - name: 'prepare AppImage directory structure' 85 | run: | 86 | ls -l build 87 | ./linuxdeploy-x86_64.AppImage --appdir ${{env.DEEPTAGS_DIR_NAME}}/ 88 | cp packaging/resources/deeptags.desktop ${{env.DEEPTAGS_DIR_NAME}}//usr/share/applications/ 89 | cp -r packaging/resources/icons ${{env.DEEPTAGS_DIR_NAME}}/usr/share/ 90 | cp build/deeptags ${{env.DEEPTAGS_DIR_NAME}}/usr/bin/ 91 | cp deeptags.png ${{env.DEEPTAGS_DIR_NAME}}/ 92 | tree ${{env.DEEPTAGS_DIR_NAME}}/ 93 | 94 | 95 | - name: 'Run linuxdeployqt' 96 | run: | 97 | ./linuxdeploy-x86_64.AppImage --appdir ${{env.DEEPTAGS_DIR_NAME}} --plugin qt --output appimage 98 | mv DeepTags*.AppImage ${{env.APPIMAGE_FILENAME}} 99 | ls -la 100 | 101 | 102 | - name: 'Generate hash for tha Appimage' 103 | run: | 104 | sha256sum ${{env.APPIMAGE_FILENAME}} > ${{env.APPIMAGE_FILENAME}}.sha256sum 105 | ls -l 106 | 107 | 108 | - name: Create release 109 | uses: Roang-zero1/github-create-release-action@v2 110 | env: 111 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 112 | with: 113 | version_regex: ^[[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+ 114 | 115 | 116 | - name: Upload release artifacts 117 | uses: Roang-zero1/github-upload-release-artifacts-action@v2 118 | env: 119 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 120 | with: 121 | args: "DeepTags*.AppImage DeepTags*.AppImage.sha256sum deeptags*amd64.deb" 122 | 123 | 124 | -------------------------------------------------------------------------------- /.github/workflows/source-release.yml: -------------------------------------------------------------------------------- 1 | name: Source Code Release 2 | 3 | 4 | on: 5 | workflow_dispatch: 6 | 7 | 8 | env: 9 | ARCHIVE_FILENAME: DeepTags_${{ github.ref_name }}.tar.xz 10 | 11 | 12 | jobs: 13 | archive: 14 | runs-on: ubuntu-20.04 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | submodules: recursive 20 | 21 | 22 | - name: Remove unnecessary files 23 | shell: bash 24 | run: | 25 | rm -rf .git .github .gitmodules .gitignore 26 | rm -rf src/3rdParty/*/{.git,.github,.gitignore} 27 | 28 | 29 | - name: Build Archive 30 | run: | 31 | tar cJvf ../${{env.ARCHIVE_FILENAME}} . 32 | mv ../${{env.ARCHIVE_FILENAME}} . 33 | 34 | 35 | - name: Generate Hashes 36 | run: | 37 | md5sum ${{env.ARCHIVE_FILENAME}} > ${{env.ARCHIVE_FILENAME}}.md5sum 38 | sha256sum ${{env.ARCHIVE_FILENAME}} > ${{env.ARCHIVE_FILENAME}}.sha256sum 39 | ls -l 40 | 41 | 42 | - name: Create release 43 | uses: Roang-zero1/github-create-release-action@v2 44 | env: 45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 46 | with: 47 | version_regex: ^[[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+ 48 | 49 | 50 | - name: Upload release artifacts 51 | uses: Roang-zero1/github-upload-release-artifacts-action@v2 52 | env: 53 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 54 | with: 55 | args: "*.tar.xz *.md5sum *.sha256sum" 56 | 57 | -------------------------------------------------------------------------------- /.github/workflows/windows-release.yml: -------------------------------------------------------------------------------- 1 | name: Windows Release 2 | 3 | 4 | on: 5 | workflow_dispatch: 6 | 7 | 8 | env: 9 | BUILD_DIR_PATH: ${{github.workspace}}/build 10 | QT_VERSION: 5.12.2 11 | QT_ARCH: win64_msvc2019_64 12 | MSVC_ARCH: x64 13 | 14 | 15 | jobs: 16 | build: 17 | runs-on: windows-2019 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | with: 22 | submodules: recursive 23 | 24 | 25 | - name: Setup MSVC 26 | uses: ilammy/msvc-dev-cmd@v1 27 | with: 28 | arch: ${{env.MSVC_ARCH}} 29 | 30 | 31 | - name: Cache Qt 32 | id: cache-qt 33 | uses: actions/cache@v4 34 | with: 35 | path: ../Qt 36 | key: ${{runner.os}}-${{env.QT_ARCH}}-QtCache-${{env.QT_VERSION}} 37 | 38 | - name: Install Qt 39 | uses: jurplel/install-qt-action@v3 40 | with: 41 | version: 5.15.2 42 | arch: win64_msvc2019_64 43 | aqtversion: '==2.0.0' 44 | setup-python: false 45 | cache: true 46 | 47 | 48 | - name: Configuration and Build 49 | run: | 50 | cmake -B ${{env.BUILD_DIR_PATH}} -DCMAKE_CXX_COMPILER=cl 51 | cmake --build ${{env.BUILD_DIR_PATH}} --config Release 52 | 53 | 54 | - name: create the dir and copy the binary to it 55 | shell: bash 56 | run: | 57 | mkdir -p DeepTags 58 | mkdir -p DeepTags/translations 59 | cp build/Release/deeptags DeepTags/ 60 | cp deeptags.png DeepTags/ 61 | cp LICENSE DeepTags/ 62 | cp images/DeepTags.ico DeepTags/ 63 | cp locale/*.ts DeepTags/translations/ 64 | ls -l DeepTags 65 | 66 | - name: execute windeployqt 67 | shell: bash 68 | run: | 69 | cd DeepTags 70 | windeployqt.exe . 71 | cd .. 72 | 73 | 74 | - name: copy runtime dlls 75 | run: | 76 | Copy-Item $env:VCToolsRedistDir\x64\Microsoft.VC142.CRT\msvcp140.dll DeepTags 77 | Copy-Item $env:VCToolsRedistDir\x64\Microsoft.VC142.CRT\msvcp140_1.dll DeepTags 78 | Copy-Item $env:VCToolsRedistDir\x64\Microsoft.VC142.CRT\vcruntime140.dll DeepTags 79 | Copy-Item $env:VCToolsRedistDir\x64\Microsoft.VC142.CRT\vcruntime140_1.dll DeepTags 80 | dir DeepTags 81 | 82 | 83 | - name: ZIP 84 | run: | 85 | 7z a DeepTags-${{github.ref_name}}-win-x64.zip DeepTags 86 | 87 | 88 | - uses: actions/upload-artifact@v4 89 | with: 90 | name: DeepTags-${{github.ref_name}}-win-x64.zip 91 | path: DeepTags-${{github.ref_name}}-win-x64.zip 92 | retention-days: 1 93 | 94 | 95 | 96 | release: 97 | needs: build 98 | runs-on: ubuntu-latest 99 | 100 | steps: 101 | 102 | - uses: actions/download-artifact@v4 103 | with: 104 | name: DeepTags-${{github.ref_name}}-win-x64.zip 105 | 106 | 107 | - name: Create release 108 | uses: Roang-zero1/github-create-release-action@v3 109 | env: 110 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 111 | with: 112 | version_regex: ^[[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+ 113 | 114 | 115 | - name: Upload deb file 116 | uses: Roang-zero1/github-upload-release-artifacts-action@v3 117 | env: 118 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 119 | with: 120 | args: DeepTags-${{github.ref_name}}-win-x64.zip 121 | 122 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | .cache 3 | *.user 4 | compile_commands.json 5 | *.autosave 6 | packaging/deeptags* 7 | *.AppImage 8 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/3rdParty/QBreeze"] 2 | path = src/3rdParty/QBreeze 3 | url = https://github.com/SZinedine/QBreeze 4 | [submodule "src/3rdParty/qmarkdowntextedit"] 5 | path = src/3rdParty/qmarkdowntextedit 6 | url = https://github.com/pbek/qmarkdowntextedit 7 | [submodule "src/3rdParty/SingleApplication"] 8 | path = src/3rdParty/SingleApplication 9 | url = https://github.com/itay-grudev/SingleApplication 10 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16) 2 | 3 | project(deeptags LANGUAGES CXX VERSION 0.8.0) 4 | add_definitions(-DDEEPTAGS_VERSION="${CMAKE_PROJECT_VERSION}") 5 | set(CMAKE_CXX_STANDARD 17) 6 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 7 | set(CMAKE_AUTOUIC ON) 8 | set(CMAKE_AUTOMOC ON) 9 | set(CMAKE_AUTORCC ON) 10 | 11 | #set(CMAKE_UNITY_BUILD TRUE) 12 | 13 | find_package(QT NAMES Qt5 REQUIRED COMPONENTS Widgets Concurrent) 14 | find_package(Qt5 REQUIRED COMPONENTS Widgets Concurrent Test) 15 | 16 | set(PROJECT_SOURCES 17 | # source files 18 | src/MainWindow.cpp 19 | src/MainWindow.h 20 | src/DocumentContentView.cpp 21 | src/DocumentContentView.h 22 | src/Document.cpp 23 | src/Document.h 24 | src/DocumentUtils.cpp 25 | src/DocumentUtils.h 26 | src/TagsTreeView.cpp 27 | src/TagsTreeView.h 28 | src/TagTreeItem.cpp 29 | src/TagTreeItem.h 30 | src/TagTreeModel.cpp 31 | src/TagTreeModel.h 32 | src/TagTreeDelegate.cpp 33 | src/TagTreeDelegate.h 34 | src/DocumentsListView.cpp 35 | src/DocumentsListView.h 36 | src/DocumentListItem.cpp 37 | src/DocumentListItem.h 38 | src/DocumentListModel.cpp 39 | src/DocumentListModel.h 40 | src/DocumentListDelegate.cpp 41 | src/DocumentListDelegate.h 42 | src/Settings.cpp 43 | src/Settings.h 44 | src/SettingsDialog.cpp 45 | src/SettingsDialog.h 46 | src/DocumentInfoDialog.cpp 47 | src/DocumentInfoDialog.h 48 | src/DataDirectoryCentralWidget.cpp 49 | src/DataDirectoryCentralWidget.h 50 | src/DataDirectoryDialog.cpp 51 | src/DataDirectoryDialog.h 52 | src/ExternalReadersCentralWidget.cpp 53 | src/ExternalReadersCentralWidget.h 54 | src/ExternalReadersDialog.cpp 55 | src/ExternalReadersDialog.h 56 | 57 | # UI files 58 | src/MainWindow.ui 59 | src/DocumentInfoDialog.ui 60 | src/DataDirectoryCentralWidget.ui 61 | src/DataDirectoryDialog.ui 62 | src/ExternalReadersCentralWidget.ui 63 | src/ExternalReadersDialog.ui 64 | src/SettingsDialog.ui 65 | 66 | # Resource files 67 | DeepTags.qrc 68 | src/3rdParty/QBreeze/qbreeze.qrc 69 | ) 70 | 71 | option(TEST "TEST" OFF) 72 | if (TEST) 73 | message("Building tests") 74 | list(APPEND PROJECT_SOURCES tests/DocumentUtilsTest.cpp) 75 | else() 76 | list(APPEND PROJECT_SOURCES src/main.cpp) 77 | endif() 78 | 79 | include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src) 80 | 81 | set(QAPPLICATION_CLASS QApplication) 82 | add_subdirectory(src/3rdParty/SingleApplication) 83 | # add_subdirectory(src/3rdParty/qmarkdowntextedit) 84 | 85 | list(APPEND PROJECT_SOURCES 86 | src/3rdParty/qmarkdowntextedit/markdownhighlighter.cpp 87 | src/3rdParty/qmarkdowntextedit/markdownhighlighter.h 88 | src/3rdParty/qmarkdowntextedit/qownlanguagedata.cpp 89 | src/3rdParty/qmarkdowntextedit/qownlanguagedata.h 90 | ) 91 | 92 | add_executable(deeptags ${PROJECT_SOURCES}) 93 | 94 | target_link_libraries(deeptags 95 | PRIVATE Qt5::Widgets 96 | PRIVATE Qt5::Concurrent 97 | PRIVATE SingleApplication::SingleApplication 98 | ) 99 | 100 | target_compile_options(deeptags PRIVATE 101 | $<$,$,$>: 102 | -Wall -Wextra> 103 | $<$: 104 | /W4>) 105 | 106 | 107 | if (TEST) 108 | target_link_libraries(deeptags PRIVATE Qt5::Test) 109 | endif() 110 | 111 | 112 | # cmake -DBENCHMARK=ON 113 | option(BENCHMARKS "BENCHMARKS" OFF) 114 | if (BENCHMARK) 115 | message("Building with benchmarks") 116 | add_definitions(-DBENCHMARKS) 117 | endif() 118 | 119 | 120 | set(BUNDLE_ID_OPTION MACOSX_BUNDLE_GUI_IDENTIFIER com.example.deeptags) 121 | 122 | set_target_properties(deeptags PROPERTIES 123 | ${BUNDLE_ID_OPTION} 124 | MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION} 125 | MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR} 126 | MACOSX_BUNDLE TRUE 127 | WIN32_EXECUTABLE TRUE 128 | ) 129 | 130 | include(GNUInstallDirs) 131 | install(TARGETS deeptags 132 | BUNDLE DESTINATION . 133 | LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} 134 | RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} 135 | ) 136 | -------------------------------------------------------------------------------- /DeepTags.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | images/icon128.png 4 | images/icon256.png 5 | images/collapse.png 6 | images/expand.png 7 | images/addFile.png 8 | images/addFolder.png 9 | images/newFile.png 10 | images/favorite.png 11 | images/pin.png 12 | images/favpin.png 13 | images/quit.png 14 | images/spinner.gif 15 | images/add.png 16 | images/delete.png 17 | images/arrowup.png 18 | images/arrowdown.png 19 | images/all_notes.png 20 | images/notebook.png 21 | images/star.png 22 | images/trash.png 23 | images/untagged.png 24 | images/color_red.png 25 | images/color_green.png 26 | images/color_blue.png 27 | images/color_yellow.png 28 | images/color_magenta.png 29 | images/color_cyan.png 30 | images/color_orange.png 31 | 32 | locale/DeepTags_fr.qm 33 | 34 | 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | DeepTags 3 |
4 | 5 |

DeepTags

6 | 7 | 8 | 9 | 10 | 11 | **DeepTags** is a Markdown notes manager that organizes notes according to tags. 12 | 13 | DeepTags supports nested tags and offers simple ways to edit them, for example by dragging and dropping a tag on a note. These notes could be read either with the integrated editor or with one or multiple third party markdown editors installed on your system. Make sure to add them into the the app from `Edit -> Markdown Readers` 14 | 15 | ![Screenshot of DeepTags on a Linux machine running plasma 5](Screenshot.png) 16 | 17 | 18 | ## Features 19 | 20 | * **Nested tags**: You can create a hierarchy of tags to organize your notes in a tree structure. For example, the representation of the nested tag: `places/africa/algeria`, would be: 21 | 22 |

23 | tag hierarchy 24 |

25 | 26 | * **Drag and Drop** a tag into a note to add it to it 27 | * **Search** through your notes by title 28 | * **No-Cloud**: DeepTags is running completely offfline. 29 | * **External Editors**: You can use your favorite markdown editor. 30 | 31 | 32 | ## Dependencies 33 | 34 | - A C++17 compiler 35 | - The Qt framework 5.15 36 | 37 | 38 | ## Downloads 39 | 40 | You can download the latest release [here](https://github.com/SZinedine/DeepTags/releases/latest). 41 | 42 | ### Linux 43 | 44 | #### Building from source 45 | 46 | ```bash 47 | git clone --recursive https://github.com/SZinedine/DeepTags.git 48 | cd DeepTags 49 | mkdir build && cd build 50 | cmake .. && make 51 | ``` 52 | 53 | #### Install on Arch Linux 54 | 55 | ```bash 56 | git clone https://aur.archlinux.org/deeptags.git 57 | cd deeptags 58 | makepkg -sic 59 | ``` 60 | 61 | or 62 | ```bash 63 | yay -S deeptags 64 | ``` 65 | 66 | 67 | ## Credit 68 | 69 | - [QMarkdownTextEdit](https://github.com/pbek/qmarkdowntextedit) 70 | - [SingleApplication](https://github.com/itay-grudev/SingleApplication) 71 | - Yannick Lung's [icons](https://www.iconfinder.com/yanlu) are used throughout the app. 72 | 73 | -------------------------------------------------------------------------------- /Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SZinedine/DeepTags/483c84729c82f784dd10f7a3e74bd2ff18a27a69/Screenshot.png -------------------------------------------------------------------------------- /deeptags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SZinedine/DeepTags/483c84729c82f784dd10f7a3e74bd2ff18a27a69/deeptags.png -------------------------------------------------------------------------------- /images/DeepTags.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SZinedine/DeepTags/483c84729c82f784dd10f7a3e74bd2ff18a27a69/images/DeepTags.ico -------------------------------------------------------------------------------- /images/add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SZinedine/DeepTags/483c84729c82f784dd10f7a3e74bd2ff18a27a69/images/add.png -------------------------------------------------------------------------------- /images/addFile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SZinedine/DeepTags/483c84729c82f784dd10f7a3e74bd2ff18a27a69/images/addFile.png -------------------------------------------------------------------------------- /images/addFolder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SZinedine/DeepTags/483c84729c82f784dd10f7a3e74bd2ff18a27a69/images/addFolder.png -------------------------------------------------------------------------------- /images/all_notes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SZinedine/DeepTags/483c84729c82f784dd10f7a3e74bd2ff18a27a69/images/all_notes.png -------------------------------------------------------------------------------- /images/arrowdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SZinedine/DeepTags/483c84729c82f784dd10f7a3e74bd2ff18a27a69/images/arrowdown.png -------------------------------------------------------------------------------- /images/arrowup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SZinedine/DeepTags/483c84729c82f784dd10f7a3e74bd2ff18a27a69/images/arrowup.png -------------------------------------------------------------------------------- /images/collapse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SZinedine/DeepTags/483c84729c82f784dd10f7a3e74bd2ff18a27a69/images/collapse.png -------------------------------------------------------------------------------- /images/color_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SZinedine/DeepTags/483c84729c82f784dd10f7a3e74bd2ff18a27a69/images/color_blue.png -------------------------------------------------------------------------------- /images/color_cyan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SZinedine/DeepTags/483c84729c82f784dd10f7a3e74bd2ff18a27a69/images/color_cyan.png -------------------------------------------------------------------------------- /images/color_green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SZinedine/DeepTags/483c84729c82f784dd10f7a3e74bd2ff18a27a69/images/color_green.png -------------------------------------------------------------------------------- /images/color_magenta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SZinedine/DeepTags/483c84729c82f784dd10f7a3e74bd2ff18a27a69/images/color_magenta.png -------------------------------------------------------------------------------- /images/color_orange.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SZinedine/DeepTags/483c84729c82f784dd10f7a3e74bd2ff18a27a69/images/color_orange.png -------------------------------------------------------------------------------- /images/color_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SZinedine/DeepTags/483c84729c82f784dd10f7a3e74bd2ff18a27a69/images/color_red.png -------------------------------------------------------------------------------- /images/color_yellow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SZinedine/DeepTags/483c84729c82f784dd10f7a3e74bd2ff18a27a69/images/color_yellow.png -------------------------------------------------------------------------------- /images/delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SZinedine/DeepTags/483c84729c82f784dd10f7a3e74bd2ff18a27a69/images/delete.png -------------------------------------------------------------------------------- /images/expand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SZinedine/DeepTags/483c84729c82f784dd10f7a3e74bd2ff18a27a69/images/expand.png -------------------------------------------------------------------------------- /images/favorite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SZinedine/DeepTags/483c84729c82f784dd10f7a3e74bd2ff18a27a69/images/favorite.png -------------------------------------------------------------------------------- /images/favpin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SZinedine/DeepTags/483c84729c82f784dd10f7a3e74bd2ff18a27a69/images/favpin.png -------------------------------------------------------------------------------- /images/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SZinedine/DeepTags/483c84729c82f784dd10f7a3e74bd2ff18a27a69/images/icon128.png -------------------------------------------------------------------------------- /images/icon256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SZinedine/DeepTags/483c84729c82f784dd10f7a3e74bd2ff18a27a69/images/icon256.png -------------------------------------------------------------------------------- /images/newFile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SZinedine/DeepTags/483c84729c82f784dd10f7a3e74bd2ff18a27a69/images/newFile.png -------------------------------------------------------------------------------- /images/notebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SZinedine/DeepTags/483c84729c82f784dd10f7a3e74bd2ff18a27a69/images/notebook.png -------------------------------------------------------------------------------- /images/pin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SZinedine/DeepTags/483c84729c82f784dd10f7a3e74bd2ff18a27a69/images/pin.png -------------------------------------------------------------------------------- /images/quit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SZinedine/DeepTags/483c84729c82f784dd10f7a3e74bd2ff18a27a69/images/quit.png -------------------------------------------------------------------------------- /images/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SZinedine/DeepTags/483c84729c82f784dd10f7a3e74bd2ff18a27a69/images/spinner.gif -------------------------------------------------------------------------------- /images/star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SZinedine/DeepTags/483c84729c82f784dd10f7a3e74bd2ff18a27a69/images/star.png -------------------------------------------------------------------------------- /images/trash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SZinedine/DeepTags/483c84729c82f784dd10f7a3e74bd2ff18a27a69/images/trash.png -------------------------------------------------------------------------------- /images/untagged.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SZinedine/DeepTags/483c84729c82f784dd10f7a3e74bd2ff18a27a69/images/untagged.png -------------------------------------------------------------------------------- /locale/DeepTags_fr.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SZinedine/DeepTags/483c84729c82f784dd10f7a3e74bd2ff18a27a69/locale/DeepTags_fr.qm -------------------------------------------------------------------------------- /locale/DeepTags_fr.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | DataDirectoryCentralWidget 6 | 7 | 8 | 9 | Data Directory 10 | 11 | 12 | 13 | 14 | Please choose a Data Directory where you want your notes to be saved. If you already have notes in a folder, point it out to import them. 15 | Veuillez choisir un répertoire de données dans lequel vous souhaitez enregistrées enregistrer vos notes. 16 | 17 | 18 | 19 | Browse 20 | Ouvrir 21 | 22 | 23 | 24 | error 25 | 26 | 27 | 28 | 29 | Error while trying to create the Data Directory 30 | Erreur lors de la créaction du Répertoire de Données 31 | 32 | 33 | 34 | DataDirectoryDialog 35 | 36 | 37 | Data Directory 38 | Répertoire de Données 39 | 40 | 41 | 42 | DocumentInfoDialog 43 | 44 | 45 | Document Information 46 | Informations Document 47 | 48 | 49 | 50 | File Path: 51 | Chemin: 52 | 53 | 54 | 55 | Title: 56 | Titre: 57 | 58 | 59 | 60 | Pinned: 61 | Épinglé: 62 | 63 | 64 | 65 | Favorite: 66 | Favori: 67 | 68 | 69 | 70 | Tags: 71 | Tags: 72 | 73 | 74 | 75 | Add a tag 76 | Ajouter un tag 77 | 78 | 79 | 80 | Delete a tag 81 | Supprimer un tag 82 | 83 | 84 | 85 | DocumentsListView 86 | 87 | 88 | Open 89 | Ouvrir 90 | 91 | 92 | 93 | Open With 94 | Ouvrir avec 95 | 96 | 97 | 98 | Edit 99 | Éditer 100 | 101 | 102 | 103 | Add a new tag 104 | Ajouter un nouveau tag 105 | 106 | 107 | 108 | Unpin 109 | Désépingler 110 | 111 | 112 | 113 | Pin To Top 114 | Épingler 115 | 116 | 117 | 118 | Favorite 119 | Favorit 120 | 121 | 122 | 123 | Copy path 124 | Copier le chemin 125 | 126 | 127 | 128 | Restore 129 | Restaurer 130 | 131 | 132 | 133 | 134 | Delete Permanently 135 | Supprimer Définitivement 136 | 137 | 138 | 139 | Move to Trash 140 | Mettre à la corbeille 141 | 142 | 143 | 144 | Append New Tag 145 | Ajouter un Nouveau Tag 146 | 147 | 148 | 149 | Write the new Tag to append 150 | Nouveau Tag 151 | 152 | 153 | 154 | Do you want to permanently delete "<b>%1</b>" 155 | Voulez vous supprimer définitivement "<b>%1</b>" 156 | 157 | 158 | 159 | Add the tag '%1' 160 | Ajouter le tag '%1' 161 | 162 | 163 | 164 | ExternalReadersCentralWidget 165 | 166 | 167 | Markdown Readers 168 | Liseuses Markdown 169 | 170 | 171 | 172 | Name of a Markdown editor/reader: 173 | Nom d'une liseuse Markdown: 174 | 175 | 176 | 177 | List of Markdown readers: 178 | Liste des liseuses Markdown: 179 | 180 | 181 | 182 | ExternalReadersDialog 183 | 184 | 185 | Markdown Readers Dialog 186 | 187 | 188 | 189 | 190 | MainWindow 191 | 192 | 193 | Show DeepTags 194 | Montrer DeepTags 195 | 196 | 197 | 198 | Exit 199 | Quitter 200 | 201 | 202 | 203 | 204 | Error 205 | Erreur 206 | 207 | 208 | 209 | This file doesn't exist 210 | Ce fichier n'existe pas 211 | 212 | 213 | 214 | Failed to create a new file 215 | Échec lors de la création d'un nouveau fichier 216 | 217 | 218 | 219 | documents 220 | documents 221 | 222 | 223 | 224 | File 225 | Fichier 226 | 227 | 228 | 229 | Recently Opened Files 230 | Documents Récents 231 | 232 | 233 | 234 | Edit 235 | Édition 236 | 237 | 238 | 239 | Themes 240 | Thèmes 241 | 242 | 243 | 244 | Help 245 | Aide 246 | 247 | 248 | 249 | New File 250 | Nouveau Fichier 251 | 252 | 253 | 254 | Ctrl+N 255 | 256 | 257 | 258 | 259 | Set/Change The Data Directory 260 | Définir/Changer le le Répertoire de Données 261 | 262 | 263 | 264 | Open the Data Directory 265 | Ouvrir le Répertoir de Données 266 | 267 | 268 | 269 | Quit 270 | Quitter 271 | 272 | 273 | 274 | Ctrl+Q 275 | 276 | 277 | 278 | 279 | Native Style 280 | Style Natif 281 | 282 | 283 | 284 | Dark Style 285 | Style Sombre 286 | 287 | 288 | 289 | Light Style 290 | Style Clair 291 | 292 | 293 | 294 | Markdown Readers 295 | Liseuses Markdown 296 | 297 | 298 | 299 | Reload Elements 300 | Recharger 301 | 302 | 303 | 304 | Display Integrated Reader 305 | Afficher le lecteur intégré 306 | 307 | 308 | 309 | 310 | About 311 | À Propos 312 | 313 | 314 | 315 | Settings 316 | Configuration 317 | 318 | 319 | 320 | 321 | Show Document Viewer 322 | Afficher le lecteur intégré 323 | 324 | 325 | 326 | Native 327 | 328 | 329 | 330 | 331 | QBreeze Dark 332 | 333 | 334 | 335 | 336 | QBreeze Light 337 | 338 | 339 | 340 | 341 | QObject 342 | 343 | Title 344 | Titre 345 | 346 | 347 | Path 348 | Chemin 349 | 350 | 351 | 352 | SettingsDialog 353 | 354 | 355 | Settings 356 | Configuration 357 | 358 | 359 | 360 | General 361 | 362 | 363 | 364 | 365 | This is applied when a file is written from DeepTags (like when adding a tag) 366 | 367 | 368 | 369 | 370 | Line Separator Type 371 | 372 | 373 | 374 | 375 | Carriage return and line feet (CRLF) 376 | 377 | 378 | 379 | 380 | Line Feed (LF) 381 | 382 | 383 | 384 | 385 | Markdown Readers 386 | Lecteurs Markdown 387 | 388 | 389 | 390 | TagsTreeView 391 | 392 | 393 | Change the color 394 | Changer la couleur 395 | 396 | 397 | 398 | Unpin 399 | Désépingler 400 | 401 | 402 | 403 | Pin 404 | Épingler 405 | 406 | 407 | 408 | Collapse 409 | Plier 410 | 411 | 412 | 413 | Expand 414 | Déplier 415 | 416 | 417 | 418 | Copy tag 419 | Copier le Tag 420 | 421 | 422 | 423 | Ui::Models::DocumentListModel 424 | 425 | 426 | Title 427 | Titre 428 | 429 | 430 | 431 | Path 432 | Chemin 433 | 434 | 435 | 436 | -------------------------------------------------------------------------------- /packaging/archlinux/PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: Zineddine SAIBI 2 | 3 | pkgname=deeptags 4 | pkgver=0.8.0 5 | pkgrel=1 6 | pkgdesc="A Markdown notes manager" 7 | arch=('x86_64') 8 | url="https://github.com/SZinedine/DeepTags" 9 | license=('GPL3') 10 | depends=( 11 | 'qt5-base' 12 | 'qt5-svg' 13 | ) 14 | makedepends=( 15 | 'qt5-tools' 16 | 'git' 17 | 'gcc' 18 | 'make' 19 | ) 20 | source=( 21 | "https://github.com/SZinedine/DeepTags/releases/download/${pkgver}/DeepTags-${pkgver}.tar.xz" 22 | ) 23 | sha256sums=( 24 | '5b3163323c3ce90f83fb2372b113039fb586ef4e75bdd4626ce7fa6b109232d1' 25 | ) 26 | conflicts=("deeptags-git") 27 | 28 | 29 | prepare() { 30 | cmake \ 31 | -B build/ \ 32 | -DCMAKE_CXX_COMPILER=g++ 33 | } 34 | 35 | build() { 36 | cmake \ 37 | --build build/ \ 38 | --config Release 39 | } 40 | 41 | package() { 42 | install -Dm 755 build/deeptags "${pkgdir}/usr/bin/deeptags" 43 | install -Dm 644 "packaging/resources/${pkgname}.appdata.xml" "${pkgdir}/usr/share/metainfo/${pkgname}.appdata.xml" 44 | install -Dm 644 "packaging/resources/${pkgname}.desktop" "${pkgdir}/usr/share/applications/${pkgname}.desktop" 45 | install -Dm 644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" 46 | 47 | local srcIco=packaging/resources/icons/hicolor 48 | local destIco=${pkgdir}/usr/share/icons/hicolor 49 | for icon in 16x16 32x32 48x48 64x64 80x80 96x96 128x128 192x192 256x256 384x384 512x512; do 50 | install -Dm 644 "${srcIco}/${icon}/apps/${pkgname}.png" "${destIco}/${icon}/apps/${pkgname}.png" 51 | done 52 | 53 | install -Dm 644 "${srcIco}/scalable/apps/${pkgname}.svg" "${destIco}/scalable/apps/${pkgname}.svg" 54 | } 55 | -------------------------------------------------------------------------------- /packaging/linux-build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | SCRIPT_DIR=$(readlink -f $(dirname $0)) 4 | PROJECT_DIR=$(readlink -f $SCRIPT_DIR/..) 5 | BUILD_DIR="$PROJECT_DIR/build" 6 | BIN_NAME="deeptags" 7 | BIN_PATH="$BUILD_DIR/$BIN_NAME" 8 | 9 | cd "$PROJECT_DIR" 10 | 11 | echo "Creating the build directory" 12 | mkdir -p "$BUILD_DIR" 13 | rm -f "$BIN_PATH" 14 | 15 | echo "running cmake" 16 | cmake -B "$BUILD_DIR" -DBENCHMARK=OFF -DTEST=OFF -DCMAKE_BUILD_TYPE=Release -G "Unix Makefiles" 17 | 18 | echo "building" 19 | make -j$(nproc) -C build 20 | 21 | 22 | if [ ! -f "$BIN_PATH" ]; then 23 | echo "the binary of $_APP_NAME doesn't exist." 24 | exit 1 25 | else 26 | echo "successfully built DeepTags" 27 | exit 0 28 | fi 29 | -------------------------------------------------------------------------------- /packaging/package-deb.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | SCRIPT_DIR=$(readlink -f $(dirname $0)) 4 | PROJECT_DIR=$(readlink -f $SCRIPT_DIR/..) 5 | BUILD_DIR="$PROJECT_DIR/build" 6 | BIN_NAME="deeptags" 7 | BIN_PATH="$BUILD_DIR/$BIN_NAME" 8 | 9 | PKG_DIR="$PROJECT_DIR/packaging" 10 | RC_DIR="$PKG_DIR/resources" 11 | DESKTOP_FILE="$RC_DIR/$BIN_NAME.desktop" 12 | APPDATA_FILE="$RC_DIR/$BIN_NAME.appdata.xml" 13 | ICONS_DIR="$RC_DIR/icons" 14 | 15 | 16 | # Check if we are on Linux 17 | if [ "$(uname)" != "Linux" ]; then 18 | echo "This script only works on linux." 19 | exit 1 20 | fi 21 | 22 | if [ ! -f "$BIN_PATH" ]; then 23 | echo "the binary of $BIN_PATH doesn't exist. You have to build the project first" 24 | exit 1 25 | fi 26 | 27 | 28 | 29 | VERSION=$($BIN_PATH --version | awk '{print $2}') 30 | DEB_DIR=$PKG_DIR/deeptags_"$VERSION"_amd64 31 | rm -rf $DEB_DIR 32 | 33 | 34 | mkdir -p $DEB_DIR/DEBIAN 35 | mkdir -p $DEB_DIR/usr/bin 36 | mkdir -p $DEB_DIR/usr/share/applications 37 | mkdir -p $DEB_DIR/usr/share/metainfo 38 | 39 | echo "copying files" 40 | cp $DESKTOP_FILE $DEB_DIR/usr/share/applications 41 | cp -r $ICONS_DIR $DEB_DIR/usr/share/ 42 | cp $BIN_PATH $DEB_DIR/usr/bin 43 | cp $APPDATA_FILE $DEB_DIR/usr/share/metainfo/ 44 | 45 | 46 | echo "creating the control file" 47 | cat >> $DEB_DIR/DEBIAN/control <= 5.15), libqt5gui5 (>= 5.15), libqt5network5 (>= 5.15), libqt5core5a (>= 5.15) 55 | Maintainer: Zineddine SAIBI 56 | Homepage: https://www.github.com/SZinedine/DeepTags 57 | Description: A markdown notes manager 58 | EOL 59 | 60 | 61 | echo "creating the deb package" 62 | dpkg-deb --build $DEB_DIR 63 | 64 | if [ -n "$DIST" ]; then 65 | echo "adding the distribution name into the output filename" 66 | mv $DEB_DIR.deb $DEB_DIR-$DIST.deb 67 | fi 68 | 69 | echo "deleting temporary files" 70 | rm -rf $DEB_DIR 71 | 72 | -------------------------------------------------------------------------------- /packaging/resources/deeptags.appdata.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | deeptags 4 | GFDL-1.3 5 | GPL-2.0+ 6 | DeepTags 7 | SZinedine 8 | DeepTags is a markdown notes manager with support for nested tags 9 | 10 |

DeepTags supports nested tags and offers simple ways to edit them, for example by dragging and dropping a tag on a note. These notes could be read either with the integrated editor or with one or multiple third party markdown editors installed on your system. Make sure to add them into the the app in Edit -> Markdown Readers.

11 |
12 | deeptags.desktop 13 | https://github.com/SZinedine/DeepTags 14 | 15 | 16 | https://github.com/SZinedine/DeepTags/raw/master/Screenshot.png 17 | 18 | 19 | 20 | deeptags.desktop 21 | 22 |
23 | -------------------------------------------------------------------------------- /packaging/resources/deeptags.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Categories=Utility;Office;Qt; 4 | Name=DeepTags 5 | Comment=A markdown notes manager that organizes notes according to tags 6 | TryExec=deeptags 7 | Exec=deeptags %F 8 | Icon=deeptags 9 | Terminal=false 10 | -------------------------------------------------------------------------------- /packaging/resources/icons/hicolor/128x128/apps/deeptags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SZinedine/DeepTags/483c84729c82f784dd10f7a3e74bd2ff18a27a69/packaging/resources/icons/hicolor/128x128/apps/deeptags.png -------------------------------------------------------------------------------- /packaging/resources/icons/hicolor/16x16/apps/deeptags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SZinedine/DeepTags/483c84729c82f784dd10f7a3e74bd2ff18a27a69/packaging/resources/icons/hicolor/16x16/apps/deeptags.png -------------------------------------------------------------------------------- /packaging/resources/icons/hicolor/192x192/apps/deeptags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SZinedine/DeepTags/483c84729c82f784dd10f7a3e74bd2ff18a27a69/packaging/resources/icons/hicolor/192x192/apps/deeptags.png -------------------------------------------------------------------------------- /packaging/resources/icons/hicolor/256x256/apps/deeptags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SZinedine/DeepTags/483c84729c82f784dd10f7a3e74bd2ff18a27a69/packaging/resources/icons/hicolor/256x256/apps/deeptags.png -------------------------------------------------------------------------------- /packaging/resources/icons/hicolor/32x32/apps/deeptags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SZinedine/DeepTags/483c84729c82f784dd10f7a3e74bd2ff18a27a69/packaging/resources/icons/hicolor/32x32/apps/deeptags.png -------------------------------------------------------------------------------- /packaging/resources/icons/hicolor/384x384/apps/deeptags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SZinedine/DeepTags/483c84729c82f784dd10f7a3e74bd2ff18a27a69/packaging/resources/icons/hicolor/384x384/apps/deeptags.png -------------------------------------------------------------------------------- /packaging/resources/icons/hicolor/48x48/apps/deeptags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SZinedine/DeepTags/483c84729c82f784dd10f7a3e74bd2ff18a27a69/packaging/resources/icons/hicolor/48x48/apps/deeptags.png -------------------------------------------------------------------------------- /packaging/resources/icons/hicolor/512x512/apps/deeptags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SZinedine/DeepTags/483c84729c82f784dd10f7a3e74bd2ff18a27a69/packaging/resources/icons/hicolor/512x512/apps/deeptags.png -------------------------------------------------------------------------------- /packaging/resources/icons/hicolor/64x64/apps/deeptags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SZinedine/DeepTags/483c84729c82f784dd10f7a3e74bd2ff18a27a69/packaging/resources/icons/hicolor/64x64/apps/deeptags.png -------------------------------------------------------------------------------- /packaging/resources/icons/hicolor/80x80/apps/deeptags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SZinedine/DeepTags/483c84729c82f784dd10f7a3e74bd2ff18a27a69/packaging/resources/icons/hicolor/80x80/apps/deeptags.png -------------------------------------------------------------------------------- /packaging/resources/icons/hicolor/8x8/apps/deeptags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SZinedine/DeepTags/483c84729c82f784dd10f7a3e74bd2ff18a27a69/packaging/resources/icons/hicolor/8x8/apps/deeptags.png -------------------------------------------------------------------------------- /packaging/resources/icons/hicolor/96x96/apps/deeptags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SZinedine/DeepTags/483c84729c82f784dd10f7a3e74bd2ff18a27a69/packaging/resources/icons/hicolor/96x96/apps/deeptags.png -------------------------------------------------------------------------------- /packaging/resources/icons/hicolor/scalable/apps/deeptags.svg: -------------------------------------------------------------------------------- 1 | 2 | 20 | 22 | 41 | 44 | 45 | 47 | 48 | 50 | image/svg+xml 51 | 53 | 54 | 55 | 56 | 57 | 61 | 69 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /src/Benchmark.h: -------------------------------------------------------------------------------- 1 | #ifndef BENCHMARK__H 2 | #define BENCHMARK__H 3 | 4 | #ifdef BENCHMARKS 5 | #include 6 | #include 7 | 8 | class Benchmark final { 9 | using time_point = std::chrono::steady_clock::time_point; 10 | static constexpr auto now = std::chrono::steady_clock::now; 11 | 12 | public: 13 | explicit Benchmark(std::string message) : mMessage(std::move(message)) { begin(); } 14 | 15 | void begin() { mBegin = now(); } 16 | 17 | void end() { 18 | if (!mEnded) { 19 | mEnd = now(); 20 | auto t = std::chrono::duration_cast(mEnd - mBegin).count(); 21 | std::printf("%s: %ld[ms]\n", mMessage.c_str(), t); 22 | } 23 | 24 | mEnded = true; 25 | } 26 | 27 | ~Benchmark() { end(); } 28 | 29 | private: 30 | std::string mMessage; 31 | bool mEnded{ false }; 32 | time_point mBegin; 33 | time_point mEnd; 34 | }; 35 | 36 | #else 37 | 38 | struct Benchmark { 39 | explicit Benchmark([[maybe_unused]] const char* message) {} 40 | void start() {} 41 | void end() {} 42 | }; 43 | 44 | #endif 45 | 46 | #endif // BENCHMARK__H 47 | -------------------------------------------------------------------------------- /src/DataDirectoryCentralWidget.cpp: -------------------------------------------------------------------------------- 1 | #include "DataDirectoryCentralWidget.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "Settings.h" 7 | #include "ui_DataDirectoryCentralWidget.h" 8 | 9 | 10 | DataDirectoryCentralWidget::DataDirectoryCentralWidget(QWidget* parent) 11 | : QWidget(parent), ui(new Ui::DataDirectoryCentralWidget) { 12 | ui->setupUi(this); 13 | ui->mDirectory->setText(getDataDir()); 14 | connect(ui->mBrowse, &QPushButton::clicked, this, &DataDirectoryCentralWidget::browse); 15 | } 16 | 17 | 18 | DataDirectoryCentralWidget::~DataDirectoryCentralWidget() { 19 | delete ui; 20 | } 21 | 22 | 23 | QString DataDirectoryCentralWidget::getDataDir() { 24 | QString dataDir = Ui::Settings::loadDataDirectory(); 25 | if (dataDir.isEmpty()) { 26 | // "~/Documents/Notes" OR "C:/Users//Documents/Notes". TODO: test this on Windows 27 | return QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + "/" + "Notes"; 28 | } 29 | 30 | return dataDir; 31 | } 32 | 33 | 34 | bool DataDirectoryCentralWidget::accept() { 35 | const QString oldDataDirectory = Ui::Settings::loadDataDirectory().simplified(); 36 | const QString newDataDirectory = ui->mDirectory->text().simplified(); 37 | 38 | if (oldDataDirectory == newDataDirectory) { 39 | return false; 40 | } 41 | 42 | if (!QDir().exists(newDataDirectory)) { 43 | if (!QDir().mkdir(newDataDirectory)) { 44 | QMessageBox::critical(this, tr("error"), 45 | tr("Error while trying to create the Data Directory")); 46 | return false; 47 | } 48 | } 49 | 50 | Ui::Settings::saveDataDirectory(newDataDirectory); 51 | emit dataDirectoryChanged(); 52 | 53 | return true; 54 | } 55 | 56 | 57 | void DataDirectoryCentralWidget::browse() { 58 | auto dir = QFileDialog::getExistingDirectory(this, tr("Data Directory"), getDataDir()); 59 | 60 | if (!dir.isEmpty()) { 61 | ui->mDirectory->setText(dir); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/DataDirectoryCentralWidget.h: -------------------------------------------------------------------------------- 1 | #ifndef DATADIRECTORYCENTRALWIDGET__H 2 | #define DATADIRECTORYCENTRALWIDGET__H 3 | 4 | #include 5 | 6 | QT_BEGIN_NAMESPACE 7 | namespace Ui { 8 | class DataDirectoryCentralWidget; 9 | } 10 | QT_END_NAMESPACE 11 | 12 | class DataDirectoryCentralWidget : public QWidget { 13 | Q_OBJECT 14 | public: 15 | explicit DataDirectoryCentralWidget(QWidget* parent = nullptr); 16 | ~DataDirectoryCentralWidget() override; 17 | /** 18 | * return the Data Directory from the Settings, 19 | * otherwise the standard path of Document 20 | */ 21 | static QString getDataDir(); 22 | 23 | public slots: 24 | /** 25 | * save the data directory into the settings if it was changed 26 | * if the directory doesn't exist, it will be created 27 | */ 28 | bool accept(); 29 | 30 | private slots: 31 | void browse(); 32 | 33 | signals: 34 | void dataDirectoryChanged(); 35 | 36 | private: 37 | Ui::DataDirectoryCentralWidget* ui; 38 | }; 39 | 40 | #endif // DATADIRECTORYCENTRALWIDGET__H 41 | -------------------------------------------------------------------------------- /src/DataDirectoryCentralWidget.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | DataDirectoryCentralWidget 4 | 5 | 6 | 7 | 0 8 | 0 9 | 450 10 | 140 11 | 12 | 13 | 14 | Data Directory 15 | 16 | 17 | 18 | 19 | 20 | Please choose a Data Directory where you want your notes to be saved. If you already have notes in a folder, point it out to import them. 21 | 22 | 23 | true 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 0 37 | 0 38 | 39 | 40 | 41 | Browse 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/DataDirectoryDialog.cpp: -------------------------------------------------------------------------------- 1 | #include "DataDirectoryDialog.h" 2 | #include "ui_DataDirectoryDialog.h" 3 | 4 | DataDirectoryDialog::DataDirectoryDialog(QWidget* parent) 5 | : QDialog(parent), ui(new Ui::DataDirectoryDialog) { 6 | ui->setupUi(this); 7 | connect(ui->dataDirWidget, &DataDirectoryCentralWidget::dataDirectoryChanged, this, 8 | [this] { emit dataDirectoryChanged(); }); 9 | } 10 | 11 | 12 | DataDirectoryDialog::~DataDirectoryDialog() { 13 | delete ui; 14 | } 15 | 16 | 17 | void DataDirectoryDialog::accept() { 18 | if (!ui->dataDirWidget->accept()) { 19 | return; 20 | } 21 | 22 | QDialog::accept(); 23 | } 24 | -------------------------------------------------------------------------------- /src/DataDirectoryDialog.h: -------------------------------------------------------------------------------- 1 | #ifndef DATADIRECTORYDIALOG__H 2 | #define DATADIRECTORYDIALOG__H 3 | 4 | #include 5 | 6 | QT_BEGIN_NAMESPACE 7 | namespace Ui { 8 | class DataDirectoryDialog; 9 | } 10 | QT_END_NAMESPACE 11 | 12 | 13 | class DataDirectoryDialog : public QDialog { 14 | Q_OBJECT 15 | public: 16 | explicit DataDirectoryDialog(QWidget* parent = nullptr); 17 | ~DataDirectoryDialog() override; 18 | void accept() final; 19 | 20 | signals: 21 | void dataDirectoryChanged(); 22 | 23 | private: 24 | Ui::DataDirectoryDialog* ui; 25 | }; 26 | 27 | #endif // DATADIRECTORYDIALOG__H 28 | -------------------------------------------------------------------------------- /src/DataDirectoryDialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | DataDirectoryDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 450 10 | 170 11 | 12 | 13 | 14 | 15 | 0 16 | 0 17 | 18 | 19 | 20 | 21 | 450 22 | 170 23 | 24 | 25 | 26 | 27 | 450 28 | 170 29 | 30 | 31 | 32 | Data Directory 33 | 34 | 35 | 36 | QLayout::SetDefaultConstraint 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | Qt::Horizontal 45 | 46 | 47 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | DataDirectoryCentralWidget 56 | QWidget 57 |
DataDirectoryCentralWidget.h
58 | 1 59 |
60 |
61 | 62 | 63 | 64 | buttonBox 65 | accepted() 66 | DataDirectoryDialog 67 | accept() 68 | 69 | 70 | 224 71 | 144 72 | 73 | 74 | 157 75 | 169 76 | 77 | 78 | 79 | 80 | buttonBox 81 | rejected() 82 | DataDirectoryDialog 83 | reject() 84 | 85 | 86 | 292 87 | 150 88 | 89 | 90 | 286 91 | 169 92 | 93 | 94 | 95 | 96 |
97 | -------------------------------------------------------------------------------- /src/Document.cpp: -------------------------------------------------------------------------------- 1 | #include "Document.h" 2 | #include 3 | #include 4 | #include 5 | #include "Benchmark.h" 6 | #include "DocumentUtils.h" 7 | 8 | namespace Doc { 9 | 10 | Document::Document(QString path) : mPath{ std::move(path) } { 11 | load(); 12 | } 13 | 14 | 15 | void Document::load() { 16 | mHeader = Utils::getHeader(mPath); 17 | if (mHeader.empty()) { 18 | mTitle = QFileInfo(mPath).baseName(); 19 | mPinned = false; 20 | mFavorited = false; 21 | mDeleted = false; 22 | return; 23 | } 24 | 25 | bool hasTitle = std::any_of(mHeader.cbegin(), mHeader.cend(), [](const QString& line) { 26 | return line.startsWith(QStringLiteral("title:")); 27 | }); 28 | mTitle = hasTitle ? Utils::getHeaderValue(titleKey, mHeader) : QFileInfo(mPath).baseName(); 29 | mPinned = Utils::getHeaderValue((pinnedKey), mHeader) == trueStr; 30 | mFavorited = Utils::getHeaderValue(favoritedKey, mHeader) == trueStr; 31 | mDeleted = Utils::getHeaderValue(deletedKey, mHeader) == trueStr; 32 | mTags = Utils::splitTags(mHeader); 33 | } 34 | 35 | 36 | QVector Document::constructDocumentList(const QStringVector& paths) { 37 | Benchmark b("Document construction"); 38 | 39 | QVector docs; 40 | QMutex mutex; 41 | auto pushNewDocument = [&docs, &mutex](const QString& p) { 42 | auto doc = new Document(p); 43 | QMutexLocker lock(&mutex); 44 | docs.push_back(doc); 45 | }; 46 | 47 | QtConcurrent::blockingMap(paths, pushNewDocument); 48 | 49 | return docs; 50 | } 51 | 52 | 53 | QString Document::getPath() const { 54 | return mPath; 55 | } 56 | 57 | 58 | QString Document::getTitle() const { 59 | return mTitle; 60 | } 61 | 62 | 63 | QStringVector Document::getTags() const { 64 | return mTags; 65 | } 66 | 67 | 68 | QStringVector Document::getHeader() const { 69 | return mHeader; 70 | } 71 | 72 | 73 | bool Document::isPinned() const { 74 | return mPinned; 75 | } 76 | 77 | 78 | bool Document::isFavorited() const { 79 | return mFavorited; 80 | } 81 | 82 | 83 | bool Document::isDeleted() const { 84 | return mDeleted; 85 | } 86 | 87 | 88 | bool Document::containsExactTag(const QString& tag) const { 89 | return mTags.contains(tag); 90 | } 91 | 92 | 93 | bool Document::containsTag(const QStringVector& tag) const { 94 | for (const QString& t : mTags) { 95 | const QStringVector tagLineChain = Utils::deconstructTag(t); 96 | 97 | if (tagLineChain.size() < tag.size()) { 98 | continue; 99 | } 100 | 101 | if (std::equal(tag.cbegin(), tag.cend(), tagLineChain.cbegin(), 102 | tagLineChain.cbegin() + tag.size())) { 103 | return true; 104 | } 105 | } 106 | 107 | return false; 108 | } 109 | 110 | 111 | bool Document::containsAllTags(const QStringList& tags) const { 112 | return std::any_of(tags.begin(), tags.end(), 113 | [this](const QString& t) { return containsTag(Utils::deconstructTag(t)); }); 114 | } 115 | 116 | 117 | void Document::setPath(const QString& path) { 118 | if (!path.isEmpty()) { 119 | mPath = path; 120 | } 121 | } 122 | 123 | 124 | void Document::setTitle(const QString& title) { 125 | if (!title.isEmpty()) { 126 | mTitle = title; 127 | } 128 | } 129 | 130 | 131 | void Document::setPinned(bool pinned) { 132 | mPinned = pinned; 133 | } 134 | 135 | 136 | void Document::setFavorited(bool favorited) { 137 | mFavorited = favorited; 138 | } 139 | 140 | 141 | void Document::setDeleted(bool deleted) { 142 | mDeleted = deleted; 143 | } 144 | 145 | 146 | void Document::setHeader(const QStringVector& header) { 147 | mHeader = header; 148 | } 149 | 150 | 151 | void Document::addTag(const QString& tag) { 152 | if (!tag.isEmpty()) { 153 | mTags.append(tag.simplified()); 154 | } 155 | } 156 | 157 | 158 | void Document::delTag(const QString& tag) { 159 | mTags.removeAll(tag); 160 | } 161 | 162 | 163 | bool operator==(const Document& lhs, const Document& rhs) { 164 | return lhs.mPath == rhs.mPath; 165 | } 166 | 167 | 168 | bool operator<(const Document& lhs, const Document& rhs) { 169 | if (lhs.mPinned && !rhs.mPinned) { 170 | return true; 171 | } else if (rhs.mPinned && !lhs.mPinned) { 172 | return false; 173 | } 174 | 175 | return lhs.mTitle < rhs.mTitle; 176 | } 177 | 178 | 179 | } // namespace Doc 180 | -------------------------------------------------------------------------------- /src/Document.h: -------------------------------------------------------------------------------- 1 | #ifndef DOCUMENT__H 2 | #define DOCUMENT__H 3 | 4 | #include 5 | #include "DocumentUtils.h" 6 | 7 | namespace Doc { 8 | /** 9 | * representation of single document 10 | */ 11 | class Document final { 12 | public: 13 | Document() = default; 14 | explicit Document(QString path); 15 | Document(const Document&) = delete; 16 | Document& operator=(const Document&) = delete; 17 | Document(Document&&) = default; 18 | Document& operator=(Document&&) = default; 19 | void load(); 20 | static QVector constructDocumentList(const QStringVector& paths); 21 | [[nodiscard]] QString getPath() const; 22 | [[nodiscard]] QString getTitle() const; 23 | [[nodiscard]] QStringVector getTags() const; 24 | [[nodiscard]] QStringVector getHeader() const; 25 | [[nodiscard]] bool isPinned() const; 26 | [[nodiscard]] bool isFavorited() const; 27 | [[nodiscard]] bool isDeleted() const; 28 | [[nodiscard]] bool containsExactTag(const QString& tag) const; 29 | [[nodiscard]] bool containsTag(const QStringVector& tag) const; 30 | [[nodiscard]] bool containsAllTags(const QStringList& tags) const; 31 | void setPath(const QString& path); 32 | void setTitle(const QString& title); 33 | void setPinned(bool pinned); 34 | void setFavorited(bool favorited); 35 | void setDeleted(bool deleted); 36 | void setHeader(const QStringVector& header); 37 | void delTag(const QString& tag); 38 | void addTag(const QString& tag); 39 | 40 | friend bool operator==(const Document& lhs, const Document& rhs); 41 | friend bool operator<(const Document& lhs, const Document& rhs); 42 | 43 | private: 44 | QString mPath; 45 | QString mTitle; 46 | bool mPinned{ false }; 47 | bool mFavorited{ false }; 48 | bool mDeleted{ false }; 49 | QStringVector mTags; 50 | QStringVector mHeader; 51 | }; 52 | 53 | } // namespace Doc 54 | #endif // DOCUMENT__H 55 | -------------------------------------------------------------------------------- /src/DocumentContentView.cpp: -------------------------------------------------------------------------------- 1 | #include "DocumentContentView.h" 2 | #include <3rdParty/qmarkdowntextedit/markdownhighlighter.h> 3 | #include 4 | #include "Document.h" 5 | 6 | DocumentContentView::DocumentContentView(QWidget* parent) 7 | : QPlainTextEdit{ parent }, mDocument(nullptr), 8 | mHighlighter{ std::make_unique() }, 9 | mWatcher{ std::make_unique() } { 10 | mHighlighter->setDocument(document()); 11 | setReadOnly(true); 12 | connect(mWatcher.get(), &QFileSystemWatcher::fileChanged, this, 13 | &DocumentContentView::onFileChanged); 14 | } 15 | 16 | 17 | void DocumentContentView::setDocument(Doc::Document* document) { 18 | mDocument = document; 19 | display(); 20 | mWatcher->addPath(mDocument->getPath()); 21 | } 22 | 23 | 24 | void DocumentContentView::onDocumentDeleted(Doc::Document* document) { 25 | if (mDocument != nullptr && mDocument == document) { 26 | reset(); 27 | } 28 | } 29 | 30 | 31 | void DocumentContentView::display() { 32 | if (mDocument != nullptr) { 33 | setPlainText(Doc::Utils::getFileContentAsString(mDocument->getPath())); 34 | } 35 | } 36 | 37 | 38 | void DocumentContentView::reset() { 39 | clear(); 40 | 41 | if (const auto paths = mWatcher->files(); !paths.isEmpty()) { 42 | mWatcher->removePaths(paths); 43 | } 44 | 45 | mDocument = nullptr; 46 | } 47 | 48 | 49 | void DocumentContentView::onFileChanged(const QString& path) { 50 | if (!QFileInfo::exists(path)) { 51 | reset(); 52 | return; 53 | } 54 | QTextCursor cur(textCursor()); 55 | auto po = cur.position(); 56 | display(); 57 | cur.setPosition(po); 58 | setTextCursor(cur); 59 | } 60 | -------------------------------------------------------------------------------- /src/DocumentContentView.h: -------------------------------------------------------------------------------- 1 | #ifndef DOCUMENTDISPLAYER__H 2 | #define DOCUMENTDISPLAYER__H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | namespace Doc { 9 | class Document; 10 | } 11 | 12 | class MarkdownHighlighter; 13 | // class QFileSystemWatcher; 14 | 15 | // namespace Ui { 16 | QT_BEGIN_NAMESPACE 17 | namespace Ui { 18 | class DocumentContentView; 19 | } 20 | QT_END_NAMESPACE 21 | 22 | class DocumentContentView : public QPlainTextEdit { 23 | Q_OBJECT 24 | public: 25 | explicit DocumentContentView(QWidget* parent = nullptr); 26 | ~DocumentContentView() override = default; 27 | 28 | public slots: 29 | void setDocument(Doc::Document* document); 30 | void onDocumentDeleted(Doc::Document* document); 31 | void reset(); 32 | 33 | private: 34 | void onFileChanged(const QString& path); 35 | void display(); 36 | 37 | private: 38 | Doc::Document* mDocument; 39 | std::unique_ptr mHighlighter; 40 | std::unique_ptr mWatcher; 41 | }; 42 | // } // namespace Ui 43 | 44 | #endif // DOCUMENTDISPLAYER__H 45 | -------------------------------------------------------------------------------- /src/DocumentInfoDialog.cpp: -------------------------------------------------------------------------------- 1 | #include "DocumentInfoDialog.h" 2 | #include 3 | #include "./ui_DocumentInfoDialog.h" 4 | #include "Document.h" 5 | 6 | DocumentInfoDialog::DocumentInfoDialog(Doc::Document* document, QWidget* parent) 7 | : QDialog(parent), ui(new Ui::DocumentInfoDialog), mDocument{ document }, 8 | mTagModel(std::make_unique()) { 9 | ui->setupUi(this); 10 | ui->mTags->setModel(mTagModel.get()); 11 | 12 | if (document != nullptr) { 13 | mDocument = document; 14 | editModeSetup(); 15 | } else { 16 | newFileModeSetup(); 17 | } 18 | 19 | setupSignalsAndSlots(); 20 | } 21 | 22 | 23 | DocumentInfoDialog::~DocumentInfoDialog() { 24 | delete ui; 25 | } 26 | 27 | 28 | void DocumentInfoDialog::newFileModeSetup() { 29 | ui->mPath->setVisible(false); // TODO: should make it visible and show the title modifications 30 | ui->filePathLabel->setVisible(false); 31 | ui->mTitle->setText("New Document"); 32 | ui->mPinned->setChecked(false); 33 | ui->mFavorited->setChecked(false); 34 | 35 | ui->mTitle->selectAll(); 36 | } 37 | 38 | 39 | void DocumentInfoDialog::editModeSetup() { 40 | ui->mPath->setText(mDocument->getPath()); 41 | ui->mTitle->setText(mDocument->getTitle()); 42 | ui->mPinned->setChecked(mDocument->isPinned()); 43 | ui->mFavorited->setChecked(mDocument->isFavorited()); 44 | 45 | mTagModel->setStringList(mDocument->getTags().toList()); 46 | } 47 | 48 | void DocumentInfoDialog::setupSignalsAndSlots() { 49 | connect(ui->buttons, &QDialogButtonBox::accepted, this, &DocumentInfoDialog::accept); 50 | connect(ui->buttons, &QDialogButtonBox::rejected, this, &DocumentInfoDialog::reject); 51 | connect(ui->mAddTag, &QPushButton::clicked, this, &DocumentInfoDialog::addTagRow); 52 | connect(ui->mDeleteTag, &QPushButton::clicked, this, &DocumentInfoDialog::deleteTagRow); 53 | } 54 | 55 | 56 | bool DocumentInfoDialog::isFavorited() const { 57 | return ui->mFavorited->isChecked(); 58 | } 59 | 60 | 61 | bool DocumentInfoDialog::isPinned() const { 62 | return ui->mPinned->isChecked(); 63 | } 64 | 65 | 66 | QString DocumentInfoDialog::getTitle() const { 67 | return ui->mTitle->text().simplified(); 68 | } 69 | 70 | 71 | QVector DocumentInfoDialog::getTags() const { 72 | return mTagModel->stringList().toVector(); 73 | } 74 | 75 | 76 | void DocumentInfoDialog::addTagRow() { 77 | QStringList sl = mTagModel->stringList(); 78 | sl << ""; 79 | 80 | mTagModel->setStringList(sl); 81 | auto index = mTagModel->index(sl.size() - 1, 0); 82 | ui->mTags->scrollToBottom(); 83 | ui->mTags->setCurrentIndex(index); 84 | ui->mTags->edit(index); 85 | } 86 | 87 | 88 | void DocumentInfoDialog::deleteTagRow() { 89 | QStringList sl = mTagModel->stringList(); 90 | int row = ui->mTags->currentIndex().row(); 91 | if (row < sl.size() && 0 <= row) { 92 | sl.removeAt(row); 93 | mTagModel->setStringList(sl); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/DocumentInfoDialog.h: -------------------------------------------------------------------------------- 1 | #ifndef DOCUMENTINFODIALOG__H 2 | #define DOCUMENTINFODIALOG__H 3 | 4 | #include 5 | #include 6 | 7 | namespace Doc { 8 | class Document; 9 | } 10 | 11 | QT_BEGIN_NAMESPACE 12 | namespace Ui { 13 | class DocumentInfoDialog; 14 | } 15 | QT_END_NAMESPACE 16 | 17 | 18 | class DocumentInfoDialog : public QDialog { 19 | public: 20 | explicit DocumentInfoDialog(Doc::Document* document, QWidget* parent); 21 | ~DocumentInfoDialog() override; 22 | 23 | [[nodiscard]] bool isFavorited() const; 24 | [[nodiscard]] bool isPinned() const; 25 | [[nodiscard]] QString getTitle() const; 26 | [[nodiscard]] QVector getTags() const; 27 | 28 | private: 29 | void editModeSetup(); 30 | void newFileModeSetup(); 31 | void setupSignalsAndSlots(); 32 | void addTagRow(); 33 | void deleteTagRow(); 34 | 35 | private: 36 | Ui::DocumentInfoDialog* ui; 37 | Doc::Document* mDocument; 38 | std::unique_ptr mTagModel; 39 | }; 40 | 41 | #endif // DOCUMENTINFODIALOG__H 42 | -------------------------------------------------------------------------------- /src/DocumentInfoDialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | DocumentInfoDialog 4 | 5 | 6 | Qt::ApplicationModal 7 | 8 | 9 | 10 | 0 11 | 0 12 | 450 13 | 350 14 | 15 | 16 | 17 | 18 | 0 19 | 0 20 | 21 | 22 | 23 | 24 | 450 25 | 350 26 | 27 | 28 | 29 | 30 | 450 31 | 350 32 | 33 | 34 | 35 | Document Information 36 | 37 | 38 | true 39 | 40 | 41 | 42 | 43 | 44 | File Path: 45 | 46 | 47 | 48 | 49 | 50 | 51 | Title: 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | Pinned: 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | Favorite: 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | Tags: 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 35 97 | 16777215 98 | 99 | 100 | 101 | Add a tag 102 | 103 | 104 | 105 | 106 | 107 | 108 | :/images/add.png:/images/add.png 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 35 117 | 16777215 118 | 119 | 120 | 121 | Delete a tag 122 | 123 | 124 | 125 | 126 | 127 | 128 | :/images/delete.png:/images/delete.png 129 | 130 | 131 | 132 | 133 | 134 | 135 | Qt::Vertical 136 | 137 | 138 | 139 | 20 140 | 40 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 153 | 154 | 155 | 156 | 157 | 158 | 159 | true 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /src/DocumentListDelegate.cpp: -------------------------------------------------------------------------------- 1 | #include "DocumentListDelegate.h" 2 | #include 3 | #include "DocumentListModel.h" 4 | 5 | namespace Ui::Delegates { 6 | 7 | DocumentListDelegate::DocumentListDelegate(QObject* parent) : QStyledItemDelegate(parent) {} 8 | 9 | 10 | void DocumentListDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, 11 | const QModelIndex& index) const { 12 | QStyleOptionViewItem myOption(option); 13 | 14 | myOption.features = QStyleOptionViewItem::HasDisplay | QStyleOptionViewItem::HasDecoration; 15 | 16 | myOption.decorationPosition = QStyleOptionViewItem::Right; 17 | 18 | const bool isPinned = index.data(Models::DocumentListModel::PinnedRole).toBool(); 19 | const bool isFavorited = index.data(Models::DocumentListModel::FavoritedRole).toBool(); 20 | 21 | static const QIcon mPinIcon(QStringLiteral(":images/pin.png")); 22 | static const QIcon mFavIcon(QStringLiteral(":images/favorite.png")); 23 | static const QIcon mFavPinIcon(QStringLiteral(":images/favpin.png")); 24 | 25 | if (isPinned && isFavorited) { 26 | myOption.icon = mFavPinIcon; 27 | } else if (isPinned) { 28 | myOption.icon = mPinIcon; 29 | } else if (isFavorited) { 30 | myOption.icon = mFavIcon; 31 | } 32 | 33 | QStyledItemDelegate::paint(painter, myOption, index); 34 | } 35 | 36 | 37 | } // namespace Ui::Delegates 38 | -------------------------------------------------------------------------------- /src/DocumentListDelegate.h: -------------------------------------------------------------------------------- 1 | #ifndef DOCUMENTLISTDELEGATE__H 2 | #define DOCUMENTLISTDELEGATE__H 3 | 4 | 5 | #include 6 | 7 | namespace Ui::Delegates { 8 | 9 | class DocumentListDelegate : public QStyledItemDelegate { 10 | Q_OBJECT 11 | public: 12 | explicit DocumentListDelegate(QObject* parent = nullptr); 13 | void paint(QPainter* painter, const QStyleOptionViewItem& option, 14 | const QModelIndex& index) const final; 15 | }; 16 | } // namespace Ui::Delegates 17 | 18 | #endif // DOCUMENTLISTDELEGATE__H 19 | -------------------------------------------------------------------------------- /src/DocumentListItem.cpp: -------------------------------------------------------------------------------- 1 | #include "DocumentListItem.h" 2 | #include "Document.h" 3 | 4 | namespace Ui::Models { 5 | 6 | DocumentListItem::DocumentListItem(Doc::Document* document) : mDocument{ document } {} 7 | 8 | 9 | Doc::Document* DocumentListItem::getDocument() { 10 | return mDocument; 11 | } 12 | 13 | 14 | QString DocumentListItem::getTitle() const { 15 | return mDocument->getTitle(); 16 | } 17 | 18 | 19 | QString DocumentListItem::getPath() const { 20 | return mDocument->getPath(); 21 | } 22 | 23 | 24 | bool DocumentListItem::isPinned() const { 25 | return mDocument->isPinned(); 26 | } 27 | 28 | 29 | bool DocumentListItem::isFavorited() const { 30 | return mDocument->isFavorited(); 31 | } 32 | 33 | 34 | bool DocumentListItem::isDeleted() const { 35 | return mDocument->isDeleted(); 36 | } 37 | 38 | } // namespace Ui::Models 39 | -------------------------------------------------------------------------------- /src/DocumentListItem.h: -------------------------------------------------------------------------------- 1 | #ifndef DOCUMENTLISTITEM__H 2 | #define DOCUMENTLISTITEM__H 3 | 4 | class QString; 5 | 6 | namespace Doc { 7 | class Document; 8 | } 9 | 10 | namespace Ui::Models { 11 | 12 | class DocumentListItem final { 13 | public: 14 | explicit DocumentListItem(Doc::Document* document); 15 | DocumentListItem(const DocumentListItem&) = delete; 16 | DocumentListItem& operator=(const DocumentListItem&) = delete; 17 | DocumentListItem(DocumentListItem&&) = default; 18 | DocumentListItem& operator=(DocumentListItem&&) = default; 19 | ~DocumentListItem() = default; 20 | Doc::Document* getDocument(); 21 | [[nodiscard]] QString getTitle() const; 22 | [[nodiscard]] QString getPath() const; 23 | [[nodiscard]] bool isPinned() const; 24 | [[nodiscard]] bool isFavorited() const; 25 | [[nodiscard]] bool isDeleted() const; 26 | 27 | private: 28 | Doc::Document* mDocument; 29 | }; 30 | 31 | 32 | } // namespace Ui::Models 33 | #endif // DOCUMENTLISTITEM__H 34 | -------------------------------------------------------------------------------- /src/DocumentListModel.cpp: -------------------------------------------------------------------------------- 1 | #include "DocumentListModel.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "Benchmark.h" 7 | #include "Document.h" 8 | #include "DocumentListItem.h" 9 | 10 | #define castItem(x) static_cast(x) 11 | using Doc::Document; 12 | 13 | namespace Ui::Models { 14 | 15 | DocumentListModel::DocumentListModel(QObject* parent) : QAbstractListModel{ parent } { 16 | connect(this, &DocumentListModel::tagAdded, this, 17 | [&] { emit numberOfDocumentsChanged(mDocuments.size()); }); 18 | connect(this, &DocumentListModel::documentPermanentlyDeleted, this, 19 | [&] { emit numberOfDocumentsChanged(mDocuments.size()); }); 20 | } 21 | 22 | 23 | int DocumentListModel::rowCount(const QModelIndex& parent) const { 24 | if (!parent.isValid()) { 25 | return mDocuments.size(); 26 | } 27 | 28 | return 0; 29 | } 30 | 31 | 32 | QVariant DocumentListModel::data(const QModelIndex& index, int role) const { 33 | if (mDocuments.empty() || !index.isValid()) { 34 | return {}; 35 | } 36 | 37 | const Document* const doc = mDocuments.at(index.row()); 38 | 39 | switch (role) { 40 | case Qt::EditRole: 41 | case Qt::DisplayRole: 42 | case TitleRole: 43 | return doc->getTitle(); 44 | case PathRole: 45 | return doc->getPath(); 46 | case PinnedRole: 47 | return doc->isPinned(); 48 | case FavoritedRole: 49 | return doc->isFavorited(); 50 | case DeletedRole: 51 | return doc->isDeleted(); 52 | case TagsRole: 53 | case TagAddRole: 54 | case TagDeleteRole: 55 | return { doc->getTags().toList() }; 56 | case Qt::ToolTipRole: { 57 | const QString title = doc->getTitle(); 58 | const QString path = doc->getPath(); 59 | return QStringLiteral("

%1: %2

%3: %4

") 60 | .arg(tr("Title"), title, tr("Path"), path); 61 | } 62 | } 63 | 64 | return {}; 65 | } 66 | 67 | 68 | bool DocumentListModel::setData(const QModelIndex& index, const QVariant& value, int role) { 69 | if (!index.isValid()) { 70 | return false; 71 | } 72 | 73 | bool changed = false; 74 | 75 | switch (role) { 76 | case Qt::EditRole: 77 | case Qt::DisplayRole: 78 | case TitleRole: { 79 | changed = Doc::Utils::setDocumentTitle(getDocumentAt(index), value.toString()); 80 | break; 81 | } 82 | case PinnedRole: 83 | changed = Doc::Utils::setDocumentPinned(getDocumentAt(index), value.toBool()); 84 | break; 85 | case FavoritedRole: 86 | changed = Doc::Utils::setDocumentFavorited(getDocumentAt(index), value.toBool()); 87 | break; 88 | case DeletedRole: 89 | changed = Doc::Utils::setDocumentDeleted(getDocumentAt(index), value.toBool()); 90 | break; 91 | case PermanentlyDeletedRole: 92 | if (!data(index, DeletedRole).toBool()) { 93 | qCritical("Error. Cannot permanently delete a file if it is not marked as 'deleted'"); 94 | } else { 95 | changed = Doc::Utils::permanentlyDeleteFile(getDocumentAt(index)->getPath()); 96 | } 97 | break; 98 | case TagsRole: 99 | case TagAddRole: { 100 | auto doc = getDocumentAt(index); 101 | changed = Doc::Utils::addDocumentTag(doc, value.toString()); 102 | if (changed && !doc->isDeleted()) { 103 | emit tagAdded(value.toString()); 104 | } 105 | break; 106 | } 107 | case TagDeleteRole: 108 | changed = Doc::Utils::delDocumentTag(getDocumentAt(index), value.toString()); 109 | if (changed) { 110 | emit tagDeleted(value.toString()); 111 | } 112 | break; 113 | } 114 | 115 | if (changed) { 116 | emit dataChanged(index, index, { role }); 117 | onDocumentChanged(index, role); 118 | emit numberOfDocumentsChanged(mDocuments.size()); 119 | } 120 | 121 | return changed; 122 | } 123 | 124 | 125 | bool DocumentListModel::setData(Document* document, const QVariant& value, int role) { 126 | if (document == nullptr) { 127 | return false; 128 | } 129 | 130 | if (auto ind = index(document); ind.isValid()) { 131 | return setData(ind, value, role); 132 | } 133 | 134 | return false; 135 | } 136 | 137 | 138 | void DocumentListModel::onDocumentChanged(const QModelIndex& index, int role) { 139 | switch (role) { 140 | case Qt::EditRole: 141 | case Qt::DisplayRole: 142 | case TitleRole: 143 | case PinnedRole: 144 | sort(); 145 | break; 146 | case FavoritedRole: 147 | break; 148 | case DeletedRole: { 149 | auto document = getDocumentAt(index); 150 | if (document->isDeleted()) { // remove tags 151 | for (const QString& tag : document->getTags()) { 152 | emit tagDeleted(tag); 153 | } 154 | } else { 155 | for (const QString& tag : document->getTags()) { 156 | emit tagAdded(tag); 157 | } 158 | } 159 | 160 | removeRow(index.row()); 161 | emit documentDeleted(document); 162 | // sort(); 163 | break; 164 | }; 165 | case PermanentlyDeletedRole: { 166 | auto doc = getDocumentAt(index); 167 | removeRow(index.row()); 168 | emit documentPermanentlyDeleted(doc); 169 | // sort(); 170 | break; 171 | }; 172 | case TagDeleteRole: { 173 | if (mActiveTag.isEmpty()) { 174 | break; 175 | } 176 | 177 | const auto doc = getDocumentAt(index); 178 | if (doc == nullptr) { 179 | break; 180 | } 181 | 182 | if (!doc->containsAllTags(mActiveTag)) { 183 | removeRow(index.row()); 184 | sort(); 185 | } 186 | 187 | break; 188 | }; 189 | } 190 | } 191 | 192 | 193 | void DocumentListModel::sort([[maybe_unused]] int column, [[maybe_unused]] Qt::SortOrder order) { 194 | emit layoutAboutToBeChanged(); 195 | std::sort(mDocuments.begin(), mDocuments.end(), [](auto a, auto b) { return *a < *b; }); 196 | updatePersistentIndexes(); 197 | emit layoutChanged(); 198 | } 199 | 200 | 201 | Document* DocumentListModel::getDocumentAt(int row) { 202 | if (row < mDocuments.size()) { 203 | return mDocuments.at(row); 204 | } 205 | 206 | return nullptr; 207 | } 208 | 209 | 210 | Document* DocumentListModel::getDocumentAt(const QModelIndex& index) { 211 | if (!index.isValid()) { 212 | return nullptr; 213 | } 214 | 215 | return getDocumentAt(index.row()); 216 | } 217 | 218 | 219 | void DocumentListModel::reset() { 220 | beginResetModel(); 221 | mDocuments.clear(); 222 | endResetModel(); 223 | mActiveTag.clear(); 224 | emit numberOfDocumentsChanged(mDocuments.size()); 225 | } 226 | 227 | 228 | void DocumentListModel::setDocuments(const QVector& documents, 229 | const QStringList& activeTag) { 230 | Benchmark b("document list"); 231 | reset(); 232 | beginInsertRows(QModelIndex(), 0, documents.size()); 233 | mDocuments = documents; 234 | mActiveTag = activeTag; 235 | sort(); 236 | endInsertRows(); 237 | emit numberOfDocumentsChanged(mDocuments.size()); 238 | } 239 | 240 | 241 | void DocumentListModel::addDocument(Document* document) { 242 | if (mDocuments.contains(document)) { 243 | return; 244 | } 245 | 246 | beginInsertRows(QModelIndex(), mDocuments.size(), mDocuments.size()); 247 | mDocuments.push_back(document); 248 | sort(); 249 | endInsertRows(); 250 | emit numberOfDocumentsChanged(mDocuments.size()); 251 | } 252 | 253 | 254 | [[deprecated("not used anywhere")]] void DocumentListModel::addDocuments( 255 | const QVector& documents) { 256 | beginInsertRows(QModelIndex(), mDocuments.size(), mDocuments.size()); 257 | 258 | for (const auto& doc : documents) { 259 | mDocuments.push_back(doc); 260 | } 261 | 262 | endInsertRows(); 263 | emit numberOfDocumentsChanged(mDocuments.size()); 264 | } 265 | 266 | 267 | void DocumentListModel::removeRow(int row) { 268 | if (mDocuments.empty() || row < 0 || mDocuments.size() <= row) { 269 | return; 270 | } 271 | 272 | beginRemoveRows(QModelIndex(), row, row); 273 | mDocuments.removeAt(row); 274 | endRemoveRows(); 275 | 276 | emit numberOfDocumentsChanged(mDocuments.size()); 277 | } 278 | 279 | 280 | Qt::ItemFlags DocumentListModel::flags(const QModelIndex& index) const { 281 | return Qt::ItemIsDropEnabled | QAbstractItemModel::flags(index); 282 | } 283 | 284 | 285 | void DocumentListModel::updatePersistentIndexes() { 286 | for (auto oldIndex : persistentIndexList()) { 287 | if (oldIndex.isValid()) { 288 | QModelIndex newIndex = index(castItem(oldIndex.internalPointer())); 289 | changePersistentIndex(oldIndex, newIndex); 290 | } 291 | } 292 | } 293 | 294 | 295 | QModelIndex DocumentListModel::index(DocumentListItem* item) { 296 | if (item == nullptr) { 297 | return {}; 298 | } 299 | 300 | const int row = getRow(item); 301 | return row < 0 ? QModelIndex() : createIndex(row, 0, item); 302 | } 303 | 304 | 305 | QModelIndex DocumentListModel::index(Document* document) { 306 | if (document == nullptr) { 307 | return {}; 308 | } 309 | 310 | const QString path = document->getPath(); 311 | if (path.isEmpty()) { 312 | return {}; 313 | } 314 | 315 | for (int i = 0; i < mDocuments.size(); i++) { 316 | if (mDocuments.at(i)->getPath() == path) { 317 | return index(i, 0, {}); 318 | } 319 | } 320 | 321 | return {}; 322 | } 323 | 324 | 325 | int DocumentListModel::getRow(DocumentListItem* item) { 326 | const QString& itemPath = item->getPath(); 327 | 328 | for (int i = 0; i < mDocuments.size(); i++) { 329 | if (itemPath == mDocuments.at(i)->getPath()) { 330 | return i; 331 | } 332 | } 333 | 334 | return -1; 335 | } 336 | 337 | 338 | QString DocumentListModel::copyPath(const QModelIndex& index) { 339 | if (!index.isValid()) { 340 | return {}; 341 | } 342 | 343 | auto path = getDocumentAt(index)->getPath(); 344 | QApplication::clipboard()->setText(path); 345 | 346 | return path; 347 | } 348 | 349 | 350 | } // namespace Ui::Models 351 | -------------------------------------------------------------------------------- /src/DocumentListModel.h: -------------------------------------------------------------------------------- 1 | #ifndef DOCUMENTLISTMODEL__H 2 | #define DOCUMENTLISTMODEL__H 3 | 4 | #include 5 | #include 6 | 7 | namespace Doc { 8 | class Document; 9 | } 10 | 11 | namespace Ui::Models { 12 | 13 | class DocumentListItem; 14 | 15 | class DocumentListModel : public QAbstractListModel { 16 | Q_OBJECT 17 | public: 18 | enum CustomRoles { 19 | TitleRole = Qt::UserRole, 20 | PathRole, 21 | PinnedRole, 22 | FavoritedRole, 23 | DeletedRole, 24 | PermanentlyDeletedRole, 25 | TagsRole, 26 | TagAddRole, 27 | TagDeleteRole 28 | }; 29 | explicit DocumentListModel(QObject* parent = nullptr); 30 | [[nodiscard]] int rowCount(const QModelIndex& parent) const final; 31 | [[nodiscard]] QVariant data(const QModelIndex& index, int role) const final; 32 | bool setData(const QModelIndex& index, const QVariant& value, int role) final; 33 | bool setData(Doc::Document* document, const QVariant& value, int role); 34 | [[nodiscard]] Qt::ItemFlags flags(const QModelIndex& index) const final; 35 | void sort(int column = 0, Qt::SortOrder order = Qt::AscendingOrder) final; 36 | Doc::Document* getDocumentAt(int row); 37 | Doc::Document* getDocumentAt(const QModelIndex& index); 38 | void reset(); 39 | void setDocuments(const QVector& documents, const QStringList& activeTag); 40 | void addDocument(Doc::Document* document); 41 | void addDocuments(const QVector& documents); 42 | void removeRow(int row); 43 | void updatePersistentIndexes(); 44 | using QAbstractListModel::index; 45 | QModelIndex index(DocumentListItem* item); 46 | QModelIndex index(Doc::Document* document); 47 | int getRow(DocumentListItem* item); 48 | QString copyPath(const QModelIndex& index); 49 | [[nodiscard]] inline auto getActiveTag() const { return mActiveTag; } 50 | 51 | public slots: 52 | void onDocumentChanged(const QModelIndex& index, int role); 53 | 54 | signals: 55 | void documentPermanentlyDeleted(Doc::Document* document); 56 | void documentDeleted(Doc::Document* document); 57 | void tagDeleted(QString tag); 58 | void tagAdded(QString tag); 59 | void numberOfDocumentsChanged(int number); 60 | 61 | private: 62 | QVector mDocuments; 63 | /** 64 | * empty `mActiveTag` means either no selected tag or special tag selected 65 | */ 66 | QStringList mActiveTag; 67 | }; 68 | 69 | } // namespace Ui::Models 70 | 71 | #endif // DOCUMENTLISTMODEL__H 72 | -------------------------------------------------------------------------------- /src/DocumentUtils.cpp: -------------------------------------------------------------------------------- 1 | #include "DocumentUtils.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "Document.h" 9 | #include "TagTreeItem.h" 10 | 11 | const auto headerDelimiter = QStringLiteral("---"); 12 | const auto singleQuote = QStringLiteral("'"); 13 | 14 | QString& enwrap(QString& str, const QString& before, const QString& after); 15 | QString& unwrap(QString& str, const QString& before, const QString& after); 16 | 17 | /** 18 | * replace all the unsupported characters for a filename 19 | */ 20 | QString& sanitizeFilename(QString& filename); 21 | 22 | 23 | namespace Doc::Utils { 24 | 25 | QStringVector getFileContent(const QString& file) { 26 | QFile qf(file); 27 | if (!qf.open(QIODevice::ReadOnly | QIODevice::Text)) { 28 | qCritical() << "Error while trying to get file content of: " << file; 29 | return {}; 30 | } 31 | 32 | QStringVector entire_file; 33 | QTextStream stream(&qf); 34 | stream.setCodec("UTF-8"); 35 | while (!stream.atEnd()) { 36 | entire_file.push_back(stream.readLine()); 37 | } 38 | 39 | return entire_file; 40 | } 41 | 42 | 43 | QString getFileContentAsString(const QString& file) { 44 | QFile qf(file); 45 | if (!qf.open(QIODevice::ReadOnly | QIODevice::Text)) { 46 | qCritical() << "Error while trying to get file content of: " << file; 47 | return {}; 48 | } 49 | 50 | QTextStream stream(&qf); 51 | stream.setCodec("UTF-8"); 52 | return stream.readAll(); 53 | } 54 | 55 | 56 | QStringVector getHeader(const QString& path) { 57 | return getHeader(getFileContent(path)); 58 | } 59 | 60 | 61 | QStringVector getHeader(const QStringVector& fileContent) { 62 | QStringVector header; 63 | if (fileContent.empty() || fileContent.at(0) != headerDelimiter) { 64 | return QStringVector{}; 65 | } 66 | 67 | int headerMark = 0; // how many times "---" have been encountered before stopping (2) 68 | for (const QString& line : fileContent) { 69 | if (headerMark == 2) { 70 | break; // break if we finished the header 71 | } 72 | 73 | if (line == headerDelimiter) { 74 | headerMark++; 75 | } else { 76 | header.push_back(line); // add to the vector if the current line is metadata 77 | } 78 | } 79 | 80 | return (headerMark == 2) ? header : QStringVector{}; 81 | } 82 | 83 | 84 | QString getHeaderLine(const QString& key, const QStringVector& header) { 85 | for (const QString& line : header) { 86 | if (line.startsWith(key)) { 87 | return line; 88 | } 89 | } 90 | 91 | return {}; 92 | } 93 | 94 | 95 | QString getHeaderValue(const QString& key, const QStringVector& header) { 96 | QString line = getHeaderLine(key, header); 97 | 98 | if (line.isEmpty()) { 99 | return {}; 100 | } 101 | 102 | const QStringVector splited = split(line, QStringLiteral(":")); 103 | 104 | if (2 < splited.size()) { 105 | QString res; 106 | 107 | for (int i = 1; i < splited.size(); i++) { 108 | res += splited.at(i) + QStringLiteral(":"); 109 | } 110 | 111 | res.chop(1); 112 | return unwrap(res, singleQuote, singleQuote); 113 | } else if (splited.size() == 2) { 114 | QString res = splited.at(1); 115 | return unwrap(res, singleQuote, singleQuote); 116 | } 117 | 118 | return {}; 119 | } 120 | 121 | 122 | bool setHeaderValue(const QString& key, const QString& value, QStringVector& header) { 123 | auto newLine = Doc::Utils::composeHeaderLine(key, value); 124 | 125 | if (hasHeaderKey(key, header)) { 126 | for (QString& line : header) { 127 | if (line.simplified().startsWith(key)) { 128 | if (line == newLine) { // no change in value; 129 | return false; 130 | } 131 | 132 | line = newLine; 133 | return true; 134 | } 135 | } 136 | } 137 | 138 | header.push_back(newLine); 139 | return true; 140 | } 141 | 142 | 143 | bool hasHeaderKey(const QString& key, const QStringVector& header) { 144 | return std::any_of(header.cbegin(), header.cend(), 145 | [&key](const auto& line) { return line.simplified().startsWith(key); }); 146 | } 147 | 148 | 149 | bool deleteHeaderLine(const QString& key, QStringVector& header) { 150 | for (int i = 0; i < header.size(); i++) { 151 | const QString& line = header.at(i); 152 | if (line.simplified().startsWith(key)) { 153 | header.removeAt(i); 154 | return true; 155 | } 156 | } 157 | 158 | return false; 159 | } 160 | 161 | void createHeader(const QString& file, QString title) { 162 | title = (title.isEmpty()) ? QFileInfo(file).baseName() : title; 163 | 164 | QStringVector header; 165 | header.push_back(headerDelimiter); 166 | header.push_back(composeHeaderLine(titleKey, title)); 167 | header.push_back(headerDelimiter); 168 | header.push_back(("")); 169 | 170 | QStringVector entire_file = getFileContent(file); 171 | for (const QString& i : entire_file) { 172 | header.push_back(i); 173 | } 174 | 175 | writeContentToFile(header, file); 176 | } 177 | 178 | 179 | void replaceHeader(Document* document, const QStringVector& newHeader) { 180 | if (newHeader.empty()) { 181 | qCritical() << "Cannot replace a header by an empty one"; 182 | return; 183 | } 184 | 185 | if (getHeader(document->getPath()).isEmpty()) { 186 | createHeader(document->getPath()); 187 | } 188 | 189 | auto fileContent = getFileContent(document->getPath()); 190 | 191 | QStringVector newContent = newHeader; 192 | newContent.prepend(headerDelimiter); 193 | newContent.append(headerDelimiter); 194 | 195 | 196 | int delimiterCount = 0; 197 | for (const QString& line : fileContent) { 198 | if (2 <= delimiterCount) { 199 | newContent.push_back(line); 200 | continue; 201 | } 202 | 203 | if (line == headerDelimiter) { 204 | delimiterCount++; 205 | } 206 | } 207 | 208 | writeContentToFile(newContent, document->getPath()); 209 | document->setHeader(newHeader); 210 | // document->load(); 211 | } 212 | 213 | 214 | bool writeContentToFile(const QStringVector& content, const QString& file) { 215 | QFile qf(file); 216 | if (!qf.open(QIODevice::WriteOnly | QIODevice::Text)) { 217 | qCritical() << "Error while trying to write content into: " << file; 218 | return false; 219 | } 220 | 221 | QTextStream stream(&qf); 222 | stream.setCodec("UTF-8"); 223 | 224 | for (auto& line : content) { 225 | stream << line; 226 | Qt::endl(stream); 227 | } 228 | 229 | stream.flush(); 230 | qf.close(); 231 | 232 | return true; 233 | } 234 | 235 | 236 | QStringVector splitTags(const QStringVector& header) { 237 | QString tagLine{ getHeaderValue(tagsKey, header) }; 238 | 239 | if (tagLine.isEmpty()) { 240 | return {}; 241 | } 242 | 243 | unwrap(tagLine, QStringLiteral("["), QStringLiteral("]")); 244 | 245 | QStringVector result; 246 | for (const QString& tag : split(tagLine, QStringLiteral(", "))) { 247 | result.push_back(tag.simplified()); 248 | } 249 | 250 | return result; 251 | } 252 | 253 | 254 | QStringVector deconstructTag(const QString& tag) { 255 | return split(tag, QStringLiteral("/")); 256 | } 257 | 258 | 259 | QStringVector getPathList(const QString& directory) { 260 | if (directory.isEmpty()) { 261 | return {}; 262 | } 263 | 264 | QVector list; 265 | 266 | if (!QDir(directory).exists()) { 267 | return list; 268 | } 269 | 270 | QDirIterator p(directory, QDir::Files, QDirIterator::Subdirectories); 271 | while (p.hasNext()) { 272 | QString pp = p.next(); 273 | if (isMD(pp)) { 274 | list.push_back(pp); 275 | } 276 | } 277 | 278 | return list; 279 | } 280 | 281 | 282 | bool isMD(const QString& file) { 283 | const auto file_ = file.toLower(); 284 | static const std::vector extension{ ".md", ".markdown", ".mdwn", ".mdown", 285 | ".mdtxt", ".mdtext", ".mkd" }; 286 | return std::any_of(extension.begin(), extension.end(), 287 | [&file_](auto ext) { return file_.endsWith(ext); }); 288 | } 289 | 290 | 291 | QString composeHeaderLine(const QString& key, const QString& value) { 292 | return QString("%1: %2").arg(key, value); 293 | } 294 | 295 | 296 | QString arrayToString(const QStringVector& array) { 297 | QString res; 298 | auto arraySize = array.size(); 299 | for (decltype(arraySize) i = 0; i < arraySize; i++) { 300 | res.append(array[i].simplified()); 301 | if (i != arraySize - 1) { 302 | res.append(", "); 303 | } 304 | } 305 | 306 | return enwrap(res, QStringLiteral("["), QStringLiteral("]")); 307 | } 308 | 309 | 310 | bool setDocumentTitle(Doc::Document* document, const QString& value) { 311 | if (document == nullptr || document->getTitle() == value.simplified()) { 312 | return false; 313 | } 314 | 315 | auto header = document->getHeader(); 316 | 317 | if (setHeaderValue(titleKey, value.simplified(), header)) { 318 | replaceHeader(document, header); 319 | document->load(); 320 | setDocumentPath(document, value.simplified()); 321 | return true; 322 | } 323 | 324 | return false; 325 | } 326 | 327 | 328 | void setDocumentPath(Doc::Document* document, const QString& documentTitle) { 329 | if (document == nullptr || documentTitle.isEmpty()) { 330 | return; 331 | } 332 | 333 | const QString oldFilePath = document->getPath(); 334 | const QString absolutePath = QFileInfo(oldFilePath).absolutePath(); 335 | QString newFilePath; 336 | int n = 0; 337 | 338 | do { 339 | newFilePath = documentTitle; 340 | if (n != 0) { 341 | newFilePath = QString("%1 (%2)").arg(newFilePath, QString::number(n)); 342 | } 343 | 344 | sanitizeFilename(newFilePath); 345 | newFilePath = absolutePath + "/" + newFilePath; 346 | if (!newFilePath.endsWith(".md")) { 347 | newFilePath += ".md"; 348 | } 349 | n++; 350 | } while (QFile::exists(newFilePath) && oldFilePath != newFilePath); 351 | 352 | if (oldFilePath == newFilePath) { 353 | return; 354 | } 355 | 356 | if (QFile::rename(oldFilePath, newFilePath)) { 357 | document->setPath(newFilePath); 358 | } 359 | } 360 | 361 | 362 | bool setDocumentBoolValue(Doc::Document* document, bool value, const QString& key) { 363 | if (document == nullptr) { 364 | return false; 365 | } 366 | 367 | QString valueStr = (value) ? trueStr : falseStr; 368 | auto header = document->getHeader(); 369 | 370 | if (setHeaderValue(key, valueStr, header)) { 371 | replaceHeader(document, header); 372 | 373 | if (!value) { 374 | deleteHeaderLine(key, header); 375 | replaceHeader(document, header); 376 | } 377 | 378 | return true; 379 | } 380 | 381 | return false; 382 | } 383 | 384 | 385 | bool setDocumentPinned(Doc::Document* document, bool value) { 386 | if (setDocumentBoolValue(document, value, pinnedKey)) { 387 | document->setPinned(value); 388 | return true; 389 | } 390 | 391 | return false; 392 | } 393 | 394 | 395 | bool setDocumentFavorited(Doc::Document* document, bool value) { 396 | if (setDocumentBoolValue(document, value, favoritedKey)) { 397 | document->setFavorited(value); 398 | return true; 399 | } 400 | 401 | return false; 402 | } 403 | 404 | 405 | bool setDocumentDeleted(Doc::Document* document, bool value) { 406 | if (setDocumentBoolValue(document, value, deletedKey)) { 407 | document->setDeleted(value); 408 | return true; 409 | } 410 | 411 | return false; 412 | } 413 | 414 | 415 | bool permanentlyDeleteFile(const QString& path) { 416 | if (!QFile::exists(path)) { 417 | qCritical() << path << "does not exist."; 418 | return false; 419 | } 420 | 421 | if (!QFile::remove(path)) { 422 | qCritical() << "Error while removing " << path; 423 | return false; 424 | } 425 | 426 | return true; 427 | } 428 | 429 | 430 | bool addDocumentTag(Doc::Document* document, QString value) { 431 | sanitizeTag(value); 432 | if (document == nullptr || !isValidTag(value)) { 433 | return false; 434 | } 435 | 436 | if (document->getTags().contains(value.simplified())) { 437 | return false; 438 | } 439 | 440 | document->addTag(value); 441 | auto tagsStr = arrayToString(document->getTags()); 442 | auto header = document->getHeader(); 443 | 444 | if (setHeaderValue(tagsKey, tagsStr, header)) { 445 | replaceHeader(document, header); 446 | return true; 447 | } 448 | 449 | return false; 450 | } 451 | 452 | 453 | bool delDocumentTag(Doc::Document* document, const QString& value) { 454 | if (document == nullptr || document->getTags().isEmpty() || 455 | !document->containsExactTag(value.simplified())) { 456 | return false; 457 | } 458 | 459 | auto header = document->getHeader(); 460 | auto tags = document->getTags(); 461 | 462 | tags.removeAll(value.simplified()); 463 | 464 | if (tags.isEmpty()) { 465 | deleteHeaderLine(tagsKey, header); 466 | } else { 467 | auto tagsStr = arrayToString(tags); 468 | setHeaderValue(tagsKey, tagsStr, header); 469 | } 470 | 471 | replaceHeader(document, header); 472 | document->delTag(value); 473 | 474 | return true; 475 | } 476 | 477 | 478 | QStringVector split(const QString& s, const QString& delimiter) { 479 | const QStringList qsl = s.split(delimiter); 480 | QStringVector res; 481 | for (const QString& i : qsl) { 482 | res.push_back(i); 483 | } 484 | 485 | return res; 486 | } 487 | 488 | 489 | bool isValidTag(const QString& tag) { 490 | if (tag.isEmpty()) { 491 | return false; 492 | } 493 | 494 | if (Ui::Models::TagTreeItem::specialTags.contains(tag)) { 495 | qCritical() << "Error. Cannot add a basic tag to a file"; 496 | return false; 497 | } 498 | 499 | QStringList forbiddenChars{ "," }; 500 | for (const auto& s : forbiddenChars) { 501 | if (tag.contains(s)) { 502 | return false; 503 | } 504 | } 505 | 506 | // check if it starts or ends with a slash 507 | return !(tag.startsWith(QLatin1String("/")) || tag.endsWith(QLatin1String("/"))); 508 | } 509 | 510 | 511 | QString& sanitizeTag(QString& tag) { 512 | tag = tag.simplified(); 513 | 514 | while (tag.startsWith(QLatin1String("/"))) { 515 | tag.remove(0, 1); 516 | } 517 | 518 | while (tag.endsWith(QLatin1String("/"))) { 519 | tag.chop(1); 520 | } 521 | 522 | while (tag.contains(QLatin1String("//"))) { 523 | tag.replace(QLatin1String("//"), QLatin1String("/")); 524 | } 525 | 526 | while (tag.contains(QLatin1String("\\"))) { 527 | tag.replace(QLatin1String("\\"), QLatin1String("/")); 528 | } 529 | 530 | return tag; 531 | } 532 | 533 | 534 | std::optional createNewFile(const QString& path, QString title) { 535 | if (path.isEmpty()) { 536 | return {}; 537 | } 538 | 539 | if (!QFileInfo::exists(path)) { 540 | return {}; 541 | } 542 | 543 | title = (title.isEmpty()) ? QStringLiteral("untitled") : title; 544 | QString rawTitle(title); 545 | sanitizeFilename(title); 546 | QString filePath = ""; 547 | int num = 0; 548 | do { 549 | if (num == 0) { 550 | filePath = QString("%1/%2.md").arg(path, title); 551 | } else { 552 | filePath = QString("%1/%2 (%3).md").arg(path, title).arg(num); 553 | } 554 | 555 | num++; 556 | } while (QFile::exists(filePath)); 557 | 558 | QFile file(filePath); 559 | if (!file.open(QIODevice::WriteOnly)) { 560 | qWarning() << "failed to create: " << filePath; 561 | return {}; 562 | } 563 | 564 | file.close(); 565 | createHeader(filePath, title); 566 | 567 | // write title to the file 568 | auto content = getFileContent(filePath); 569 | content.push_back(QLatin1String("")); 570 | content.push_back(rawTitle); 571 | content.push_back(QLatin1String("==========")); 572 | content.push_back(QLatin1String("")); 573 | 574 | writeContentToFile(content, filePath); 575 | 576 | return filePath; 577 | } 578 | 579 | } // namespace Doc::Utils 580 | 581 | 582 | /** 583 | * add a particle before and after the string 584 | */ 585 | QString& enwrap(QString& str, const QString& before, const QString& after) { 586 | str = str.simplified(); 587 | str.prepend(before); 588 | str.append(after); 589 | return str; 590 | } 591 | 592 | /** 593 | * remove a particle before and after the string 594 | */ 595 | QString& unwrap(QString& str, const QString& before, const QString& after) { 596 | str = str.simplified(); 597 | 598 | if (str.size() < 2 || !(str.startsWith(before) && str.endsWith(after))) { 599 | return str; 600 | } 601 | 602 | str.chop(1); 603 | str.remove(0, 1); 604 | 605 | return str; 606 | } 607 | 608 | 609 | QString& sanitizeFilename(QString& filename) { 610 | filename = filename.simplified(); 611 | 612 | filename.replace(QLatin1String(":"), QLatin1String(";")); 613 | filename.replace(QLatin1String("/"), QLatin1String("-")); 614 | filename.replace(QLatin1String("\\"), QLatin1String("-")); 615 | filename.replace(QLatin1String("|"), QLatin1String("-")); 616 | filename.replace(QLatin1String("<"), QLatin1String("(")); 617 | filename.replace(QLatin1String(">"), QLatin1String(")")); 618 | filename.replace(QLatin1String("*"), QLatin1String("-")); 619 | filename.replace(QLatin1String("?"), QLatin1String("")); 620 | filename.replace(QLatin1String("\""), QLatin1String("'")); 621 | 622 | return filename; 623 | } 624 | -------------------------------------------------------------------------------- /src/DocumentUtils.h: -------------------------------------------------------------------------------- 1 | #ifndef DOCUMENTUTILS__H 2 | #define DOCUMENTUTILS__H 3 | 4 | #include 5 | #include 6 | 7 | using QStringVector = QVector; 8 | namespace Doc { 9 | class Document; 10 | } 11 | 12 | /* header keys */ 13 | const auto pinnedKey = QStringLiteral("pinned"); 14 | const auto deletedKey = QStringLiteral("deleted"); 15 | const auto favoritedKey = QStringLiteral("favorited"); 16 | const auto titleKey = QStringLiteral("title"); 17 | const auto tagsKey = QStringLiteral("tags"); 18 | const auto trueStr = QStringLiteral("true"); 19 | const auto falseStr = QStringLiteral("false"); 20 | 21 | namespace Doc::Utils { 22 | 23 | /** 24 | * extract a header from the file content 25 | */ 26 | [[nodiscard]] QStringVector getHeader(const QStringVector& fileContent); 27 | /** 28 | * return a header from a file 29 | */ 30 | [[nodiscard]] QStringVector getHeader(const QString& path); 31 | /** 32 | * return the content of an entire file, each line is a string 33 | */ 34 | [[nodiscard]] QStringVector getFileContent(const QString& file); 35 | /** 36 | * return the content of an entire file as a single string 37 | */ 38 | [[nodiscard]] QString getFileContentAsString(const QString& file); 39 | /** 40 | * look in the header for an entry that starts with a key and return the entire line 41 | */ 42 | [[nodiscard]] QString getHeaderLine(const QString& key, const QStringVector& header); 43 | /** 44 | * look in the header for an entry and return it as a string 45 | */ 46 | [[nodiscard]] QString getHeaderValue(const QString& key, const QStringVector& header); 47 | /** 48 | * add a line to a header, or change its value if it already exists 49 | */ 50 | bool setHeaderValue(const QString& key, const QString& value, QStringVector& header); 51 | /** 52 | * check if a header contains a key 53 | */ 54 | bool hasHeaderKey(const QString& key, const QStringVector& header); 55 | /** 56 | * delete a line that has a key in a header 57 | */ 58 | bool deleteHeaderLine(const QString& key, QStringVector& header); 59 | /** 60 | * check if a file contains a header. If not, create one with a title 61 | */ 62 | void createHeader(const QString& file, QString title = ""); 63 | /** 64 | * replace the header inside the file by the one provided as argument 65 | */ 66 | void replaceHeader(Document* document, const QStringVector& newHeader); 67 | /** 68 | * Write a sequence of lines into a file 69 | */ 70 | bool writeContentToFile(const QStringVector& content, const QString& filepath); 71 | /** 72 | * find the tag line inside the header, and return the content in vector 73 | */ 74 | [[nodiscard]] QStringVector splitTags(const QStringVector& header); 75 | /** 76 | * "a/b/c" => {a, b, c} 77 | */ 78 | [[nodiscard]] QStringVector deconstructTag(const QString& tag); 79 | /** 80 | * in the provided directory, look for any valid file and return them in a vector 81 | */ 82 | [[nodiscard]] QStringVector getPathList(const QString& directory); 83 | /** 84 | * check if a file as a markdown extension 85 | */ 86 | [[nodiscard]] bool isMD(const QString& file); 87 | /** 88 | * produce a header entry line 89 | */ 90 | [[nodiscard]] QString composeHeaderLine(const QString& key, const QString& value); 91 | /** 92 | * turn a list of strings into its string representation 93 | */ 94 | [[nodiscard]] QString arrayToString(const QStringVector& array); 95 | /** 96 | * modify a header entry and write the result into a file. 97 | * return true if something is changed. false if not. 98 | */ 99 | bool setDocumentTitle(Doc::Document* document, const QString& value); 100 | /** 101 | * rename a document 102 | */ 103 | void setDocumentPath(Doc::Document* document, const QString& documentTitle); 104 | /** 105 | * applied the pinned value to the Document and change the underlying file accordingly 106 | */ 107 | bool setDocumentPinned(Doc::Document* document, bool value); 108 | /** 109 | * applied the favorited value to the Document and change the underlying file accordingly 110 | */ 111 | bool setDocumentFavorited(Doc::Document* document, bool value); 112 | /** 113 | * applied the deleted value to the Document and change the underlying file accordingly 114 | */ 115 | bool setDocumentDeleted(Doc::Document* document, bool value); 116 | /** 117 | * permanently delete from disk the underlying file of a Document 118 | */ 119 | bool permanentlyDeleteFile(const QString& path); 120 | /** 121 | * add a tag value to the Document and change the underlying file accordingly 122 | */ 123 | bool addDocumentTag(Doc::Document* document, QString value); 124 | /** 125 | * delete a single tag string. 126 | * If the tag list becomes empty, 127 | * the whole tag entry is removed from the header. 128 | */ 129 | bool delDocumentTag(Doc::Document* document, const QString& value); 130 | /** 131 | * remove whitespaces and the obvious invalid chars 132 | */ 133 | QString& sanitizeTag(QString& tag); 134 | /** 135 | * sanitize the tag and check if it is valid 136 | * special tags being invalid tags because this function is meant to add custom tags 137 | */ 138 | bool isValidTag(const QString& tag); 139 | /** 140 | * split a single string into particles and return them in a vector of strings 141 | * for example: 142 | * input: "Notebooks/sheets/random" 143 | * output: {"Notebooks", "sheets, "random"} 144 | */ 145 | QStringVector split(const QString& s, const QString& delimiter); 146 | /** 147 | * create a new file from the path and the title 148 | */ 149 | std::optional createNewFile(const QString& path, QString title = ""); 150 | 151 | } // namespace Doc::Utils 152 | 153 | #endif // DOCUMENTUTILS__H 154 | -------------------------------------------------------------------------------- /src/DocumentsListView.cpp: -------------------------------------------------------------------------------- 1 | #include "DocumentsListView.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include "Document.h" 11 | #include "DocumentInfoDialog.h" 12 | #include "DocumentListDelegate.h" 13 | #include "DocumentListModel.h" 14 | 15 | using Ui::Models::DocumentListModel; 16 | using Ui::Delegates::DocumentListDelegate; 17 | using std::make_unique; 18 | namespace Settings = Ui::Settings; 19 | 20 | const auto ctrl_p = QKeySequence::fromString(QStringLiteral("Ctrl+p")); 21 | const auto ctrl_s = QKeySequence::fromString(QStringLiteral("Ctrl+s")); 22 | const auto ctrl_e = QKeySequence::fromString(QStringLiteral("Ctrl+e")); 23 | const auto space_ = QKeySequence::fromString(QStringLiteral("Space")); 24 | constexpr QSize iconSize_(35, 20); 25 | 26 | DocumentsListView::DocumentsListView(QWidget* parent) 27 | : QListView(parent), mDeletegate{ new DocumentListDelegate(this) } { 28 | setItemDelegate(mDeletegate); 29 | setDragDropMode(DropOnly); 30 | setContextMenuPolicy(Qt::CustomContextMenu); 31 | setSelectionMode(QAbstractItemView::SingleSelection); 32 | setIconSize(iconSize_); 33 | setUniformItemSizes(true); 34 | setDragEnabled(true); 35 | setAcceptDrops(true); 36 | setDropIndicatorShown(true); 37 | // setAlternatingRowColors(true); 38 | 39 | connect(this, &DocumentsListView::clicked, this, &DocumentsListView::onDocumentSelected); 40 | connect(this, &DocumentsListView::doubleClicked, this, &DocumentsListView::openDocumentAt); 41 | connect(this, &DocumentsListView::customContextMenuRequested, this, 42 | &DocumentsListView::onCustomContextMenuRequested); 43 | 44 | auto toggleBoolPin = [this] { toggleBool(currentIndex(), DocumentListModel::PinnedRole); }; 45 | auto toggleBoolFav = [this] { toggleBool(currentIndex(), DocumentListModel::FavoritedRole); }; 46 | auto launchDID = [this] { launchDocumentInfoDialog(currentIndex()); }; 47 | auto docSelected = [this] { onDocumentSelected(currentIndex()); }; 48 | mShortcuts.push_back(make_unique(ctrl_p, this, toggleBoolPin)); 49 | mShortcuts.push_back(make_unique(ctrl_s, this, toggleBoolFav)); 50 | mShortcuts.push_back(make_unique(ctrl_e, this, launchDID)); 51 | mShortcuts.push_back(make_unique(space_, this, docSelected)); 52 | } 53 | 54 | 55 | DocumentsListView::~DocumentsListView() { 56 | delete mDeletegate; 57 | } 58 | 59 | 60 | void DocumentsListView::openCurrentDocument() { 61 | openDocumentAt(currentIndex()); 62 | } 63 | 64 | 65 | void DocumentsListView::onCustomContextMenuRequested(const QPoint& pos) { 66 | const auto index = indexAt(pos); 67 | if (!index.isValid()) { 68 | return; 69 | } 70 | 71 | Doc::Document* const doc = model()->getDocumentAt(index.row()); 72 | 73 | if (doc == nullptr) { 74 | return; 75 | } 76 | 77 | auto menu = std::make_unique(); 78 | 79 | menu->addAction(tr("Open"), this, [this, doc] { 80 | emit documentToOpen(doc->getPath(), Ui::Settings::loadDefaultExternalReaders()); 81 | }); 82 | 83 | if (const auto readers = Settings::loadExternalReaders(); !readers.isEmpty()) { 84 | auto openWith = menu->addMenu(tr("Open With")); 85 | for (const auto& editor : Settings::loadExternalReaders()) { 86 | openWith->addAction( 87 | editor, this, [this, editor, doc] { emit documentToOpen(doc->getPath(), editor); }); 88 | } 89 | } 90 | 91 | if (!doc->isDeleted()) { 92 | menu->addSeparator(); 93 | 94 | auto editAction = 95 | menu->addAction(tr("Edit"), this, [this, index] { launchDocumentInfoDialog(index); }); 96 | 97 | menu->addAction(tr("Add a new tag"), this, [this, index] { addTagDialog(index); }); 98 | 99 | const QString pinStr = doc->isPinned() ? tr("Unpin") : tr("Pin To Top"); 100 | auto pinAction = menu->addAction( 101 | pinStr, this, [this, index] { toggleBool(index, DocumentListModel::PinnedRole); }); 102 | auto favAction = menu->addAction(tr("Favorite"), this, [this, index] { 103 | toggleBool(index, DocumentListModel::FavoritedRole); 104 | }); 105 | 106 | editAction->setShortcut(ctrl_e); 107 | pinAction->setShortcut(ctrl_p); 108 | favAction->setShortcut(ctrl_s); 109 | pinAction->setCheckable(true); 110 | favAction->setCheckable(true); 111 | pinAction->setChecked(doc->isPinned()); 112 | favAction->setChecked(doc->isFavorited()); 113 | } 114 | 115 | menu->addSeparator(); 116 | menu->addAction(tr("Copy path"), this, [this, index] { model()->copyPath(index); }); 117 | 118 | if (doc->isDeleted()) { 119 | menu->addAction(tr("Restore"), this, 120 | [this, index] { model()->setData(index, false, model()->DeletedRole); }); 121 | menu->addAction(tr("Delete Permanently"), this, 122 | [this, index] { permanentlyDelete(index); }); 123 | } else { 124 | menu->addAction(tr("Move to Trash"), this, 125 | [this, index] { model()->setData(index, true, model()->DeletedRole); }); 126 | } 127 | 128 | menu->exec(mapToGlobal(pos)); 129 | } 130 | 131 | 132 | void DocumentsListView::onDocumentSelected(const QModelIndex& index) { 133 | if (!index.isValid()) { 134 | return; 135 | } 136 | 137 | if (auto doc = model()->getDocumentAt(index)) { 138 | emit documentSelected(doc); 139 | } 140 | } 141 | 142 | 143 | void DocumentsListView::addTagDialog(const QModelIndex& index) { 144 | const QString title = tr("Append New Tag"); 145 | const QString label = tr("Write the new Tag to append"); 146 | auto tag = QInputDialog::getText(this, title, label, QLineEdit::Normal); 147 | 148 | Doc::Utils::sanitizeTag(tag); 149 | if (Doc::Utils::isValidTag(tag)) { 150 | model()->setData(index, tag, model()->TagAddRole); 151 | } 152 | } 153 | 154 | 155 | void DocumentsListView::openDocumentAt(const QModelIndex& index) { 156 | if (!index.isValid()) { 157 | return; 158 | } 159 | 160 | auto path = model()->data(index, model()->PathRole).toString(); 161 | emit documentToOpen(path, Settings::loadDefaultExternalReaders()); 162 | } 163 | 164 | 165 | void DocumentsListView::permanentlyDelete(const QModelIndex& index) { 166 | if (!index.isValid()) { 167 | return; 168 | } 169 | 170 | auto doc = model()->getDocumentAt(index); 171 | auto res = QMessageBox::question( 172 | this, tr("Delete Permanently"), 173 | tr("Do you want to permanently delete \"%1\"").arg(doc->getTitle())); 174 | 175 | if (res == QMessageBox::Yes) { 176 | model()->setData(index, {}, model()->PermanentlyDeletedRole); 177 | } 178 | } 179 | 180 | 181 | void DocumentsListView::launchDocumentInfoDialog(const QModelIndex& index) { 182 | if (!index.isValid()) { 183 | return; 184 | } 185 | 186 | auto document = model()->getDocumentAt(index); 187 | auto dialog = std::make_unique(document, nullptr); 188 | if (dialog->exec() == dialog->Rejected) { 189 | return; 190 | } 191 | 192 | if (bool favorited = dialog->isFavorited(); document->isFavorited() != favorited) { 193 | model()->setData(document, favorited, Ui::Models::DocumentListModel::FavoritedRole); 194 | } 195 | 196 | if (bool pinned = dialog->isPinned(); document->isPinned() != pinned) { 197 | model()->setData(document, pinned, Ui::Models::DocumentListModel::PinnedRole); 198 | } 199 | 200 | if (auto title = dialog->getTitle(); document->getTitle() != title) { 201 | model()->setData(document, title, Ui::Models::DocumentListModel::TitleRole); 202 | } 203 | 204 | if (auto tags = dialog->getTags(); document->getTags() != tags) { 205 | // check if any deleted tags 206 | for (QString tag : document->getTags()) { 207 | Doc::Utils::sanitizeTag(tag); 208 | if (!Doc::Utils::isValidTag(tag)) { 209 | continue; 210 | } 211 | 212 | if (!dialog->getTags().contains(tag)) { 213 | model()->setData(document, tag, Ui::Models::DocumentListModel::TagDeleteRole); 214 | } 215 | } 216 | 217 | // check for new tags and add them 218 | for (QString tag : tags) { 219 | Doc::Utils::sanitizeTag(tag); 220 | if (!Doc::Utils::isValidTag(tag)) { 221 | continue; 222 | } 223 | 224 | if (!document->containsExactTag(tag)) { 225 | model()->setData(document, tag, Ui::Models::DocumentListModel::TagAddRole); 226 | } 227 | } 228 | } 229 | 230 | setCurrentDocument(document); 231 | } 232 | 233 | 234 | void DocumentsListView::dropEvent(QDropEvent* event) { 235 | const auto data = event->mimeData(); 236 | if (data->hasText() && !data->text().isEmpty()) { 237 | const QString tag = data->text(); 238 | auto index = indexAt(event->pos()); 239 | Doc::Document* doc = model()->getDocumentAt(index); 240 | 241 | if (doc == nullptr) { 242 | return; 243 | } 244 | 245 | auto actionText = tr("Add the tag '%1'").arg(data->text()); 246 | 247 | // context menu 248 | auto menu = std::make_unique(); 249 | auto addTagsAction = menu->addAction(actionText); 250 | connect(addTagsAction, &QAction::triggered, this, 251 | [=] { model()->setData(index, tag, model()->TagsRole); }); 252 | 253 | menu->exec(mapToGlobal(event->pos())); 254 | 255 | event->setDropAction(Qt::CopyAction); 256 | event->accept(); 257 | 258 | } else { 259 | event->ignore(); 260 | } 261 | } 262 | 263 | 264 | void DocumentsListView::dragMoveEvent(QDragMoveEvent* event) { 265 | if (event->mimeData()->hasText()) { 266 | event->setDropAction(Qt::CopyAction); 267 | event->accept(); 268 | } else { 269 | event->ignore(); 270 | } 271 | } 272 | 273 | 274 | void DocumentsListView::dragEnterEvent(QDragEnterEvent* event) { 275 | if (event->mimeData()->hasText()) { 276 | event->accept(); 277 | } else { 278 | event->ignore(); 279 | } 280 | } 281 | 282 | 283 | DocumentListModel* DocumentsListView::model() { 284 | return static_cast(QListView::model()); 285 | } 286 | 287 | 288 | void DocumentsListView::setCurrentDocument(Doc::Document* document) { 289 | if (auto modelIndex = model()->index(document); modelIndex.isValid()) { 290 | setCurrentIndex(modelIndex); 291 | } 292 | } 293 | 294 | 295 | void DocumentsListView::toggleBool(const QModelIndex& index, int role) { 296 | if (!index.isValid()) { 297 | return; 298 | } 299 | 300 | bool value = false; 301 | auto document = model()->getDocumentAt(index.row()); 302 | 303 | switch (role) { 304 | case DocumentListModel::PinnedRole: 305 | value = !model()->data(index, DocumentListModel::PinnedRole).toBool(); 306 | break; 307 | case DocumentListModel::FavoritedRole: 308 | value = !model()->data(index, DocumentListModel::FavoritedRole).toBool(); 309 | break; 310 | case DocumentListModel::DeletedRole: 311 | value = !model()->data(index, DocumentListModel::DeletedRole).toBool(); 312 | break; 313 | default: 314 | return; 315 | } 316 | 317 | model()->setData(index, value, role); 318 | 319 | setCurrentDocument(document); 320 | } 321 | -------------------------------------------------------------------------------- /src/DocumentsListView.h: -------------------------------------------------------------------------------- 1 | #ifndef DOCUMENTLISTVIEW__H 2 | #define DOCUMENTLISTVIEW__H 3 | 4 | #include 5 | 6 | class QShortcut; 7 | 8 | namespace Ui::Models { 9 | class DocumentListModel; 10 | } 11 | 12 | namespace Ui::Delegates { 13 | class DocumentListDelegate; 14 | } 15 | 16 | namespace Doc { 17 | class Document; 18 | } 19 | 20 | QT_BEGIN_NAMESPACE 21 | namespace Ui { 22 | class DocumentsListView; 23 | } 24 | QT_END_NAMESPACE 25 | 26 | 27 | class DocumentsListView : public QListView { 28 | Q_OBJECT 29 | public: 30 | explicit DocumentsListView(QWidget* parent = nullptr); 31 | ~DocumentsListView() override; 32 | 33 | public slots: 34 | void openCurrentDocument(); 35 | 36 | private slots: 37 | void onCustomContextMenuRequested(const QPoint& pos); 38 | void onDocumentSelected(const QModelIndex& index); 39 | void addTagDialog(const QModelIndex& index); 40 | void openDocumentAt(const QModelIndex& index); 41 | void launchDocumentInfoDialog(const QModelIndex& index); 42 | void permanentlyDelete(const QModelIndex& index); 43 | 44 | private: 45 | void dropEvent(QDropEvent* event) final; 46 | void dragMoveEvent(QDragMoveEvent* event) final; 47 | void dragEnterEvent(QDragEnterEvent* event) final; 48 | Ui::Models::DocumentListModel* model(); 49 | void setCurrentDocument(Doc::Document* document); 50 | void toggleBool(const QModelIndex& index, int role); 51 | 52 | signals: 53 | void documentSelected(Doc::Document* document); 54 | void documentToOpen(const QString& path, const QString& editor = ""); 55 | 56 | private: 57 | Ui::Delegates::DocumentListDelegate* mDeletegate; 58 | std::vector> mShortcuts; 59 | }; 60 | 61 | 62 | #endif // DOCUMENTLISTVIEW__H 63 | -------------------------------------------------------------------------------- /src/ExternalReadersCentralWidget.cpp: -------------------------------------------------------------------------------- 1 | #include "ExternalReadersCentralWidget.h" 2 | #include 3 | #include 4 | #include 5 | #include "Settings.h" 6 | #include "ui_ExternalReadersCentralWidget.h" 7 | 8 | 9 | ExternalReadersCentralWidget::ExternalReadersCentralWidget(QWidget* parent) 10 | : QWidget(parent), ui(new Ui::ExternalReadersCentralWidget) { 11 | ui->setupUi(this); 12 | 13 | mListViewModel = std::make_unique(); 14 | mListViewModel->setStringList(Ui::Settings::loadExternalReaders()); 15 | ui->listView->setModel(mListViewModel.get()); 16 | 17 | connect(ui->addLine, &QPushButton::clicked, this, &ExternalReadersCentralWidget::addItem); 18 | connect(ui->rmButton, &QPushButton::clicked, this, &ExternalReadersCentralWidget::delItem); 19 | connect(ui->upButton, &QPushButton::clicked, this, &ExternalReadersCentralWidget::itemUp); 20 | connect(ui->downButton, &QPushButton::clicked, this, &ExternalReadersCentralWidget::itemDown); 21 | 22 | mRetShortcut = std::make_unique(QKeySequence(QStringLiteral("Return")), this); 23 | connect(mRetShortcut.get(), &QShortcut::activated, this, 24 | &ExternalReadersCentralWidget::addItem); 25 | } 26 | 27 | 28 | ExternalReadersCentralWidget::~ExternalReadersCentralWidget() { 29 | delete ui; 30 | } 31 | 32 | 33 | void ExternalReadersCentralWidget::itemUp() { 34 | auto row = ui->listView->currentIndex().row(); 35 | mListViewModel->moveRow({}, row, {}, row - 1); 36 | } 37 | 38 | 39 | void ExternalReadersCentralWidget::itemDown() { 40 | auto row = ui->listView->currentIndex().row(); 41 | // TODO: it should be 1 instead of 2. check this with other version, maybe a Qt bug 42 | mListViewModel->moveRow({}, row, {}, row + 2); 43 | } 44 | 45 | 46 | // void ExternalReadersCentralWidget::browse() { 47 | // auto f = QFileDialog::getOpenFileName(this, tr("Markdown Editor"), QDir::homePath()); 48 | // f = f.simplified(); 49 | // if (f.isEmpty()) return; 50 | // ui->editorLine->setText(f); 51 | // } 52 | 53 | 54 | void ExternalReadersCentralWidget::addItem() { 55 | auto item = ui->editorLine->text().simplified(); 56 | if (item.isEmpty() || mListViewModel->stringList().contains(item)) { 57 | return; 58 | } 59 | 60 | int itemsCount = mListViewModel->rowCount(); 61 | mListViewModel->insertRow(itemsCount); 62 | auto index = mListViewModel->index(itemsCount, 0); 63 | mListViewModel->setData(index, item); 64 | ui->editorLine->clear(); 65 | ui->editorLine->setFocus(); 66 | } 67 | 68 | 69 | void ExternalReadersCentralWidget::delItem() { 70 | int row = ui->listView->currentIndex().row(); 71 | if (row < 0) { 72 | return; 73 | } 74 | 75 | mListViewModel->removeRow(row); 76 | } 77 | 78 | 79 | void ExternalReadersCentralWidget::accept() { 80 | Ui::Settings::saveExternalReaders(mListViewModel->stringList()); 81 | emit accepted(); 82 | } 83 | -------------------------------------------------------------------------------- /src/ExternalReadersCentralWidget.h: -------------------------------------------------------------------------------- 1 | #ifndef EXTERNALREADERSCENTRALWIDGET__H 2 | #define EXTERNALREADERSCENTRALWIDGET__H 3 | 4 | #include 5 | 6 | class QStringListModel; 7 | class QShortcut; 8 | 9 | QT_BEGIN_NAMESPACE 10 | namespace Ui { 11 | class ExternalReadersCentralWidget; 12 | } 13 | QT_END_NAMESPACE 14 | 15 | class ExternalReadersCentralWidget : public QWidget { 16 | Q_OBJECT 17 | public: 18 | explicit ExternalReadersCentralWidget(QWidget* parent = nullptr); 19 | ~ExternalReadersCentralWidget() override; 20 | // void browse(); // not used anywhere. TODO something with it or delete 21 | 22 | public slots: 23 | void accept(); 24 | 25 | private: 26 | void addItem(); 27 | void delItem(); 28 | void itemUp(); 29 | void itemDown(); 30 | 31 | signals: 32 | void accepted(); 33 | 34 | private: 35 | Ui::ExternalReadersCentralWidget* ui; 36 | std::unique_ptr mListViewModel; 37 | std::unique_ptr mRetShortcut; 38 | }; 39 | 40 | #endif // EXTERNALREADERSCENTRALWIDGET__H 41 | -------------------------------------------------------------------------------- /src/ExternalReadersCentralWidget.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | ExternalReadersCentralWidget 4 | 5 | 6 | 7 | 0 8 | 0 9 | 572 10 | 379 11 | 12 | 13 | 14 | 15 | 300 16 | 200 17 | 18 | 19 | 20 | 21 | 800 22 | 600 23 | 24 | 25 | 26 | Markdown Readers 27 | 28 | 29 | 30 | 31 | 32 | Name of a Markdown editor/reader: 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 0 46 | 0 47 | 48 | 49 | 50 | 51 | 35 52 | 16777215 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | :/images/add.png:/images/add.png 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | List of Markdown readers: 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 0 85 | 0 86 | 87 | 88 | 89 | 90 | 35 91 | 16777215 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | :/images/delete.png:/images/delete.png 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 0 108 | 0 109 | 110 | 111 | 112 | 113 | 35 114 | 16777215 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | :/images/arrowup.png:/images/arrowup.png 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 0 131 | 0 132 | 133 | 134 | 135 | 136 | 35 137 | 16777215 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | :/images/arrowdown.png:/images/arrowdown.png 146 | 147 | 148 | 149 | 150 | 151 | 152 | Qt::Vertical 153 | 154 | 155 | 156 | 20 157 | 40 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | -------------------------------------------------------------------------------- /src/ExternalReadersDialog.cpp: -------------------------------------------------------------------------------- 1 | #include "ExternalReadersDialog.h" 2 | #include "ExternalReadersCentralWidget.h" 3 | #include "ui_ExternalReadersDialog.h" 4 | 5 | ExternalReadersDialog::ExternalReadersDialog(QWidget* parent) 6 | : QDialog(parent), ui(new Ui::ExternalReadersDialog) { 7 | ui->setupUi(this); 8 | exec(); 9 | } 10 | 11 | ExternalReadersDialog::~ExternalReadersDialog() { 12 | delete ui; 13 | } 14 | 15 | void ExternalReadersDialog::accept() { 16 | ui->externalReadersCentralWidget->accept(); 17 | QDialog::accept(); 18 | } 19 | -------------------------------------------------------------------------------- /src/ExternalReadersDialog.h: -------------------------------------------------------------------------------- 1 | #ifndef EXTERNALREADERSDIALOG__H 2 | #define EXTERNALREADERSDIALOG__H 3 | 4 | 5 | #include 6 | 7 | QT_BEGIN_NAMESPACE 8 | namespace Ui { 9 | class ExternalReadersDialog; 10 | } 11 | QT_END_NAMESPACE 12 | 13 | class ExternalReadersDialog : public QDialog { 14 | Q_OBJECT 15 | 16 | public: 17 | explicit ExternalReadersDialog(QWidget* parent = nullptr); 18 | ~ExternalReadersDialog() override; 19 | 20 | public slots: 21 | void accept() final; 22 | 23 | private: 24 | Ui::ExternalReadersDialog* ui; 25 | }; 26 | 27 | #endif // EXTERNALREADERSDIALOG__H 28 | -------------------------------------------------------------------------------- /src/ExternalReadersDialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | ExternalReadersDialog 4 | 5 | 6 | Qt::NonModal 7 | 8 | 9 | 10 | 0 11 | 0 12 | 400 13 | 300 14 | 15 | 16 | 17 | 18 | 0 19 | 0 20 | 21 | 22 | 23 | 24 | 450 25 | 350 26 | 27 | 28 | 29 | Markdown Readers Dialog 30 | 31 | 32 | true 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | ExternalReadersCentralWidget 50 | QWidget 51 |
ExternalReadersCentralWidget.h
52 | 1 53 |
54 |
55 | 56 | 57 | 58 | buttonBox 59 | rejected() 60 | ExternalReadersDialog 61 | reject() 62 | 63 | 64 | 300 65 | 412 66 | 67 | 68 | 300 69 | 217 70 | 71 | 72 | 73 | 74 | buttonBox 75 | accepted() 76 | ExternalReadersDialog 77 | accept() 78 | 79 | 80 | 300 81 | 412 82 | 83 | 84 | 300 85 | 217 86 | 87 | 88 | 89 | 90 |
91 | -------------------------------------------------------------------------------- /src/MainWindow.h: -------------------------------------------------------------------------------- 1 | #ifndef MAINWINDOW_H 2 | #define MAINWINDOW_H 3 | 4 | #include 5 | #include 6 | 7 | namespace Ui::Models { 8 | class TagTreeModel; 9 | class DocumentListModel; 10 | } // namespace Ui::Models 11 | 12 | namespace Doc { 13 | class Document; 14 | } 15 | 16 | QT_BEGIN_NAMESPACE 17 | namespace Ui { 18 | class MainWindow; 19 | } 20 | QT_END_NAMESPACE 21 | 22 | class QLineEdit; 23 | class QMenu; 24 | class QAction; 25 | class QShortcut; 26 | class QLabel; 27 | class QSystemTrayIcon; 28 | 29 | class MainWindow final : public QMainWindow { 30 | Q_OBJECT 31 | public: 32 | explicit MainWindow(QWidget* parent = nullptr); 33 | ~MainWindow() override; 34 | 35 | private: 36 | void closeEvent(QCloseEvent* event) final; 37 | void setup(); 38 | void setupModels(); 39 | void setupMenuBar(); 40 | void setupSettings(); 41 | void setupSignalsAndSlots(); 42 | void setupMenu(); 43 | void setupKeyboardShortcuts(); 44 | void setupSystemTray(); 45 | void loadDocuments(); 46 | void clearDocuments(); 47 | 48 | private slots: 49 | void onTagClicked(const QStringList& tags); 50 | void toggleDocumentContentViewVisibility(); 51 | void onChangeDataDirAction(); 52 | void openFile(QString path, QString editor = ""); 53 | void createNewFile(); 54 | void settingsDialog(); 55 | void search(const QString& text); 56 | void onNumberOfDocumentsChanged(int number); 57 | void onDocumentPermanentlyDeleted(Doc::Document* document); 58 | void onSystemTryIconActivated(int reason); 59 | void applyTheme(const QString& theme); 60 | void on_ReturnPressed(); 61 | void about(); 62 | 63 | signals: 64 | void loadedUi(); 65 | 66 | private: 67 | Ui::MainWindow* ui; 68 | QVector mDocuments; 69 | std::unique_ptr mTagTreeModel; 70 | std::unique_ptr mDocumentListModel; 71 | std::unique_ptr mSearchBar; 72 | std::unique_ptr mSearchBarEraseText; 73 | std::unique_ptr mNumberOfFilesLabel; 74 | std::unique_ptr mSystemTrayIcon; 75 | std::unique_ptr mSystemTrayMenu; 76 | }; 77 | #endif // MAINWINDOW_H 78 | -------------------------------------------------------------------------------- /src/MainWindow.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 910 10 | 600 11 | 12 | 13 | 14 | DeepTags 15 | 16 | 17 | 18 | 19 | 20 | 21 | 0 22 | 23 | 24 | QLayout::SetFixedSize 25 | 26 | 27 | 28 | 29 | 30 | 0 31 | 0 32 | 33 | 34 | 35 | 36 | 30 37 | 0 38 | 39 | 40 | 41 | 42 | 30 43 | 30 44 | 45 | 46 | 47 | 48 | 30 49 | 0 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | :/images/collapse.png:/images/collapse.png 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 0 66 | 0 67 | 68 | 69 | 70 | 71 | 30 72 | 0 73 | 74 | 75 | 76 | 77 | 30 78 | 30 79 | 80 | 81 | 82 | 83 | 30 84 | 0 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | :/images/arrowdown.png:/images/arrowdown.png 93 | 94 | 95 | 96 | 16 97 | 16 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | Qt::Vertical 106 | 107 | 108 | 109 | 20 110 | 40 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | Qt::Horizontal 121 | 122 | 123 | false 124 | 125 | 126 | 127 | false 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 0 140 | 0 141 | 910 142 | 19 143 | 144 | 145 | 146 | 147 | File 148 | 149 | 150 | 151 | Recently Opened Files 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | Edit 165 | 166 | 167 | 168 | Themes 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | Help 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | :/images/addFile.png:/images/addFile.png 197 | 198 | 199 | New File 200 | 201 | 202 | Ctrl+N 203 | 204 | 205 | 206 | 207 | Set/Change The Data Directory 208 | 209 | 210 | 211 | 212 | Open the Data Directory 213 | 214 | 215 | 216 | 217 | 218 | :/images/quit.png:/images/quit.png 219 | 220 | 221 | Quit 222 | 223 | 224 | Ctrl+Q 225 | 226 | 227 | 228 | 229 | Native Style 230 | 231 | 232 | 233 | 234 | Dark Style 235 | 236 | 237 | 238 | 239 | Light Style 240 | 241 | 242 | 243 | 244 | Markdown Readers 245 | 246 | 247 | 248 | 249 | Reload Elements 250 | 251 | 252 | 253 | 254 | true 255 | 256 | 257 | true 258 | 259 | 260 | Display Integrated Reader 261 | 262 | 263 | 264 | 265 | About 266 | 267 | 268 | 269 | 270 | Settings 271 | 272 | 273 | 274 | 275 | true 276 | 277 | 278 | false 279 | 280 | 281 | Show Document Viewer 282 | 283 | 284 | Show Document Viewer 285 | 286 | 287 | 288 | 289 | true 290 | 291 | 292 | Native 293 | 294 | 295 | 296 | 297 | true 298 | 299 | 300 | QBreeze Dark 301 | 302 | 303 | 304 | 305 | true 306 | 307 | 308 | QBreeze Light 309 | 310 | 311 | 312 | 313 | 314 | TagsTreeView 315 | QTreeView 316 |
TagsTreeView.h
317 |
318 | 319 | DocumentsListView 320 | QListView 321 |
DocumentsListView.h
322 |
323 | 324 | DocumentContentView 325 | QTextBrowser 326 |
DocumentContentView.h
327 |
328 |
329 | 330 | 331 | 332 | 333 |
334 | -------------------------------------------------------------------------------- /src/Settings.cpp: -------------------------------------------------------------------------------- 1 | #include "Settings.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | const auto mainGroup = QStringLiteral("main"); 8 | const auto pinnedTagsLabel = QStringLiteral("pinned_tags"); 9 | const auto coloredTagsLabel = QStringLiteral("colored_tags"); 10 | const auto documentViewerVisibilityLabel = QStringLiteral("document_viewer_visibility"); 11 | const auto splitterSizeLabel = QStringLiteral("splitter_size"); 12 | const auto windowSizeLabel = QStringLiteral("window_size"); 13 | const auto dataDirectoryLabel = QStringLiteral("data_directory"); 14 | const auto externalReadersGroup = QStringLiteral("markdown_editors"); 15 | const auto externalReadersLabel = QStringLiteral("list"); 16 | const auto recentFilesGroup = QStringLiteral("files"); 17 | const auto recentFilesListLabel = QStringLiteral("recently_opened_files"); 18 | const auto themeLabel = QStringLiteral("theme"); 19 | constexpr int maxSizeRecentFilesList = 15; 20 | 21 | bool contains(const QString& group, const QString& label); 22 | QVariant getValue(const QString& group, const QString& label); 23 | void saveValue(const QString& group, const QString& label, const QVariant& value); 24 | 25 | namespace Ui::Settings { 26 | 27 | 28 | void saveUiSettings(const QSize& windowSize, const QByteArray& splitterState) { 29 | saveValue(mainGroup, windowSizeLabel, QVariant(windowSize)); // size of the window 30 | saveValue(mainGroup, splitterSizeLabel, QVariant(splitterState)); 31 | } 32 | 33 | 34 | QByteArray loadSplitterState() { 35 | return getValue(mainGroup, splitterSizeLabel).toByteArray(); 36 | } 37 | 38 | 39 | void saveSplitterState(QByteArray& data) { 40 | saveValue(mainGroup, splitterSizeLabel, data); 41 | } 42 | 43 | 44 | QSize loadWindowSize() { 45 | return getValue(mainGroup, windowSizeLabel).toSize(); 46 | } 47 | 48 | 49 | QVector getPinnedTags() { 50 | return getValue(mainGroup, pinnedTagsLabel).toStringList().toVector(); 51 | } 52 | 53 | 54 | void setPinnedTags(const QVector& tags) { 55 | saveValue(mainGroup, pinnedTagsLabel, QVariant(tags.toList())); 56 | } 57 | 58 | 59 | QHash getColoredTags() { 60 | return getValue(mainGroup, coloredTagsLabel).toHash(); 61 | } 62 | 63 | 64 | void setColoredTags(const QHash& colors) { 65 | saveValue(mainGroup, coloredTagsLabel, QVariant(colors)); 66 | } 67 | 68 | 69 | void saveDocumentViewerVisibility(bool use) { 70 | saveValue(mainGroup, documentViewerVisibilityLabel, QVariant(use)); 71 | } 72 | 73 | 74 | bool loadDocumentViewerVisibility() { 75 | return getValue(mainGroup, documentViewerVisibilityLabel).toBool(); 76 | } 77 | 78 | 79 | bool hasLoadDocumentViewerVisibility() { 80 | return contains(mainGroup, documentViewerVisibilityLabel); 81 | } 82 | 83 | 84 | void saveDataDirectory(const QString& value) { 85 | saveValue(mainGroup, dataDirectoryLabel, value); 86 | } 87 | 88 | 89 | QString loadDataDirectory() { 90 | return getValue(mainGroup, dataDirectoryLabel).toString(); 91 | } 92 | 93 | 94 | void saveExternalReaders(const QStringList& value) { 95 | saveValue(externalReadersGroup, externalReadersLabel, value); 96 | } 97 | 98 | 99 | QStringList loadExternalReaders() { 100 | return getValue(externalReadersGroup, externalReadersLabel).toStringList(); 101 | } 102 | 103 | 104 | QString loadDefaultExternalReaders() { 105 | if (auto lst = loadExternalReaders(); !lst.isEmpty()) { 106 | return lst.at(0); 107 | } 108 | 109 | return ""; 110 | } 111 | 112 | 113 | void saveRecentFiles(const QStringList& value) { 114 | if (maxSizeRecentFilesList < value.size()) { 115 | saveValue(recentFilesGroup, recentFilesListLabel, 116 | QStringList(value.begin(), value.begin() + maxSizeRecentFilesList)); 117 | 118 | } else { 119 | saveValue(recentFilesGroup, recentFilesListLabel, value); 120 | } 121 | } 122 | 123 | 124 | void addRecentFile(const QString& value) { 125 | auto lst = loadRecentFiles(); 126 | if (lst.contains(value)) { 127 | lst.removeAll(value); 128 | return; 129 | } 130 | 131 | lst.push_front(value); 132 | saveRecentFiles(lst); 133 | } 134 | 135 | 136 | QStringList loadRecentFiles() { 137 | return getValue(recentFilesGroup, recentFilesListLabel).toStringList(); 138 | } 139 | 140 | 141 | void saveTheme(const QString& value) { 142 | saveValue(mainGroup, themeLabel, value); 143 | } 144 | 145 | 146 | QString loadTheme() { 147 | return getValue(mainGroup, themeLabel).toString(); 148 | } 149 | 150 | } // namespace Ui::Settings 151 | 152 | 153 | void saveValue(const QString& group, const QString& label, const QVariant& value) { 154 | QSettings s; 155 | s.beginGroup(group); 156 | s.setValue(label, value); 157 | s.endGroup(); 158 | } 159 | 160 | 161 | QVariant getValue(const QString& group, const QString& label) { 162 | QSettings s; 163 | s.beginGroup(group); 164 | QVariant val = s.value(label); 165 | s.endGroup(); 166 | return val; 167 | } 168 | 169 | 170 | bool contains(const QString& group, const QString& label) { 171 | QSettings s; 172 | s.beginGroup(group); 173 | const bool val = s.contains(label); 174 | s.endGroup(); 175 | return val; 176 | } 177 | -------------------------------------------------------------------------------- /src/Settings.h: -------------------------------------------------------------------------------- 1 | #ifndef SETTINGS__H 2 | #define SETTINGS__H 3 | 4 | #include 5 | 6 | class QVariant; 7 | class QSize; 8 | 9 | namespace Ui::Settings { 10 | 11 | QVector getPinnedTags(); 12 | QHash getColoredTags(); 13 | void setColoredTags(const QHash& colors); 14 | void setPinnedTags(const QVector& tags); 15 | void setPinnedTag(const QString& tag); 16 | 17 | void saveUiSettings(const QSize& windowSize, const QByteArray& splitterState); 18 | QSize loadWindowSize(); 19 | QByteArray loadSplitterState(); 20 | void saveSplitterState(); 21 | 22 | /** 23 | * display the document viewer or not 24 | */ 25 | void saveDocumentViewerVisibility(bool use); 26 | bool loadDocumentViewerVisibility(); 27 | bool hasLoadDocumentViewerVisibility(); 28 | 29 | /** 30 | * Save/Load the Data Directory 31 | */ 32 | void saveDataDirectory(const QString& value); 33 | QString loadDataDirectory(); 34 | 35 | /** 36 | * Save/Load external readers 37 | */ 38 | void saveExternalReaders(const QStringList& value); 39 | QStringList loadExternalReaders(); 40 | QString loadDefaultExternalReaders(); 41 | 42 | /** 43 | * Save/Load recent files 44 | */ 45 | void saveRecentFiles(const QStringList& value); 46 | QStringList loadRecentFiles(); 47 | void addRecentFile(const QString& value); 48 | 49 | /** 50 | * Save/Load theme 51 | */ 52 | void saveTheme(const QString& value); 53 | QString loadTheme(); 54 | } // namespace Ui::Settings 55 | 56 | #endif // SETTINGS__H 57 | -------------------------------------------------------------------------------- /src/SettingsDialog.cpp: -------------------------------------------------------------------------------- 1 | #include "SettingsDialog.h" 2 | #include "./ui_SettingsDialog.h" 3 | 4 | 5 | SettingsDialog::SettingsDialog(QWidget* parent) : QDialog(parent), ui(new Ui::SettingsDialog) { 6 | ui->setupUi(this); 7 | ui->lineSeparatorGroupBox->setHidden(true); // TODO: unhide this and properly implement it 8 | } 9 | 10 | 11 | SettingsDialog::~SettingsDialog() { 12 | delete ui; 13 | } 14 | 15 | 16 | DataDirectoryCentralWidget* SettingsDialog::getDataDirWidget() { 17 | return ui->dataDirWidget; 18 | } 19 | 20 | 21 | auto* SettingsDialog::externalMdReaders() { 22 | return ui->mdReaders; 23 | } 24 | 25 | 26 | void SettingsDialog::accept() { 27 | ui->mdReaders->accept(); 28 | ui->dataDirWidget->accept(); 29 | QDialog::accept(); 30 | } 31 | -------------------------------------------------------------------------------- /src/SettingsDialog.h: -------------------------------------------------------------------------------- 1 | #ifndef SETTINGSDIALOG__H 2 | #define SETTINGSDIALOG__H 3 | 4 | #include 5 | 6 | class DataDirectoryCentralWidget; 7 | 8 | QT_BEGIN_NAMESPACE 9 | namespace Ui { 10 | class SettingsDialog; 11 | } 12 | QT_END_NAMESPACE 13 | 14 | class SettingsDialog : public QDialog { 15 | Q_OBJECT 16 | public: 17 | explicit SettingsDialog(QWidget* parent = nullptr); 18 | ~SettingsDialog() override; 19 | DataDirectoryCentralWidget* getDataDirWidget(); 20 | auto* externalMdReaders(); 21 | void accept() final; 22 | 23 | private: 24 | Ui::SettingsDialog* ui; 25 | }; 26 | 27 | #endif // SETTINGSDIALOG__H 28 | -------------------------------------------------------------------------------- /src/SettingsDialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | SettingsDialog 4 | 5 | 6 | Qt::ApplicationModal 7 | 8 | 9 | 10 | 0 11 | 0 12 | 600 13 | 387 14 | 15 | 16 | 17 | 18 | 0 19 | 0 20 | 21 | 22 | 23 | Settings 24 | 25 | 26 | 27 | 28 | 29 | 0 30 | 31 | 32 | 33 | General 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | This is applied when a file is written from DeepTags (like when adding a tag) 43 | 44 | 45 | Line Separator Type 46 | 47 | 48 | 49 | 50 | 51 | true 52 | 53 | 54 | Carriage return and line feet (CRLF) 55 | 56 | 57 | true 58 | 59 | 60 | 61 | 62 | 63 | 64 | Line Feed (LF) 65 | 66 | 67 | false 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | Qt::Vertical 78 | 79 | 80 | 81 | 20 82 | 40 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | Markdown Readers 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | DataDirectoryCentralWidget 108 | QWidget 109 |
DataDirectoryCentralWidget.h
110 | 1 111 |
112 | 113 | ExternalReadersCentralWidget 114 | QWidget 115 |
ExternalReadersCentralWidget.h
116 | 1 117 |
118 |
119 | 120 | 121 | 122 | buttonBox 123 | rejected() 124 | SettingsDialog 125 | reject() 126 | 127 | 128 | 405 129 | 380 130 | 131 | 132 | 399 133 | 299 134 | 135 | 136 | 137 | 138 | buttonBox 139 | accepted() 140 | SettingsDialog 141 | accept() 142 | 143 | 144 | 405 145 | 380 146 | 147 | 148 | 399 149 | 299 150 | 151 | 152 | 153 | 154 |
155 | -------------------------------------------------------------------------------- /src/TagTreeDelegate.cpp: -------------------------------------------------------------------------------- 1 | #include "TagTreeDelegate.h" 2 | #include "TagTreeModel.h" 3 | 4 | 5 | namespace Ui::Delegates { 6 | 7 | TagTreeDelegate::TagTreeDelegate(QObject* parent) : QStyledItemDelegate(parent) {} 8 | 9 | 10 | void TagTreeDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, 11 | const QModelIndex& index) const { 12 | QStyleOptionViewItem myOption(option); 13 | myOption.features = QStyleOptionViewItem::HasDisplay; 14 | 15 | if (index.data(Ui::Models::TagTreeModel::PinnedTagRole).toBool()) { // pinned items are bold 16 | myOption.font.setBold(true); 17 | } 18 | 19 | QStyledItemDelegate::paint(painter, myOption, index); 20 | } 21 | 22 | 23 | } // namespace Ui::Delegates 24 | -------------------------------------------------------------------------------- /src/TagTreeDelegate.h: -------------------------------------------------------------------------------- 1 | #ifndef TAGTREEDELEGATE__h 2 | #define TAGTREEDELEGATE__h 3 | 4 | #include 5 | 6 | namespace Ui::Delegates { 7 | 8 | class TagTreeDelegate : public QStyledItemDelegate { 9 | Q_OBJECT 10 | public: 11 | explicit TagTreeDelegate(QObject* parent = nullptr); 12 | void paint(QPainter* painter, const QStyleOptionViewItem& option, 13 | const QModelIndex& index) const final; 14 | }; 15 | } // namespace Ui::Delegates 16 | 17 | 18 | #endif // TAGTREEDELEGATE__h 19 | -------------------------------------------------------------------------------- /src/TagTreeItem.cpp: -------------------------------------------------------------------------------- 1 | #include "TagTreeItem.h" 2 | #include 3 | #include 4 | #include "Settings.h" 5 | 6 | 7 | namespace Ui::Models { 8 | 9 | TagTreeItem::TagTreeItem(QString name, TagTreeItem* parent) 10 | : mName{ std::move(name) }, mParent{ parent } { 11 | if (mParent) { 12 | mParent->addChild(this); 13 | } 14 | } 15 | 16 | 17 | TagTreeItem::~TagTreeItem() { 18 | for (auto child : mChildren) { 19 | if (child) { 20 | delete child; 21 | } 22 | } 23 | } 24 | 25 | 26 | QString TagTreeItem::getName() const { 27 | return mName; 28 | } 29 | 30 | 31 | QString TagTreeItem::getCompleteName() const { 32 | QString result = mName; 33 | TagTreeItem* p = mParent; 34 | while (p) { 35 | result.prepend(p->getName() + "/"); 36 | p = p->getParent(); 37 | } 38 | 39 | return result.remove(0, 1); 40 | } 41 | 42 | 43 | QVector TagTreeItem::getChildren() { 44 | return mChildren; 45 | } 46 | 47 | 48 | TagTreeItem* TagTreeItem::getParent() { 49 | return mParent; 50 | } 51 | 52 | void TagTreeItem::addChild(TagTreeItem* child) { 53 | mChildren.push_back(child); 54 | } 55 | 56 | 57 | TagTreeItem* TagTreeItem::getChild(int index) { 58 | return (index < mChildren.size()) ? mChildren.at(index) : nullptr; 59 | } 60 | 61 | 62 | TagTreeItem* TagTreeItem::getChild(const QString& name) { 63 | for (TagTreeItem* item : mChildren) { 64 | if (name == item->mName) { 65 | return item; 66 | } 67 | } 68 | 69 | return nullptr; 70 | } 71 | 72 | 73 | void TagTreeItem::removeChild(const QString& name) { 74 | for (int i = 0; i < mChildren.size(); i++) { 75 | if (name == mChildren.at(i)->getName()) { 76 | delete mChildren.takeAt(i); 77 | } 78 | } 79 | } 80 | 81 | 82 | int TagTreeItem::row() const { 83 | if (mParent != nullptr) { 84 | return mParent->getChildren().indexOf(const_cast(this)); 85 | } 86 | 87 | return -1; 88 | } 89 | 90 | 91 | void TagTreeItem::setName(const QString& name) { 92 | mName = name; 93 | } 94 | 95 | 96 | void TagTreeItem::setColor(const QString& color) const { 97 | if (color.isEmpty()) { 98 | return; 99 | } 100 | auto tag = getCompleteName(); 101 | 102 | if (color == "default") { 103 | coloredTags.remove(tag); 104 | } else { 105 | coloredTags.insert(tag, QVariant::fromValue(color)); 106 | } 107 | 108 | Settings::setColoredTags(coloredTags); 109 | 110 | refreshFromSettings(); 111 | } 112 | 113 | 114 | void TagTreeItem::setPinned(bool pinned) { 115 | const QString name = getCompleteName(); 116 | bool madeChange = false; 117 | 118 | if (pinned) { 119 | if (!pinnedTags.contains(name)) { 120 | pinnedTags.push_back(name); 121 | madeChange = true; 122 | } 123 | 124 | } else { 125 | if (pinnedTags.contains(name)) { 126 | pinnedTags.removeAll(name); 127 | madeChange = true; 128 | } 129 | } 130 | 131 | if (madeChange) { 132 | Settings::setPinnedTags(pinnedTags); 133 | } 134 | } 135 | 136 | 137 | void TagTreeItem::sortChildren() { 138 | std::sort(mChildren.begin(), mChildren.end(), [](TagTreeItem* a, TagTreeItem* b) { 139 | if (a->isSpecialTag() && !b->isSpecialTag()) { 140 | return true; 141 | } 142 | 143 | if (!a->isSpecialTag() && b->isSpecialTag()) { 144 | return false; 145 | } 146 | 147 | if (a->isPinned() && !b->isPinned()) { 148 | return true; 149 | } 150 | 151 | if (!a->isPinned() && b->isPinned()) { 152 | return false; 153 | } 154 | 155 | return *a < *b; 156 | }); 157 | 158 | for (auto& child : mChildren) { 159 | child->sortChildren(); 160 | } 161 | } 162 | 163 | 164 | bool TagTreeItem::isSpecialTag() const { 165 | return specialTags.contains(getCompleteName()); 166 | } 167 | 168 | 169 | bool TagTreeItem::isPinned() const { 170 | return pinnedTags.contains(getCompleteName()); 171 | } 172 | 173 | 174 | QString TagTreeItem::getColor() const { 175 | return coloredTags[getCompleteName()].toString(); 176 | } 177 | 178 | 179 | void TagTreeItem::refreshFromSettings() { 180 | pinnedTags = Settings::getPinnedTags(); 181 | coloredTags = Settings::getColoredTags(); 182 | } 183 | 184 | 185 | /** 186 | *** Operator overloading 187 | */ 188 | 189 | bool operator<(const TagTreeItem& lhs, const TagTreeItem& rhs) { 190 | return lhs.mName < rhs.mName; 191 | } 192 | 193 | 194 | bool operator>(const TagTreeItem& lhs, const TagTreeItem& rhs) { 195 | return rhs < lhs; 196 | } 197 | 198 | 199 | bool operator<=(const TagTreeItem& lhs, const TagTreeItem& rhs) { 200 | return !(lhs > rhs); 201 | } 202 | 203 | 204 | bool operator>=(const TagTreeItem& lhs, const TagTreeItem& rhs) { 205 | return !(lhs < rhs); 206 | } 207 | 208 | } // namespace Ui::Models 209 | -------------------------------------------------------------------------------- /src/TagTreeItem.h: -------------------------------------------------------------------------------- 1 | #ifndef TAGTREEITEM__H 2 | #define TAGTREEITEM__H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | class QVariant; 9 | 10 | namespace Ui::Models { 11 | 12 | /** 13 | * this represents a single particle in the tag tree 14 | */ 15 | class TagTreeItem final { 16 | public: 17 | explicit TagTreeItem(QString name, TagTreeItem* parent = nullptr); 18 | TagTreeItem(const TagTreeItem&) = delete; 19 | TagTreeItem& operator=(const TagTreeItem&) = delete; 20 | TagTreeItem(TagTreeItem&&) = default; 21 | TagTreeItem& operator=(TagTreeItem&&) = default; 22 | ~TagTreeItem(); 23 | [[nodiscard]] QString getName() const; 24 | [[nodiscard]] QString getCompleteName() const; 25 | [[nodiscard]] QVector getChildren(); 26 | [[nodiscard]] TagTreeItem* getParent(); 27 | void addChild(TagTreeItem* child); 28 | TagTreeItem* getChild(int index); 29 | TagTreeItem* getChild(const QString& name); 30 | void removeChild(const QString& name); 31 | [[nodiscard]] int row() const; 32 | void setName(const QString& name); 33 | void setColor(const QString& color) const; 34 | void setPinned(bool pinned); 35 | void sortChildren(); 36 | [[nodiscard]] bool isSpecialTag() const; 37 | [[nodiscard]] bool isPinned() const; 38 | [[nodiscard]] QString getColor() const; 39 | 40 | friend bool operator<(const TagTreeItem& lhs, const TagTreeItem& rhs); 41 | friend bool operator>(const TagTreeItem& lhs, const TagTreeItem& rhs); 42 | friend bool operator<=(const TagTreeItem& lhs, const TagTreeItem& rhs); 43 | friend bool operator>=(const TagTreeItem& lhs, const TagTreeItem& rhs); 44 | 45 | static void refreshFromSettings(); 46 | 47 | public: 48 | inline static const QVector specialTags{ "All Notes", "Favorite", "Trash", 49 | "Untagged" }; 50 | ; 51 | inline static QVector pinnedTags{}; 52 | inline static QHash coloredTags{}; 53 | 54 | private: 55 | QString mName; 56 | TagTreeItem* mParent; 57 | QVector mChildren; 58 | }; 59 | 60 | } // namespace Ui::Models 61 | 62 | #endif // TAGTREEITEM__H 63 | -------------------------------------------------------------------------------- /src/TagTreeModel.cpp: -------------------------------------------------------------------------------- 1 | #include "TagTreeModel.h" 2 | #include 3 | #include 4 | #include 5 | #include "Benchmark.h" 6 | #include "Document.h" 7 | #include "DocumentUtils.h" 8 | #include "TagTreeItem.h" 9 | 10 | #define castItem(x) static_cast(x) 11 | 12 | namespace Ui::Models { 13 | 14 | TagTreeModel::TagTreeModel(QVector* documents, QObject* parent) 15 | : QAbstractItemModel{ parent }, mRootItem{ new TagTreeItem("") }, mDocuments{ documents } { 16 | TagTreeItem::refreshFromSettings(); 17 | setDocuments(documents); 18 | } 19 | 20 | 21 | TagTreeModel::~TagTreeModel() { 22 | delete mRootItem; 23 | } 24 | 25 | 26 | void TagTreeModel::setDocuments(QVector* documents) { 27 | mDocuments = documents; 28 | setupTags(); 29 | } 30 | 31 | 32 | void TagTreeModel::setupPermanentTags() { 33 | if (mRootItem == nullptr) { 34 | mRootItem = new TagTreeItem(""); 35 | } 36 | 37 | for (const auto& tag : TagTreeItem::specialTags) { 38 | if (mRootItem->getChild(tag) == nullptr) { 39 | new TagTreeItem(tag, mRootItem); 40 | } 41 | } 42 | } 43 | 44 | 45 | void TagTreeModel::reset() { 46 | beginResetModel(); 47 | 48 | mDocuments = nullptr; 49 | delete mRootItem; 50 | mRootItem = nullptr; 51 | 52 | endResetModel(); 53 | } 54 | 55 | 56 | void TagTreeModel::setupTags() { 57 | Benchmark b("tags loading"); 58 | setupPermanentTags(); 59 | 60 | if (mDocuments == nullptr || mDocuments->empty()) { 61 | return; 62 | } 63 | 64 | for (const auto& doc : *mDocuments) { 65 | if (doc->isDeleted()) { 66 | continue; 67 | } 68 | 69 | for (auto& tagChain : doc->getTags()) { 70 | TagTreeItem* previous = mRootItem; 71 | 72 | for (auto& tag : Doc::Utils::deconstructTag(tagChain)) { 73 | if (auto prev = previous->getChild(tag); prev != nullptr) { 74 | previous = prev; 75 | continue; 76 | } 77 | 78 | auto newItem = new TagTreeItem(tag, previous); 79 | previous = newItem; 80 | } 81 | } 82 | } 83 | 84 | sortTags(); 85 | } 86 | 87 | 88 | void TagTreeModel::addTag(const QString& tagChain) { 89 | if (tagChain.isEmpty()) { 90 | return; 91 | } 92 | 93 | emit layoutAboutToBeChanged(); 94 | 95 | TagTreeItem* previous = mRootItem; 96 | 97 | for (auto& tag : Doc::Utils::deconstructTag(tagChain)) { 98 | if (auto prev = previous->getChild(tag); prev != nullptr) { 99 | previous = prev; 100 | continue; 101 | } 102 | 103 | auto newItem = new TagTreeItem(tag, previous); 104 | previous->sortChildren(); 105 | int i = newItem->row(); 106 | beginInsertRows(index(previous), i, i); 107 | previous = newItem; 108 | endInsertRows(); 109 | } 110 | 111 | updatePersistentIndexes(); 112 | emit layoutChanged(); 113 | } 114 | 115 | 116 | TagTreeItem* getTagTreeItem(const QStringVector& tagSeq, TagTreeItem* root) { 117 | if (root == nullptr) { 118 | return nullptr; 119 | } 120 | 121 | TagTreeItem* res = root; 122 | for (const auto& tag : tagSeq) { 123 | if (auto child = res->getChild(tag)) { 124 | res = child; 125 | } else { 126 | return root; 127 | } 128 | } 129 | 130 | return res; 131 | } 132 | 133 | 134 | void TagTreeModel::deleteTag(const QString& tagChain) { 135 | if (tagChain.isEmpty() || tagExists(tagChain)) { 136 | return; 137 | } 138 | 139 | const auto tagSeq = Doc::Utils::deconstructTag(tagChain); 140 | TagTreeItem* item = getTagTreeItem(tagSeq, mRootItem); 141 | 142 | TagTreeItem* parent = item->getParent(); 143 | auto parentIndex = index(parent); 144 | 145 | if (parent == nullptr) { 146 | return; 147 | } 148 | 149 | int row = item->row(); 150 | emit layoutAboutToBeChanged(); 151 | beginRemoveRows(parentIndex, row, row); 152 | item->getParent()->removeChild(item->getName()); 153 | endRemoveRows(); 154 | 155 | emit layoutChanged(); 156 | 157 | // delete parent of item if it is empty 158 | const QString tag = parent->getCompleteName(); 159 | if (1 < tagSeq.size() && !tagExists(tag)) { 160 | deleteTag(tag); 161 | } 162 | } 163 | 164 | 165 | bool TagTreeModel::tagExists(const QString& tag) { 166 | const QStringVector tagChain = Doc::Utils::deconstructTag(tag); 167 | return std::any_of(mDocuments->cbegin(), mDocuments->cend(), 168 | [&tagChain](auto t) { return !t->isDeleted() && t->containsTag(tagChain); }); 169 | // for (const Doc::Document* const t : *mDocuments) { 170 | // if (!t->isDeleted() && t->containsTag(tagChain)) { 171 | // return true; 172 | // } 173 | // } 174 | 175 | // return false; 176 | } 177 | 178 | 179 | void TagTreeModel::sortTags() { 180 | if (mRootItem != nullptr) { 181 | mRootItem->sortChildren(); 182 | } 183 | } 184 | 185 | 186 | int TagTreeModel::columnCount([[maybe_unused]] const QModelIndex& parent) const { 187 | return 1; 188 | } 189 | 190 | 191 | int TagTreeModel::rowCount(const QModelIndex& parent) const { 192 | if (0 < parent.column()) { 193 | return 0; 194 | } 195 | 196 | auto parentItem = (parent.isValid()) ? itemAt(parent) : mRootItem; 197 | if (parentItem != nullptr) { 198 | return parentItem->getChildren().size(); 199 | } 200 | 201 | return 0; 202 | } 203 | 204 | 205 | QVariant TagTreeModel::data(const QModelIndex& index, int role) const { 206 | if (!index.isValid()) { 207 | return {}; 208 | } 209 | 210 | switch (role) { 211 | case Qt::DisplayRole: 212 | case NameRole: 213 | return itemAt(index)->getName(); 214 | case CompleteNameRole: 215 | return itemAt(index)->getCompleteName(); 216 | case SpecialTagRole: 217 | return TagTreeItem::specialTags.contains(itemAt(index)->getCompleteName()); 218 | case Qt::ForegroundRole: 219 | if (const QString color = itemAt(index)->getColor(); !color.isEmpty()) { 220 | return QColor(color); 221 | } 222 | 223 | return {}; 224 | case PinnedTagRole: 225 | return itemAt(index)->isPinned(); 226 | case Qt::DecorationRole: { 227 | if (const QString n = data(index, NameRole).toString(); n == "All Notes") { 228 | return QIcon(QStringLiteral(":images/all_notes.png")); 229 | } else if (n == "Trash") { 230 | return QIcon(QStringLiteral(":images/trash.png")); 231 | } else if (n == "Favorite") { 232 | return QIcon(QStringLiteral(":images/star.png")); 233 | } else if (n == "Untagged") { 234 | return QIcon(QStringLiteral(":images/untagged.png")); 235 | } else if (n == "Notebook") { 236 | return QIcon(QStringLiteral(":images/notebook.png")); 237 | } 238 | } 239 | } 240 | 241 | return {}; 242 | } 243 | 244 | 245 | bool TagTreeModel::setData(const QModelIndex& index, const QVariant& value, int role) { 246 | auto item = itemAt(index); 247 | bool changed = false; 248 | 249 | if (item == nullptr) { 250 | return false; 251 | } 252 | 253 | switch (role) { 254 | case Qt::DisplayRole: 255 | case NameRole: 256 | item->setName(value.toString()); 257 | changed = true; 258 | break; 259 | case Qt::ForegroundRole: 260 | item->setColor(value.toString()); 261 | changed = true; 262 | break; 263 | case PinnedTagRole: 264 | item->setPinned(value.toBool()); 265 | emit layoutAboutToBeChanged(); 266 | sortTags(); 267 | updatePersistentIndexes(); 268 | emit layoutChanged(); 269 | changed = true; 270 | break; 271 | } 272 | 273 | if (changed) { 274 | emit dataChanged(index, index, { role }); 275 | } 276 | 277 | return changed; 278 | } 279 | 280 | 281 | void TagTreeModel::updatePersistentIndexes() { 282 | QModelIndexList persistentList = persistentIndexList(); 283 | 284 | for (auto oldIndex : persistentList) { 285 | if (oldIndex.isValid()) { 286 | QModelIndex newIndex = index(castItem(oldIndex.internalPointer())); 287 | changePersistentIndex(oldIndex, newIndex); 288 | } 289 | } 290 | } 291 | 292 | 293 | QModelIndex TagTreeModel::index(TagTreeItem* item) const { 294 | if (item == nullptr) { 295 | return {}; 296 | } 297 | 298 | const int row = item->row(); 299 | return row < 0 ? QModelIndex() : createIndex(row, 0, item); 300 | } 301 | 302 | 303 | QModelIndex TagTreeModel::index(int row, int column, const QModelIndex& parent) const { 304 | if (!hasIndex(row, column, parent)) { 305 | return {}; 306 | } 307 | 308 | TagTreeItem* parentItem = parent.isValid() ? castItem(parent.internalPointer()) : mRootItem; 309 | 310 | TagTreeItem* childItem = parentItem->getChild(row); 311 | if (childItem != nullptr) { 312 | return createIndex(row, column, childItem); 313 | } 314 | 315 | return {}; 316 | } 317 | 318 | 319 | QModelIndex TagTreeModel::parent(const QModelIndex& index) const { 320 | if (!index.isValid()) { 321 | return {}; 322 | } 323 | 324 | TagTreeItem* childItem = castItem(index.internalPointer()); 325 | TagTreeItem* parentItem = childItem->getParent(); 326 | 327 | if (parentItem == mRootItem) { 328 | return {}; 329 | } 330 | 331 | return createIndex(parentItem->row(), 0, parentItem); 332 | } 333 | 334 | 335 | QHash TagTreeModel::roleNames() const { 336 | QHash names; 337 | names[NameRole] = "name"; 338 | names[CompleteNameRole] = "completeName"; 339 | names[SpecialTagRole] = "specialTag"; 340 | names[PinnedTagRole] = "pinned"; 341 | 342 | return names; 343 | } 344 | 345 | 346 | Qt::ItemFlags TagTreeModel::flags(const QModelIndex& index) const { 347 | Qt::ItemFlags defaultFlags = QAbstractItemModel::flags(index); 348 | 349 | if (index.isValid()) { 350 | return Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | defaultFlags; 351 | } 352 | 353 | return Qt::ItemIsDropEnabled | defaultFlags; 354 | } 355 | 356 | 357 | Qt::DropActions TagTreeModel::supportedDropActions() const { 358 | return Qt::CopyAction; 359 | } 360 | 361 | 362 | TagTreeItem* TagTreeModel::itemAt(const QModelIndex& index) { 363 | if (index.isValid()) { 364 | return castItem(index.internalPointer()); 365 | } 366 | 367 | return nullptr; 368 | } 369 | } // namespace Ui::Models 370 | -------------------------------------------------------------------------------- /src/TagTreeModel.h: -------------------------------------------------------------------------------- 1 | #ifndef TAGTREEMODEL__H 2 | #define TAGTREEMODEL__H 3 | 4 | #include 5 | 6 | namespace Doc { 7 | class Document; 8 | } 9 | 10 | namespace Ui::Models { 11 | 12 | class TagTreeItem; 13 | 14 | class TagTreeModel final : public QAbstractItemModel { 15 | Q_OBJECT 16 | public: 17 | enum CustomRoles { NameRole = Qt::UserRole, CompleteNameRole, SpecialTagRole, PinnedTagRole }; 18 | 19 | explicit TagTreeModel(QVector* documents = nullptr, QObject* parent = nullptr); 20 | ~TagTreeModel() override; 21 | 22 | public slots: 23 | void deleteTag(const QString& tag); 24 | void addTag(const QString& tag); 25 | void reset(); 26 | void setDocuments(QVector* documents); 27 | 28 | public: 29 | [[nodiscard]] int columnCount(const QModelIndex& parent = QModelIndex()) const final; 30 | [[nodiscard]] int rowCount(const QModelIndex& parent = QModelIndex()) const final; 31 | [[nodiscard]] QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const final; 32 | [[nodiscard]] QModelIndex index(int row, int column, 33 | const QModelIndex& parent = QModelIndex()) const final; 34 | QModelIndex index(TagTreeItem* item) const; 35 | [[nodiscard]] QModelIndex parent(const QModelIndex& index) const final; 36 | [[nodiscard]] Qt::ItemFlags flags(const QModelIndex& index) const final; 37 | [[nodiscard]] QHash roleNames() const final; 38 | bool setData(const QModelIndex& index, const QVariant& value, int role) final; 39 | [[nodiscard]] Qt::DropActions supportedDropActions() const final; 40 | 41 | private: 42 | void setupTags(); 43 | void setupPermanentTags(); 44 | void updatePersistentIndexes(); 45 | bool tagExists(const QString& tag); 46 | void sortTags(); 47 | [[nodiscard]] static TagTreeItem* itemAt(const QModelIndex& index); 48 | 49 | private: 50 | TagTreeItem* mRootItem; 51 | QVector* mDocuments; 52 | }; 53 | 54 | } // namespace Ui::Models 55 | 56 | #endif // TAGTREEMODEL__H 57 | -------------------------------------------------------------------------------- /src/TagsTreeView.cpp: -------------------------------------------------------------------------------- 1 | #include "TagsTreeView.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include "TagTreeDelegate.h" 12 | #include "TagTreeModel.h" 13 | 14 | using Ui::Models::TagTreeModel; 15 | using Ui::Delegates::TagTreeDelegate; 16 | 17 | constexpr std::array colors{ "default", "green", "yellow", "orange", 18 | "red", "magenta", "blue", "cyan" }; 19 | 20 | TagsTreeView::TagsTreeView(QWidget* parent) : QTreeView(parent) { 21 | setHeaderHidden(true); 22 | setDragDropMode(DragOnly); 23 | setSelectionMode(QAbstractItemView::ExtendedSelection); 24 | setDragEnabled(true); 25 | setAcceptDrops(true); 26 | setDropIndicatorShown(true); 27 | setExpandsOnDoubleClick(true); 28 | setContextMenuPolicy(Qt::CustomContextMenu); 29 | 30 | setItemDelegate(new TagTreeDelegate(this)); 31 | connect(this, &TagsTreeView::clicked, this, &TagsTreeView::onClicked); 32 | connect(this, &TagsTreeView::customContextMenuRequested, this, 33 | &TagsTreeView::onCustomContextMenuRequested); 34 | } 35 | 36 | 37 | void TagsTreeView::changeTagColor(const QModelIndex& index, const QString& color) { 38 | model()->setData(index, color, Qt::ForegroundRole); 39 | } 40 | 41 | 42 | void TagsTreeView::pinTag(const QModelIndex& index, bool pin) { 43 | model()->setData(index, pin, TagTreeModel::PinnedTagRole); 44 | } 45 | 46 | 47 | void TagsTreeView::onClicked([[maybe_unused]] const QModelIndex& index) { 48 | QStringList tags; 49 | for (auto ind : selectedIndexes()) { 50 | if (!ind.isValid()) { 51 | continue; 52 | } 53 | 54 | auto tag = model()->data(ind, model()->CompleteNameRole).toString(); 55 | if (!tag.isEmpty()) { 56 | tags.push_back(tag); 57 | } 58 | } 59 | 60 | if (!tags.isEmpty()) { 61 | emit tagsSelected(tags); 62 | } 63 | } 64 | 65 | 66 | TagTreeModel* TagsTreeView::model() { 67 | return static_cast(QTreeView::model()); 68 | } 69 | 70 | 71 | void TagsTreeView::onCustomContextMenuRequested(QPoint pos) { 72 | const auto index = indexAt(pos); 73 | if (index.internalPointer() == nullptr) { 74 | return; 75 | } 76 | 77 | auto isPinned = index.data(TagTreeModel::PinnedTagRole).toBool(); 78 | auto expanded = isExpanded(index); 79 | 80 | auto menu = std::make_unique(); 81 | auto colorMenu = menu->addMenu(tr("Change the color")); 82 | auto pin = menu->addAction((isPinned) ? tr("Unpin") : tr("Pin")); 83 | 84 | for (auto color : colors) { 85 | const QString iconPath = QStringLiteral(":images/color_") + color; 86 | auto act = colorMenu->addAction(QIcon(iconPath), tr(color)); 87 | connect(act, &QAction::triggered, this, [=] { changeTagColor(index, color); }); 88 | } 89 | 90 | auto expandCollapse = menu->addAction(((expanded) ? tr("Collapse") : tr("Expand"))); 91 | menu->addMenu(colorMenu); 92 | auto copyTag = menu->addAction(tr("Copy tag")); 93 | 94 | connect(pin, &QAction::triggered, this, [=] { pinTag(index, !isPinned); }); 95 | connect(expandCollapse, &QAction::triggered, this, [=] { setExpanded(index, !expanded); }); 96 | connect(copyTag, &QAction::triggered, this, [=] { 97 | const QString tag = model()->data(index, model()->CompleteNameRole).toString(); 98 | qApp->clipboard()->setText(tag); 99 | }); 100 | 101 | menu->exec(mapToGlobal(pos)); 102 | } 103 | 104 | 105 | void TagsTreeView::dragMoveEvent(QDragMoveEvent* event) { 106 | if (event->mimeData()->hasText()) { 107 | event->setDropAction(Qt::CopyAction); 108 | event->accept(); 109 | } else { 110 | event->ignore(); 111 | } 112 | } 113 | 114 | 115 | void TagsTreeView::startDrag([[maybe_unused]] Qt::DropActions supportedActions) { 116 | const QString tag = model()->data(currentIndex(), model()->CompleteNameRole).toString(); 117 | 118 | auto mime = new QMimeData; 119 | mime->setText(tag); 120 | auto drag = new QDrag(this); 121 | drag->setMimeData(mime); 122 | drag->exec(Qt::CopyAction); 123 | } 124 | -------------------------------------------------------------------------------- /src/TagsTreeView.h: -------------------------------------------------------------------------------- 1 | #ifndef TAGSTREEVIEW__H 2 | #define TAGSTREEVIEW__H 3 | 4 | #include 5 | 6 | // namespace Ui { 7 | 8 | namespace Ui::Models { 9 | class TagTreeModel; 10 | } 11 | 12 | QT_BEGIN_NAMESPACE 13 | namespace Ui { 14 | class TagsTreeView; 15 | } 16 | QT_END_NAMESPACE 17 | 18 | class TagsTreeView : public QTreeView { 19 | Q_OBJECT 20 | public: 21 | explicit TagsTreeView(QWidget* parent = nullptr); 22 | ~TagsTreeView() override = default; 23 | 24 | public slots: 25 | void changeTagColor(const QModelIndex& index, const QString& color); 26 | void pinTag(const QModelIndex& index, bool pin); 27 | 28 | public slots: 29 | void onClicked(const QModelIndex& index); 30 | 31 | private: 32 | Ui::Models::TagTreeModel* model(); 33 | void onCustomContextMenuRequested(QPoint pos); 34 | void dragMoveEvent(QDragMoveEvent* event) final; 35 | void startDrag(Qt::DropActions supportedActions) final; 36 | 37 | signals: 38 | void tagsSelected(const QStringList& tags); 39 | }; 40 | 41 | // } // namespace Ui 42 | #endif // TAGSTREEVIEW__H 43 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "MainWindow.h" 6 | 7 | #ifndef DEEPTAGS_VERSION 8 | #error "The DEEPTAGS_VERSION flag isn't defined. Please define it and proceed" 9 | #endif 10 | 11 | int error(); 12 | const auto NAME = QStringLiteral("DeepTags"); 13 | const auto DEEPTAGS_WEBSITE = QStringLiteral("https://github.com/SZinedine/DeepTags"); 14 | const auto ORG_WEBSITE = QStringLiteral("https://github.com/SZinedine"); 15 | const auto VERSION = QStringLiteral(DEEPTAGS_VERSION); 16 | 17 | 18 | int main(int argc, char* argv[]) { 19 | switch (argc) { 20 | case 1: 21 | break; 22 | case 2: { 23 | if (std::strcmp(argv[1], "-v") == 0 || std::strcmp(argv[1], "--version") == 0) { 24 | std::puts(QString("%1 %2").arg(NAME, VERSION).toStdString().c_str()); 25 | return 0; 26 | } else if (std::strcmp(argv[1], "-h") == 0 || std::strcmp(argv[1], "--help") == 0) { 27 | const QString HELP_MESSAGE = 28 | QString("DeepTags %1 (%2)\n").arg(VERSION, DEEPTAGS_WEBSITE) + 29 | "Copyright (C) 2024 Zineddine SAIBI .\n" + 30 | "License GPLv3+: GNU GPL version 3 or later \n" + 31 | "This is free software: you are free to change and redistribute it.\n"; 32 | std::puts(HELP_MESSAGE.toStdString().c_str()); 33 | 34 | return 0; 35 | } else { 36 | return error(); 37 | } 38 | } 39 | default: { 40 | return error(); 41 | } 42 | } 43 | 44 | SingleApplication app(argc, argv); 45 | QApplication::setApplicationName(NAME); 46 | QApplication::setOrganizationName(NAME); 47 | QApplication::setApplicationVersion(VERSION); 48 | QApplication::setOrganizationDomain(ORG_WEBSITE); 49 | 50 | auto locale = QLocale::system().name().section('_', 0, 0); 51 | // locale = "fr"; // for test 52 | auto translationFile = QString(":locale/DeepTags_%1.qm").arg(locale); 53 | QTranslator tr; 54 | tr.load(translationFile); 55 | QApplication::installTranslator(&tr); 56 | 57 | MainWindow win; 58 | win.setWindowTitle(NAME); 59 | win.setWindowIcon(QIcon((QStringLiteral(":images/icon128.png")))); 60 | QObject::connect(&app, &SingleApplication::instanceStarted, &win, &QMainWindow::raise); 61 | win.show(); 62 | 63 | return QApplication::exec(); 64 | } 65 | 66 | 67 | int error() { 68 | std::puts("Error. Invalid arguments"); 69 | return 1; 70 | } 71 | -------------------------------------------------------------------------------- /tag_hierarchy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SZinedine/DeepTags/483c84729c82f784dd10f7a3e74bd2ff18a27a69/tag_hierarchy.png -------------------------------------------------------------------------------- /tests/DocumentUtilsTest.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "../src/DocumentUtils.h" 4 | #include "Document.h" 5 | 6 | static QVector fileContent { 7 | "---", 8 | "title: 'some title'", 9 | "pinned: false", 10 | "favorited: true", 11 | "deleted: false", 12 | "tags: [a/b/c, d/e/f, g, h]", 13 | "---", 14 | "", 15 | "Some title", 16 | "============", 17 | "" 18 | }; 19 | 20 | 21 | class DocumentUtilsTest : public QObject { 22 | Q_OBJECT 23 | private slots: 24 | void LAB(); 25 | void arrayToStringTest(); 26 | void deconstructTagTest(); 27 | void isMDTest(); 28 | void getHeaderTest(); 29 | void getHeaderLineTest(); 30 | void getHeaderValueTest(); 31 | void hasHeaderKeyTest(); 32 | void setHeaderValueTest(); 33 | void deleteHeaderLineTest(); 34 | }; 35 | 36 | 37 | void DocumentUtilsTest::LAB() { 38 | } 39 | 40 | void DocumentUtilsTest::arrayToStringTest() { 41 | QStringVector sl{ "Earth/Africa/Algeria", "Sciences/Physics" }; 42 | QString expected = "[Earth/Africa/Algeria, Sciences/Physics]"; 43 | auto actual = Doc::Utils::arrayToString(sl); 44 | 45 | QCOMPARE(actual, expected); 46 | } 47 | 48 | 49 | void DocumentUtilsTest::deconstructTagTest() { 50 | const QString tag = "a/b/c"; 51 | QStringVector expected = { "a", "b", "c" }; 52 | QStringVector actual = Doc::Utils::deconstructTag(tag); 53 | 54 | QCOMPARE(actual, expected); 55 | } 56 | 57 | 58 | void DocumentUtilsTest::isMDTest() { 59 | QVERIFY(Doc::Utils::isMD("abc.md")); 60 | QVERIFY(Doc::Utils::isMD("abc.a.md")); 61 | QVERIFY(Doc::Utils::isMD("abc.md.md")); 62 | QVERIFY(Doc::Utils::isMD("abc.MD")); 63 | QVERIFY(Doc::Utils::isMD("abc.mD")); 64 | QVERIFY(Doc::Utils::isMD("abc.Md")); 65 | QVERIFY(Doc::Utils::isMD("abc.b.Md")); 66 | 67 | QVERIFY(Doc::Utils::isMD("abc.markdown")); 68 | QVERIFY(Doc::Utils::isMD("abc.MARKDOWN")); 69 | QVERIFY(Doc::Utils::isMD("abc.Markdown")); 70 | 71 | QVERIFY(!Doc::Utils::isMD("abc.markdown.")); 72 | QVERIFY(!Doc::Utils::isMD("abc.md.")); 73 | } 74 | 75 | 76 | void DocumentUtilsTest::getHeaderTest() { 77 | QVector result{Doc::Utils::getHeader(fileContent)}; 78 | QVector expected{ QVector(fileContent.begin() + 1, fileContent.begin() + 6)}; 79 | 80 | QCOMPARE(result.size(), expected.size()); 81 | 82 | for (int i = 0; i < result.size(); i++) { 83 | QCOMPARE(result.at(i), expected.at(i)); 84 | } 85 | } 86 | 87 | 88 | void DocumentUtilsTest::getHeaderLineTest() { 89 | QVector header{Doc::Utils::getHeader(fileContent)}; 90 | 91 | QString trueTitle = *(fileContent.begin()+1); 92 | QString truePinned = *(fileContent.begin()+2); 93 | QString trueFavorited = *(fileContent.begin()+3); 94 | QString trueDeleted = *(fileContent.begin()+4); 95 | QString trueTags = *(fileContent.begin()+5); 96 | 97 | QCOMPARE(trueTitle, Doc::Utils::getHeaderLine("title", header)); 98 | QCOMPARE(truePinned, Doc::Utils::getHeaderLine("pinned", header)); 99 | QCOMPARE(trueFavorited, Doc::Utils::getHeaderLine("favorited", header)); 100 | QCOMPARE(trueDeleted, Doc::Utils::getHeaderLine("deleted", header)); 101 | QCOMPARE(trueTags, Doc::Utils::getHeaderLine("tags", header)); 102 | QCOMPARE(QString(), Doc::Utils::getHeaderLine("EMPTY", header)); 103 | } 104 | 105 | 106 | void DocumentUtilsTest::getHeaderValueTest() { 107 | 108 | QVector header{Doc::Utils::getHeader(fileContent)}; 109 | 110 | QCOMPARE(QString("some title"), Doc::Utils::getHeaderValue("title", header)); 111 | QCOMPARE(QString("false"), Doc::Utils::getHeaderValue("pinned", header)); 112 | QCOMPARE(QString("true"), Doc::Utils::getHeaderValue("favorited", header)); 113 | QCOMPARE(QString("false"), Doc::Utils::getHeaderValue("deleted", header)); 114 | QCOMPARE(QString("[a/b/c, d/e/f, g, h]"), Doc::Utils::getHeaderValue("tags", header)); 115 | QCOMPARE(QString(""), Doc::Utils::getHeaderValue("EMPTY", header)); 116 | } 117 | 118 | 119 | void DocumentUtilsTest::hasHeaderKeyTest() { 120 | auto header = Doc::Utils::getHeader(fileContent); 121 | QVERIFY(Doc::Utils::hasHeaderKey("title", header)); 122 | QVERIFY(Doc::Utils::hasHeaderKey("pinned", header)); 123 | QVERIFY(Doc::Utils::hasHeaderKey("favorited", header)); 124 | QVERIFY(Doc::Utils::hasHeaderKey("deleted", header)); 125 | QVERIFY(Doc::Utils::hasHeaderKey("tags", header)); 126 | QVERIFY(!Doc::Utils::hasHeaderKey("abc", header)); 127 | } 128 | 129 | 130 | void DocumentUtilsTest::setHeaderValueTest() { 131 | 132 | auto header = Doc::Utils::getHeader(fileContent); 133 | QString newTitle = "custom title"; 134 | QString newTags = "[aaa]"; 135 | 136 | Doc::Utils::setHeaderValue("title", newTitle, header); 137 | Doc::Utils::setHeaderValue("tags", newTags, header); 138 | 139 | QCOMPARE(Doc::Utils::getHeaderValue("title", header), newTitle); 140 | QCOMPARE(Doc::Utils::getHeaderValue("tags", header), newTags); 141 | } 142 | 143 | 144 | void DocumentUtilsTest::deleteHeaderLineTest() { 145 | auto header = Doc::Utils::getHeader(fileContent); 146 | int size = header.size(); 147 | 148 | QVERIFY(Doc::Utils::hasHeaderKey("title", header)); 149 | QVERIFY(Doc::Utils::deleteHeaderLine("title", header)); 150 | QVERIFY(!Doc::Utils::hasHeaderKey("title", header)); 151 | QCOMPARE(header.size(), size-1); 152 | } 153 | 154 | 155 | QTEST_MAIN(DocumentUtilsTest) 156 | #include "DocumentUtilsTest.moc" 157 | --------------------------------------------------------------------------------