├── .clang-format ├── .github ├── FUNDING.yml └── workflows │ ├── build_docker.yml │ ├── build_linux.yml │ ├── cleanup_docker.yml │ └── main_ci.yml ├── .gitignore ├── .vscode └── settings.json ├── CMakeLists.txt ├── LICENSE ├── ci ├── docker │ └── Dockerfile.base └── scripts │ ├── delete_ghcr_image_version.sh │ ├── docker_build_push.sh │ ├── hooks │ └── clang-format-hook.sh │ └── report_coverage.sh ├── configure.sh ├── docs └── images │ └── demo_screenshot.png ├── include ├── CMakeLists.txt └── qspdlog │ ├── qabstract_spdlog_toolbar.hpp │ └── qspdlog.hpp ├── readme.md ├── sample ├── CMakeLists.txt └── sample.cpp ├── src ├── CMakeLists.txt ├── qabstract_spdlog_toolbar.cpp ├── qspdlog.cpp ├── qspdlog_model.cpp ├── qspdlog_model.hpp ├── qspdlog_proxy_model.cpp ├── qspdlog_proxy_model.hpp ├── qspdlog_resources.qrc ├── qspdlog_style_dialog.cpp ├── qspdlog_style_dialog.hpp ├── qspdlog_toolbar.cpp ├── qspdlog_toolbar.hpp ├── qt_logger_sink.hpp └── res │ ├── critical.png │ ├── debug.png │ ├── error.png │ ├── info.png │ ├── trace.png │ └── warn.png └── test ├── CMakeLists.txt └── test_qspdlog.cpp /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | # BasedOnStyle: LLVM 4 | AccessModifierOffset: -4 5 | AlignAfterOpenBracket: BlockIndent 6 | AlignArrayOfStructures: Right 7 | AlignConsecutiveMacros: true 8 | AlignConsecutiveAssignments: false 9 | AlignConsecutiveDeclarations: false 10 | AlignEscapedNewlines: Left 11 | AlignOperands: true 12 | AlignTrailingComments: true 13 | AllowAllArgumentsOnNextLine: true 14 | AllowAllConstructorInitializersOnNextLine: false 15 | AllowAllParametersOfDeclarationOnNextLine: true 16 | AllowShortBlocksOnASingleLine: Always 17 | AllowShortCaseLabelsOnASingleLine: true 18 | AllowShortFunctionsOnASingleLine: All 19 | AllowShortLambdasOnASingleLine: All 20 | AllowShortIfStatementsOnASingleLine: Never 21 | AllowShortLoopsOnASingleLine: false 22 | AlwaysBreakAfterReturnType: None 23 | AlwaysBreakBeforeMultilineStrings: false 24 | AlwaysBreakTemplateDeclarations: Yes 25 | BinPackArguments: false 26 | BinPackParameters: false 27 | BraceWrapping: 28 | AfterCaseLabel: false 29 | AfterClass: false 30 | AfterControlStatement: false 31 | AfterEnum: false 32 | AfterFunction: false 33 | AfterNamespace: false 34 | AfterObjCDeclaration: false 35 | AfterStruct: false 36 | AfterUnion: false 37 | AfterExternBlock: false 38 | BeforeCatch: false 39 | BeforeElse: false 40 | IndentBraces: false 41 | SplitEmptyFunction: true 42 | SplitEmptyRecord: true 43 | SplitEmptyNamespace: true 44 | BreakBeforeBinaryOperators: None 45 | BreakBeforeBraces: Linux 46 | BreakBeforeInheritanceComma: true 47 | BreakInheritanceList: BeforeComma 48 | BreakConstructorInitializers: BeforeComma 49 | BreakBeforeTernaryOperators: true 50 | BreakConstructorInitializersBeforeComma: true 51 | BreakAfterJavaFieldAnnotations: false 52 | BreakStringLiterals: true 53 | ColumnLimit: 80 54 | CommentPragmas: "^ IWYU pragma:" 55 | CompactNamespaces: false 56 | ConstructorInitializerAllOnOneLineOrOnePerLine: false 57 | ConstructorInitializerIndentWidth: 4 58 | ContinuationIndentWidth: 4 59 | Cpp11BracedListStyle: false 60 | DeriveLineEnding: false 61 | DerivePointerAlignment: false 62 | DisableFormat: false 63 | ExperimentalAutoDetectBinPacking: false 64 | FixNamespaceComments: true 65 | ForEachMacros: 66 | - for 67 | - foreach 68 | - Q_FOREACH 69 | - BOOST_FOREACH 70 | IncludeBlocks: Regroup 71 | IncludeCategories: 72 | - Regex: 'stdafx\.h' 73 | Priority: -3 74 | - Regex: 'prototypes\.h' 75 | Priority: -2 76 | - Regex: "<[[:alnum:]_./]+>" 77 | Priority: -1 78 | - Regex: ".*" 79 | Priority: 1 80 | SortPriority: 0 81 | IncludeIsMainRegex: "(Test)?$" 82 | IncludeIsMainSourceRegex: "" 83 | IndentCaseLabels: true 84 | IndentGotoLabels: false 85 | IndentPPDirectives: AfterHash 86 | IndentWidth: 4 87 | IndentWrappedFunctionNames: true 88 | LambdaBodyIndentation: OuterScope 89 | JavaScriptQuotes: Leave 90 | JavaScriptWrapImports: true 91 | KeepEmptyLinesAtTheStartOfBlocks: false 92 | MacroBlockBegin: "" 93 | MacroBlockEnd: "" 94 | MaxEmptyLinesToKeep: 1 95 | NamespaceIndentation: None 96 | ObjCBinPackProtocolList: Auto 97 | ObjCBlockIndentWidth: 2 98 | ObjCSpaceAfterProperty: false 99 | ObjCSpaceBeforeProtocolList: true 100 | PenaltyBreakAssignment: 2 101 | PenaltyBreakBeforeFirstCallParameter: 19 102 | PenaltyBreakComment: 300 103 | PenaltyBreakFirstLessLess: 120 104 | PenaltyBreakString: 1000 105 | PenaltyBreakTemplateDeclaration: 10 106 | PenaltyExcessCharacter: 1000000 107 | PenaltyReturnTypeOnItsOwnLine: 10000 108 | PointerAlignment: Left 109 | ReflowComments: true 110 | RemoveBracesLLVM: true 111 | SortIncludes: true 112 | SortUsingDeclarations: true 113 | ShortNamespaceLines: 0 114 | SpaceAfterCStyleCast: false 115 | SpaceAfterLogicalNot: false 116 | SpaceAfterTemplateKeyword: true 117 | SpaceBeforeAssignmentOperators: true 118 | SpaceBeforeCpp11BracedList: true 119 | SpaceBeforeCtorInitializerColon: true 120 | SpaceBeforeInheritanceColon: true 121 | SpaceBeforeParens: ControlStatements 122 | SpaceBeforeRangeBasedForLoopColon: true 123 | SpaceBeforeSquareBrackets: false 124 | SpaceInEmptyBlock: true 125 | SpaceInEmptyParentheses: false 126 | SpacesBeforeTrailingComments: 1 127 | SpacesInAngles: false 128 | SpacesInCStyleCastParentheses: false 129 | SpacesInConditionalStatement: false 130 | SpacesInContainerLiterals: true 131 | SpacesInParentheses: false 132 | SpacesInSquareBrackets: true 133 | SpaceBeforeCaseColon: false 134 | Standard: Latest 135 | StatementMacros: 136 | - Q_UNUSED 137 | - QT_REQUIRE_VERSION 138 | TabWidth: 4 139 | UseCRLF: false 140 | UseTab: Never 141 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: 4 | patreon: arsdever 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: ['https://www.buymeacoffee.com/arsdever'] 14 | -------------------------------------------------------------------------------- /.github/workflows/build_docker.yml: -------------------------------------------------------------------------------- 1 | name: Build docker image 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | tag: 7 | description: "Tag to build and push with" 8 | required: true 9 | default: "latest" 10 | workflow_call: 11 | inputs: 12 | tag: 13 | type: string 14 | required: true 15 | default: "latest" 16 | outputs: 17 | image: 18 | description: "The docker image that was generated" 19 | value: ${{ jobs.build_and_push_docker_image.outputs.image }} 20 | tag: 21 | description: "The tag that the docker image was pushed with" 22 | value: ${{ jobs.build_and_push_docker_image.outputs.tag }} 23 | 24 | jobs: 25 | build_and_push_docker_image: 26 | runs-on: ubuntu-latest 27 | permissions: 28 | packages: write 29 | contents: read 30 | outputs: 31 | image: ${{ steps.setup_output.outputs.docker_image }} 32 | tag: ${{ steps.setup_output.outputs.docker_tag }} 33 | steps: 34 | - name: Check out the repository 35 | uses: actions/checkout@v3 36 | 37 | - name: Log in to the Container registry 38 | uses: docker/login-action@v2 39 | with: 40 | registry: ghcr.io 41 | username: ${{ github.actor }} 42 | password: ${{ secrets.GITHUB_TOKEN }} 43 | 44 | - name: Check if build related changes were made 45 | id: check 46 | uses: dorny/paths-filter@v2 47 | with: 48 | filters: | 49 | build: 50 | - 'ci/scripts/docker.sh' 51 | - 'ci/docker/**' 52 | - '.github/workflows/build_docker.yml' 53 | 54 | - name: Setup environment variables related to the docker image 55 | id: setup_output 56 | run: | 57 | echo "docker_image=ghcr.io/${{ github.repository }}" >> $GITHUB_OUTPUT 58 | echo "docker_tag=${{ steps.check.outputs.build != 'true' && 'latest' || inputs.tag }}" >> $GITHUB_OUTPUT 59 | 60 | - name: Print info about the job being triggered 61 | run: | 62 | echo "Building and deploying the docker image:" 63 | echo " docker_image=${{ steps.setup_output.outputs.docker_image }}" 64 | echo " docker_tag=${{ steps.setup_output.outputs.docker_tag }}" 65 | 66 | - name: Print the reason for skipping the job 67 | if: steps.check.outputs.build != 'true' 68 | run: | 69 | echo "Skipping the job because no build related changes were made." 70 | 71 | - name: Build and push Docker images 72 | if: steps.check.outputs.build == 'true' 73 | run: > 74 | ./ci/scripts/docker_build_push.sh 75 | ./ci/docker/Dockerfile.base 76 | ${{ steps.setup_output.outputs.docker_image }} 77 | ${{ steps.setup_output.outputs.docker_tag }} 78 | -------------------------------------------------------------------------------- /.github/workflows/build_linux.yml: -------------------------------------------------------------------------------- 1 | name: Build and test 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | image: 7 | type: string 8 | required: true 9 | tag: 10 | type: string 11 | required: true 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | container: 17 | image: ${{ inputs.image }}:${{ inputs.tag }} 18 | steps: 19 | - name: Check out the repository 20 | uses: actions/checkout@v3 21 | 22 | - name: Configure 23 | run: | 24 | set -e 25 | cmake -B build -S . -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON -DBUILD_COVERAGE=ON 26 | 27 | - name: Build 28 | run: | 29 | set -e 30 | cmake --build build -j 31 | 32 | - name: Upload artifacts 33 | uses: actions/upload-artifact@v3 34 | with: 35 | name: linux-build 36 | path: build 37 | 38 | test: 39 | runs-on: ubuntu-latest 40 | needs: build 41 | container: 42 | image: ghcr.io/${{ github.repository }}:latest 43 | steps: 44 | - name: Download artifacts 45 | uses: actions/download-artifact@v3 46 | with: 47 | name: linux-build 48 | path: build 49 | 50 | - name: Test 51 | run: | 52 | set -e 53 | # https://github.com/actions/upload-artifact/issues/38 54 | # TODO: remove when the issue with preserving the permissions is fixed 55 | chmod +x build/test/qspdlog_test_ui 56 | cd build 57 | export DISPLAY=:1 58 | Xvfb :1 -screen 0 1024x768x24 & 59 | ctest --output-on-failure -j 60 | 61 | - name: Upload artifacts 62 | uses: actions/upload-artifact@v3 63 | with: 64 | name: coverage-profile 65 | path: build/test/default.profraw 66 | 67 | report_coverage: 68 | runs-on: ubuntu-latest 69 | needs: test 70 | container: 71 | image: ghcr.io/${{ github.repository }}:latest 72 | steps: 73 | - name: Check out the repository 74 | uses: actions/checkout@v3 75 | 76 | - name: Download build artifacts 77 | uses: actions/download-artifact@v3 78 | with: 79 | name: linux-build 80 | path: build 81 | 82 | - name: Download coverage profile artifacts 83 | uses: actions/download-artifact@v3 84 | with: 85 | name: coverage-profile 86 | path: build/test 87 | 88 | 89 | - name: Report coverage 90 | run: | 91 | set -e 92 | llvm-profdata-15 merge -sparse build/test/default.profraw -o coverage.profdata 93 | llvm-cov-15 show build/test/qspdlog_test_ui --instr-profile=coverage.profdata --ignore-filename-regex=build/ > coverage.txt 94 | 95 | - name: Upload coverage repot to codecov 96 | uses: codecov/codecov-action@v3 97 | with: 98 | token: ${{ secrets.CODECOV_TOKEN }} 99 | files: ./coverage.txt 100 | flags: uitests 101 | name: ${{ inputs.tag }} 102 | fail_ci_if_error: true 103 | -------------------------------------------------------------------------------- /.github/workflows/cleanup_docker.yml: -------------------------------------------------------------------------------- 1 | name: Delete Docker images 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - closed 7 | 8 | jobs: 9 | cleanup_docker: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | packages: write 13 | contents: read 14 | steps: 15 | - name: Check out the repository 16 | uses: actions/checkout@v3 17 | 18 | - name: Log in to the Container registry 19 | uses: docker/login-action@v2 20 | with: 21 | registry: ghcr.io 22 | username: ${{ github.actor }} 23 | password: ${{ secrets.GITHUB_TOKEN }} 24 | 25 | - name: Delete Docker images 26 | run: > 27 | ./ci/scripts/delete_ghcr_image_version.sh 28 | ${{ secrets.GITHUB_TOKEN }} 29 | pr-${{ github.event.number }} 30 | -------------------------------------------------------------------------------- /.github/workflows/main_ci.yml: -------------------------------------------------------------------------------- 1 | name: Trigger the CI workflow 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | trigger_docker_build: 13 | uses: "./.github/workflows/build_docker.yml" 14 | with: 15 | # if this is a pull request, use the pull request number as the tag, otherwise set it to "latest" 16 | tag: ${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.number) || 'latest' }} 17 | 18 | trigger_builds: 19 | needs: trigger_docker_build 20 | uses: "./.github/workflows/build_linux.yml" 21 | with: 22 | image: ${{ needs.trigger_docker_build.outputs.image }} 23 | tag: ${{ needs.trigger_docker_build.outputs.tag }} 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | cmake-build* 2 | .idea 3 | build/ 4 | coverage_report/ 5 | default.profraw 6 | coverage.profdata 7 | 8 | CMakeLists.txt.user 9 | CMakeCache.txt 10 | CMakeFiles 11 | CMakeScripts 12 | Testing 13 | Makefile 14 | cmake_install.cmake 15 | install_manifest.txt 16 | compile_commands.json 17 | CTestTestfile.cmake 18 | _deps 19 | 20 | # Prerequisites 21 | *.d 22 | 23 | # Compiled Object files 24 | *.slo 25 | *.lo 26 | *.o 27 | *.obj 28 | 29 | # Precompiled Headers 30 | *.gch 31 | *.pch 32 | 33 | # Compiled Dynamic libraries 34 | *.so 35 | *.dylib 36 | *.dll 37 | 38 | # Fortran module files 39 | *.mod 40 | *.smod 41 | 42 | # Compiled Static libraries 43 | *.lai 44 | *.la 45 | *.a 46 | *.lib 47 | 48 | # Executables 49 | *.exe 50 | *.out 51 | *.app 52 | 53 | .vscode/* 54 | !.vscode/settings.json 55 | !.vscode/tasks.json 56 | !.vscode/launch.json 57 | !.vscode/extensions.json 58 | !.vscode/*.code-snippets 59 | 60 | # Local History for Visual Studio Code 61 | .history/ 62 | 63 | # Built Visual Studio Code Extensions 64 | *.vsix 65 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmake.configureArgs": [ 3 | "-DCMAKE_TOOLCHAIN_FILE=${env:VCPKG_DIR}/scripts/buildsystems/vcpkg.cmake", 4 | "-DBUILD_TESTING=TRUE", 5 | "-DBUILD_COVERAGE=TRUE" 6 | ], 7 | "files.associations": { 8 | "memory": "cpp", 9 | "__split_buffer": "cpp", 10 | "deque": "cpp", 11 | "vector": "cpp", 12 | "*.tcc": "cpp", 13 | "memory_resource": "cpp", 14 | "string": "cpp", 15 | "__bit_reference": "cpp", 16 | "__bits": "cpp", 17 | "__config": "cpp", 18 | "__debug": "cpp", 19 | "__errc": "cpp", 20 | "__hash_table": "cpp", 21 | "__mutex_base": "cpp", 22 | "__node_handle": "cpp", 23 | "__nullptr": "cpp", 24 | "__string": "cpp", 25 | "__threading_support": "cpp", 26 | "__tuple": "cpp", 27 | "array": "cpp", 28 | "atomic": "cpp", 29 | "bit": "cpp", 30 | "cctype": "cpp", 31 | "chrono": "cpp", 32 | "cmath": "cpp", 33 | "compare": "cpp", 34 | "concepts": "cpp", 35 | "condition_variable": "cpp", 36 | "cstddef": "cpp", 37 | "cstdint": "cpp", 38 | "cstdio": "cpp", 39 | "cstdlib": "cpp", 40 | "cstring": "cpp", 41 | "ctime": "cpp", 42 | "cwchar": "cpp", 43 | "cwctype": "cpp", 44 | "exception": "cpp", 45 | "format": "cpp", 46 | "initializer_list": "cpp", 47 | "iosfwd": "cpp", 48 | "istream": "cpp", 49 | "limits": "cpp", 50 | "mutex": "cpp", 51 | "new": "cpp", 52 | "ostream": "cpp", 53 | "ratio": "cpp", 54 | "sstream": "cpp", 55 | "stdexcept": "cpp", 56 | "string_view": "cpp", 57 | "system_error": "cpp", 58 | "thread": "cpp", 59 | "tuple": "cpp", 60 | "type_traits": "cpp", 61 | "typeinfo": "cpp", 62 | "unordered_map": "cpp", 63 | "algorithm": "cpp", 64 | "functional": "cpp", 65 | "iterator": "cpp", 66 | "random": "cpp", 67 | "utility": "cpp", 68 | "stop_token": "cpp", 69 | "__verbose_abort": "cpp", 70 | "locale": "cpp", 71 | "variant": "cpp", 72 | "__locale": "cpp", 73 | "bitset": "cpp", 74 | "charconv": "cpp", 75 | "clocale": "cpp", 76 | "complex": "cpp", 77 | "cstdarg": "cpp", 78 | "ios": "cpp", 79 | "numbers": "cpp", 80 | "numeric": "cpp", 81 | "optional": "cpp", 82 | "queue": "cpp", 83 | "semaphore": "cpp", 84 | "streambuf": "cpp", 85 | "*.rh": "cpp", 86 | "codecvt": "cpp", 87 | "list": "cpp", 88 | "map": "cpp", 89 | "future": "cpp", 90 | "iomanip": "cpp", 91 | "cinttypes": "cpp" 92 | }, 93 | "C_Cpp.default.configurationProvider": "ms-vscode.cmake-tools", 94 | "sonarlint.pathToCompileCommands": "${workspaceFolder}/build/compile_commands.json" 95 | } 96 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | 3 | project(qspdlog) 4 | 5 | set(CMAKE_AUTOMOC ON) 6 | set(CMAKE_AUTORCC ON) 7 | set(CMAKE_AUTOUIC ON) 8 | 9 | set(CMAKE_CXX_STANDARD 20) 10 | 11 | option(QSPDLOG_BUILD_TESTING "Option to enable/disable building the tests" 12 | ${BUILD_TESTING}) 13 | option(QSPDLOG_REPORT_COVERAGE "Option to enable/disable coverage report" 14 | ${BUILD_COVERAGE}) 15 | 16 | # set QT version to 6 if not set already 17 | if("${QT_VERSION}" STREQUAL "") 18 | message( 19 | "Parent scope didn't tell the QT version to use. Defaulting it to Qt6") 20 | set(QT_VERSION 6) 21 | else() 22 | message("Qt version set by the parent scope is Qt${QT_VERSION}") 23 | endif() 24 | 25 | find_package(Qt${QT_VERSION} REQUIRED COMPONENTS Core Gui Widgets) 26 | find_package(spdlog REQUIRED) 27 | 28 | add_subdirectory(include) 29 | 30 | link_libraries(Qt::Widgets spdlog::spdlog qspdlog::interface) 31 | 32 | if(QSPDLOG_REPORT_COVERAGE) 33 | message("Building with coverage report") 34 | add_compile_options(-fprofile-instr-generate -fcoverage-mapping --coverage) 35 | add_link_options(-fprofile-instr-generate -fcoverage-mapping --coverage) 36 | endif() 37 | 38 | add_subdirectory(src) 39 | add_subdirectory(sample) 40 | 41 | if(BUILD_TESTING) 42 | enable_testing(true) 43 | add_subdirectory(test) 44 | endif() 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Arsen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ci/docker/Dockerfile.base: -------------------------------------------------------------------------------- 1 | # Setup variables 2 | ARG CLANG_VERSION=15 3 | 4 | FROM ubuntu:latest 5 | 6 | ARG CLANG_VERSION=15 7 | 8 | ENV CXX="clang++-$CLANG_VERSION" 9 | ENV CC="clang-$CLANG_VERSION" 10 | 11 | RUN apt update 12 | 13 | RUN apt install -y \ 14 | build-essential \ 15 | cmake \ 16 | git \ 17 | wget \ 18 | software-properties-common \ 19 | xvfb \ 20 | && rm -rf /var/lib/apt/lists/* 21 | 22 | RUN apt update 23 | 24 | RUN apt install -y \ 25 | qt6-base-dev \ 26 | libgl1-mesa-dev 27 | 28 | RUN wget https://apt.llvm.org/llvm.sh && \ 29 | chmod +x llvm.sh && \ 30 | ./llvm.sh $CLANG_VERSION 31 | 32 | RUN git clone https://github.com/gabime/spdlog.git && \ 33 | cd spdlog && \ 34 | git checkout v1.11.0 && \ 35 | cmake -B build -DCMAKE_BUILD_TYPE=Release -DSPDLOG_BUILD_EXAMPLES=OFF -DSPDLOG_BUILD_TESTS=OFF -DSPDLOG_BUILD_BENCH=OFF && \ 36 | cmake --build build -j --target install 37 | -------------------------------------------------------------------------------- /ci/scripts/delete_ghcr_image_version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # Check input arguments 6 | if [ $# -ne 2 ]; then 7 | echo "Usage: $0 " 8 | exit 1 9 | fi 10 | 11 | # Get token 12 | token=$1 13 | image_version=$2 14 | 15 | # Get image versions 16 | id=$(curl \ 17 | -X GET \ 18 | -H "Accept: application/vnd.github.v3+json" \ 19 | -H "Authorization: bearer $token" \ 20 | https://api.github.com/users/arsdever/packages/container/qspdlog/versions | \ 21 | jq '.[] | select(.name == "'"$image_version"'")' | \ 22 | jq .id) 23 | 24 | curl \ 25 | -X DELETE \ 26 | -H "Accept: application/vnd.github.v3+json" \ 27 | -H "Authorization: bearer $token" \ 28 | https://api.github.com/users/arsdever/packages/container/qspdlog/versions/$id 29 | -------------------------------------------------------------------------------- /ci/scripts/docker_build_push.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Enable exit on error 4 | set -e 5 | 6 | # Check if docker is installed 7 | which docker 8 | if [ $? -ne 0 ]; then 9 | echo "Docker is not installed" 10 | exit 1 11 | fi 12 | 13 | # Get the dockerfile name, image name and the tag 14 | dockerfile="" 15 | imagename="" 16 | tag="" 17 | 18 | # Check the argument count 19 | if [ $# -lt 2 ]; then 20 | echo "Usage: $0 " 21 | exit 1 22 | else 23 | dockerfile=$1 24 | imagename=$2 25 | tag=$3 26 | fi 27 | 28 | docker build -f $dockerfile -t $imagename:$tag . 29 | docker push $imagename:$tag 30 | -------------------------------------------------------------------------------- /ci/scripts/hooks/clang-format-hook.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | clang_format_executable=clang-format 4 | if [ -z "$(which $clang_format_executable)" ]; then 5 | echo "$clang_format_executable not found" 6 | exit 1 7 | fi 8 | 9 | STYLE=$(git config --get hooks.clangformat.style) 10 | if [ -n "${STYLE}" ] ; then 11 | STYLEARG="-style=${STYLE}" 12 | else 13 | STYLEARG="" 14 | fi 15 | 16 | format_file() { 17 | file="${1}" 18 | if [ -f "$file" ]; then 19 | $clang_format_executable -i "${STYLEARG}" "${1}" 20 | git add "${1}" 21 | fi 22 | } 23 | 24 | case "${1}" in 25 | --about ) 26 | echo "Runs clang-format on source files" 27 | ;; 28 | * ) 29 | for file in $(git diff-index --cached --name-only HEAD | grep -iE '\.(cpp|cc|h|hpp)$' ) ; do 30 | format_file "${file}" 31 | done 32 | ;; 33 | esac 34 | -------------------------------------------------------------------------------- /ci/scripts/report_coverage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # https://stackoverflow.com/a/3466183/10185183 4 | # detect the machine type 5 | EXE_SUFFIX="" 6 | unameOut="$(uname -s)" 7 | case "${unameOut}" in 8 | Linux*);; 9 | Darwin*);; 10 | CYGWIN*|MINGW*) EXE_SUFFIX=".exe";; 11 | *);; 12 | esac 13 | 14 | set -e 15 | 16 | cmake -S . -B build/coverage -G Ninja -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTING=ON -DQSPDLOG_REPORT_COVERAGE=ON $@ 17 | cmake --build build/coverage -j 18 | ctest --test-dir build/coverage --output-on-failure -j 19 | llvm-profdata merge -sparse build/coverage/test/default.profraw -o coverage.profdata 20 | llvm-cov show build/coverage/test/qspdlog_test_ui$EXE_SUFFIX --instr-profile=coverage.profdata --ignore-filename-regex=build/ --ignore-filename-regex=_autogen --format=html -o coverage_report 21 | rm build/coverage/test/qspdlog_filter_history 22 | rm -fr coverage.profdata 23 | -------------------------------------------------------------------------------- /configure.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # setup git hooks 4 | cp ci/scripts/hooks/clang-format-hook.sh .git/hooks/pre-commit 5 | -------------------------------------------------------------------------------- /docs/images/demo_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsdever/qspdlog/7db53413c4b3d16a52b1c22f281a8c39e2148851/docs/images/demo_screenshot.png -------------------------------------------------------------------------------- /include/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library( 2 | qspdlog_interface INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/qspdlog/qspdlog.hpp 3 | ${CMAKE_CURRENT_SOURCE_DIR}/qspdlog/qabstract_spdlog_toolbar.hpp) 4 | add_library(qspdlog::interface ALIAS qspdlog_interface) 5 | 6 | target_include_directories(qspdlog_interface 7 | INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) 8 | -------------------------------------------------------------------------------- /include/qspdlog/qabstract_spdlog_toolbar.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | class QLineEdit; 4 | class QAction; 5 | class QComboBox; 6 | class QSpdLog; 7 | 8 | class QAbstractSpdLogToolBar 9 | { 10 | public: 11 | /** 12 | * @brief Constructor 13 | */ 14 | explicit QAbstractSpdLogToolBar() = default; 15 | 16 | /** 17 | * @brief Destructor 18 | */ 19 | virtual ~QAbstractSpdLogToolBar(); 20 | 21 | public: 22 | /** 23 | * @brief Set the parent. 24 | * 25 | * @param parent the parent 26 | */ 27 | void setParent(QSpdLog* parent); 28 | 29 | /** 30 | * @brief Get the filter text editing widget. 31 | * 32 | * The widget is used to filter the messages in the widget. 33 | * 34 | * @return QLineEdit* the filter text editing widget 35 | */ 36 | virtual QLineEdit* filter() = 0; 37 | 38 | /** 39 | * @brief Get the case sensitive action. 40 | * 41 | * The action is used to toggle the case sensitivity of the filtering. 42 | * 43 | * @return QAction* the case sensitive action 44 | */ 45 | virtual QAction* caseSensitive() = 0; 46 | 47 | /** 48 | * @brief Get the regular expression action. 49 | * 50 | * The action is used to toggle the regular expression mode of the 51 | * filtering. 52 | * 53 | * @return QAction* the regular expression action 54 | */ 55 | virtual QAction* regex() = 0; 56 | 57 | /** 58 | * @brief Get the clear history action. 59 | * 60 | * The action is used to clear the history of the filter. 61 | * 62 | * @return QAction* the clear history action 63 | */ 64 | virtual QAction* clearHistory() = 0; 65 | 66 | /** 67 | * @brief Get the style action. 68 | * 69 | * The action is used to toggle the style of the widget. 70 | * 71 | * @return QAction* the style action 72 | */ 73 | virtual QAction* style() = 0; 74 | 75 | /** 76 | * @brief Get the auto scroll policy combo box. 77 | * 78 | * The combo box is used to select the auto scroll policy. 79 | * 80 | * @return QComboBox* the auto scroll policy combo box 81 | */ 82 | virtual QComboBox* autoScrollPolicy() = 0; 83 | 84 | private: 85 | QSpdLog* _parent; 86 | }; 87 | 88 | extern QAbstractSpdLogToolBar* createToolBar(); 89 | -------------------------------------------------------------------------------- /include/qspdlog/qspdlog.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | namespace spdlog 7 | { 8 | class logger; 9 | namespace sinks 10 | { 11 | class sink; 12 | } // namespace sinks 13 | } // namespace spdlog 14 | 15 | class QAbstractSpdLogToolBar; 16 | class QMenu; 17 | class QSpdLogModel; 18 | class QSpdLogProxyModel; 19 | class QTreeView; 20 | 21 | enum class AutoScrollPolicy { 22 | AutoScrollPolicyDisabled = 23 | 0, // Never scroll to the bottom, leave the scrollbar where it was. 24 | AutoScrollPolicyEnabled = 25 | 1, // Always scroll to the bottom when new rows are inserted. 26 | AutoScrollPolicyEnabledIfBottom = 27 | 2, // Scroll to the bottom only if the scrollbar was at the bottom 28 | // before inserting the new ones. 29 | }; 30 | 31 | class QSpdLog : public QWidget 32 | { 33 | public: 34 | /** 35 | * @brief Constructor 36 | * 37 | * @param parent The parent widget. 38 | */ 39 | explicit QSpdLog(QWidget* parent = nullptr); 40 | 41 | /** 42 | * @brief Destructor 43 | */ 44 | ~QSpdLog(); 45 | 46 | public: 47 | /** 48 | * @brief Clear the contents of the model. 49 | * 50 | * The method will clear up all the cached messages. There's no way after 51 | * this to restore them. 52 | */ 53 | void clear(); 54 | 55 | /** 56 | * @brief Register a toolbar. 57 | * 58 | * The toolbar will be set up for the current instance. Being set up means 59 | * - all the actions from the toolbar will affect current instance 60 | * - the changes to one toolbar will be reflected in the other ones as well 61 | * 62 | * @param toolbar the toolbar 63 | */ 64 | void registerToolbar(QAbstractSpdLogToolBar* toolbar); 65 | 66 | /** 67 | * @brief Remove a toolbar. 68 | * 69 | * The toolbar will be removed from the current instance. The toolbar will 70 | * not be deleted. 71 | * 72 | * @param toolbar the toolbar 73 | */ 74 | void removeToolbar(QAbstractSpdLogToolBar* toolbar); 75 | 76 | /** 77 | * @brief Get the sink. 78 | * 79 | * The sink should be used by the user to add it into any logger whose 80 | * output the user want's to see in the widget. 81 | * 82 | * @return std::shared_ptr the sink of the widget 83 | */ 84 | std::shared_ptr sink(); 85 | 86 | /** 87 | * @brief Get the number of items in the widget. 88 | * 89 | * @return std::size_t the number of items in the widget 90 | */ 91 | std::size_t itemsCount() const; 92 | 93 | /** 94 | * @brief Set the maximum number of items in the widget. 95 | * 96 | * @param std::optional the maximum number of items in the 97 | * widget 98 | */ 99 | void setMaxEntries(std::optional maxEntries); 100 | 101 | /** 102 | * @brief Get the maximum number of items in the widget. 103 | * 104 | * @return std::optional the maximum number of items in the 105 | * widget 106 | */ 107 | std::optional getMaxEntries() const; 108 | 109 | /** 110 | * @brief Set the foreground QBrush for the messages of the corresponding 111 | * logger. 112 | * 113 | * @param std::string_view the name of the logger of which to set the 114 | * foreground brush 115 | * @param std::optional the brush object or std::nullopt 116 | */ 117 | void setLoggerForeground( 118 | std::string_view loggerName, std::optional brush 119 | ); 120 | 121 | /** 122 | * @brief Get the foreground QBrush for the messages of the corresponding 123 | * logger. 124 | * 125 | * @param std::string_view the name of the logger of which to get the 126 | * foreground brush from 127 | * @return std::optional the QBrush object or std::nullopt 128 | */ 129 | std::optional getLoggerForeground(std::string_view loggerName 130 | ) const; 131 | 132 | /** 133 | * @brief Set the background QBrush for the messages of the corresponding 134 | * logger. 135 | * 136 | * @param std::string_view the name of the logger of which to set the 137 | * background brush 138 | * @param std::optional the brush object or std::nullopt 139 | */ 140 | void setLoggerBackground(std::string_view, std::optional brush); 141 | 142 | /** 143 | * @brief Get the background QBrush for the messages of the corresponding 144 | * logger. 145 | * 146 | * @param std::string_view the name of the logger of which to get the 147 | * background brush from 148 | * @return std::optional the QBrush object or std::nullopt 149 | */ 150 | std::optional getLoggerBackground(std::string_view loggerName 151 | ) const; 152 | 153 | /** 154 | * @brief Set the text QFont for the messages of the corresponding 155 | * logger. 156 | * 157 | * @param std::string_view the name of the logger of which to set the 158 | * font 159 | * @param std::optional the QFont object or std::nullopt 160 | */ 161 | void setLoggerFont(std::string_view loggerName, std::optional font); 162 | 163 | /** 164 | * @brief Get the text QFont for the messages of the corresponding 165 | * logger. 166 | * 167 | * @param std::string_view the name of the logger of which to get the 168 | * font from 169 | * @return std::optional the QFont object or std::nullopt 170 | */ 171 | std::optional getLoggerFont(std::string_view loggerName) const; 172 | 173 | /** 174 | * @brief Set the policy of the auto-scrolling feature. 175 | * 176 | * This function will set the policy for auto-scrolling and will update all 177 | * the registered toolbars. 178 | * 179 | * @param policy the auto-scrolling policy 180 | */ 181 | void setAutoScrollPolicy(AutoScrollPolicy policy); 182 | 183 | private slots: 184 | void filterData( 185 | const QString& text, bool isRegularExpression, bool isCaseSensitive 186 | ); 187 | void updateAutoScrollPolicy(int index); 188 | 189 | private: 190 | QSpdLogModel* _sourceModel; 191 | QSpdLogProxyModel* _proxyModel; 192 | QTreeView* _view; 193 | bool _scrollIsAtBottom; 194 | QMetaObject::Connection _scrollConnection; 195 | std::shared_ptr _sink; 196 | std::list _toolbars; 197 | }; 198 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # QSpdLog 2 | 3 | ![QSpdLog](docs/images/demo_screenshot.png) 4 | 5 | [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/arsdever/qspdlog/build_linux.yml?label=linux&logo=github)](https://github.com/arsdever/qspdlog/actions/workflows/main_ci.yml?query=branch%3Amain+) 6 | [![Coverage](https://img.shields.io/codecov/c/gh/arsdever/qspdlog?flag=uitests&logo=codecov&token=7d1a74f3-709e-4b2b-9b6f-c2ed5c36d7a4)](https://app.codecov.io/gh/arsdever/qspdlog/commit/4cb624e6fe8d0abcf810ba3b2ea9db69755c9ffd/tree) 7 | 8 | This repository contains the source code of a library, which provides a Qt widget for displaying log messages coming from the [spdlog](https://github.com/gabime/spdlog) library. 9 | 10 | ## Features 11 | 12 | * Display log messages in a tree view 13 | * Display an icon for each log level 14 | * Separate message structure from message content 15 | * Search in messages 16 | * via regular expressions 17 | * use match case option 18 | * reuse search history 19 | * Auto scrolling feature with various options 20 | * disabled 21 | * scroll to the bottom when a new message is added 22 | * scroll to the bottom when a new message is added unless the user scrolled up 23 | * **many more to come** 24 | * **[request or suggest new ones](https://github.com/arsdever/qspdlog/issues/new/choose)** 25 | 26 | ## Usage 27 | 28 | In its initial implementation only CMake tool is considered. To use the library in your project, you have: 29 | 30 | 1. Add the library as a submodule (or any kind of folder in your machine) 31 | 32 | ```bash 33 | git submodule add https://github.com/arsdever/qspdlog.git 34 | ``` 35 | 36 | 2. Add the library to your CMake project 37 | 38 | ```cmake 39 | add_subdirectory(qspdlog) 40 | ``` 41 | 42 | 3. Add the library to your target 43 | 44 | ```cmake 45 | target_link_libraries(${PROJECT_NAME} qspdlog::lib) 46 | ``` 47 | 48 | 4. Include the interface header into the source file where you want to instantiate the widget 49 | 50 | ```cpp 51 | #include 52 | ``` 53 | 54 | 5. Instantiate the widget, register loggers and show the widget 55 | 56 | ```cpp 57 | QSpdLogWidget* widget = new QSpdLogWidget(); 58 | 59 | auto sink = widget->sink(); 60 | // register the sink to whatever logger you want 61 | 62 | widget->show(); 63 | ``` 64 | 65 | A complete example can be found in the [sample](sample) folder. 66 | 67 | > Note: In the sample it's considered that you already added the library as a submodule. 68 | 69 | ## License 70 | 71 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 72 | 73 | ## Acknowledgments 74 | 75 | * [spdlog](https://github.com/gabime/spdlog) for the logging library 76 | * [Qt](https://www.qt.io/) for the GUI framework 77 | 78 | ## Projects using QSpdLog 79 | 80 | * [gamify](https://github.com/arsdever/gamify) 81 | -------------------------------------------------------------------------------- /sample/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_executable(qspdlog_sample sample.cpp) 2 | 3 | target_link_libraries(qspdlog_sample Qt::Core Qt::Gui Qt::Widgets qspdlog::lib) 4 | -------------------------------------------------------------------------------- /sample/sample.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "qspdlog/qabstract_spdlog_toolbar.hpp" 11 | #include "qspdlog/qspdlog.hpp" 12 | #include "spdlog/spdlog.h" 13 | 14 | std::shared_ptr createLogger(std::string name) 15 | { 16 | auto logger = std::make_shared(name); 17 | logger->set_level(spdlog::level::trace); 18 | return logger; 19 | } 20 | 21 | void configureColorScheme() 22 | { 23 | #ifdef Q_OS_WIN 24 | QSettings settings( 25 | "HKEY_CURRENT_" 26 | "USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personaliz" 27 | "e", 28 | QSettings::NativeFormat 29 | ); 30 | if (settings.value("AppsUseLightTheme") == 0) { 31 | qApp->setStyle(QStyleFactory::create("Fusion")); 32 | QPalette darkPalette; 33 | QColor darkColor = QColor(45, 45, 45); 34 | QColor disabledColor = QColor(127, 127, 127); 35 | darkPalette.setColor(QPalette::Window, darkColor); 36 | darkPalette.setColor(QPalette::WindowText, Qt::white); 37 | darkPalette.setColor(QPalette::Base, QColor(18, 18, 18)); 38 | darkPalette.setColor(QPalette::AlternateBase, darkColor); 39 | darkPalette.setColor(QPalette::ToolTipBase, Qt::white); 40 | darkPalette.setColor(QPalette::ToolTipText, Qt::white); 41 | darkPalette.setColor(QPalette::Text, Qt::white); 42 | darkPalette.setColor(QPalette::Disabled, QPalette::Text, disabledColor); 43 | darkPalette.setColor(QPalette::Button, darkColor); 44 | darkPalette.setColor(QPalette::ButtonText, Qt::white); 45 | darkPalette.setColor( 46 | QPalette::Disabled, QPalette::ButtonText, disabledColor 47 | ); 48 | darkPalette.setColor(QPalette::BrightText, Qt::red); 49 | darkPalette.setColor(QPalette::Link, QColor(42, 130, 218)); 50 | 51 | darkPalette.setColor(QPalette::Highlight, QColor(42, 130, 218)); 52 | darkPalette.setColor(QPalette::HighlightedText, Qt::black); 53 | darkPalette.setColor( 54 | QPalette::Disabled, QPalette::HighlightedText, disabledColor 55 | ); 56 | 57 | qApp->setPalette(darkPalette); 58 | 59 | qApp->setStyleSheet( 60 | "QToolTip { color: #ffffff; background-color: #2a82da; " 61 | "border: 1px solid white; }" 62 | ); 63 | } 64 | #endif 65 | } 66 | 67 | void configureToolbar( 68 | QToolBar& toolbar, QSpdLog& logView, std::shared_ptr logger 69 | ) 70 | { 71 | QAction* clearAction = toolbar.addAction("Clear"); 72 | QAction* generateAction = toolbar.addAction("Generate"); 73 | QAction* generateMultipleAction = toolbar.addAction("GenerateMultiple"); 74 | 75 | generateAction->connect( 76 | generateAction, 77 | &QAction::triggered, 78 | [ logger ](bool) { 79 | // generate 10 messages with random levels 80 | for (int i = 0; i < 10; ++i) 81 | logger->log( 82 | static_cast( 83 | rand() % spdlog::level::off 84 | ), 85 | "Message {}", 86 | i 87 | ); 88 | }); 89 | 90 | generateMultipleAction->connect( 91 | generateMultipleAction, 92 | &QAction::triggered, 93 | [ &logView, logger ](bool) { 94 | // create 10 threads and generate 10 messages with random levels 95 | std::vector threads; 96 | for (int i = 0; i < 10; ++i) { 97 | threads.emplace_back([ &logView, i, logger ]() { 98 | auto threadLocalLogger = 99 | createLogger(fmt::format("thread {}", i)); 100 | threadLocalLogger->sinks().push_back(logView.sink()); 101 | logger->info("Thread {} started", i); 102 | for (int i = 0; i < 10; ++i) { 103 | threadLocalLogger->log( 104 | static_cast( 105 | rand() % spdlog::level::off 106 | ), 107 | "Message {}", 108 | i 109 | ); 110 | } 111 | logger->info("Thread {} finished", i); 112 | }); 113 | } 114 | 115 | for (auto& thread : threads) 116 | thread.join(); 117 | }); 118 | 119 | clearAction->connect(clearAction, &QAction::triggered, [ &logView ](bool) { 120 | logView.clear(); 121 | }); 122 | } 123 | 124 | int main(int argc, char** argv) 125 | { 126 | Q_INIT_RESOURCE(qspdlog_resources); 127 | 128 | QApplication app(argc, argv); 129 | 130 | configureColorScheme(); 131 | 132 | auto logger = createLogger("main"); 133 | 134 | QToolBar toolbar("Manipulation toolbar"); 135 | toolbar.show(); 136 | 137 | QSpdLog log; 138 | log.show(); 139 | log.move(toolbar.pos() + QPoint(0, toolbar.height() + 50)); 140 | 141 | QAbstractSpdLogToolBar* logToolbar = createToolBar(); 142 | log.registerToolbar(logToolbar); 143 | dynamic_cast(logToolbar)->show(); 144 | 145 | logger->sinks().push_back(log.sink()); 146 | 147 | configureToolbar(toolbar, log, logger); 148 | 149 | int result = app.exec(); 150 | spdlog::shutdown(); 151 | 152 | return result; 153 | } 154 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(SOURCES 2 | qspdlog.cpp qabstract_spdlog_toolbar.cpp qspdlog_model.cpp 3 | qspdlog_proxy_model.cpp qspdlog_toolbar.cpp qspdlog_style_dialog.cpp) 4 | set(HEADERS qspdlog_model.hpp qt_logger_sink.hpp qspdlog_proxy_model.hpp 5 | qspdlog_style_dialog.hpp) 6 | set(RESOURCES qspdlog_resources.qrc) 7 | 8 | add_library(qspdlog_lib STATIC ${HEADERS} ${SOURCES} ${RESOURCES}) 9 | add_library(qspdlog::lib ALIAS qspdlog_lib) 10 | 11 | target_link_libraries(qspdlog_lib PUBLIC qspdlog::interface) 12 | -------------------------------------------------------------------------------- /src/qabstract_spdlog_toolbar.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | QAbstractSpdLogToolBar::~QAbstractSpdLogToolBar() 5 | { 6 | if (_parent) { 7 | _parent->removeToolbar(this); 8 | _parent = nullptr; 9 | } 10 | } 11 | 12 | void QAbstractSpdLogToolBar::setParent(QSpdLog* parent) { _parent = parent; } 13 | -------------------------------------------------------------------------------- /src/qspdlog.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "qspdlog/qspdlog.hpp" 12 | 13 | #include "qspdlog/qabstract_spdlog_toolbar.hpp" 14 | #include "qspdlog_model.hpp" 15 | #include "qspdlog_proxy_model.hpp" 16 | #include "qspdlog_style_dialog.hpp" 17 | #include "qt_logger_sink.hpp" 18 | 19 | QSpdLog::QSpdLog(QWidget* parent) 20 | : QWidget(parent) 21 | , _sourceModel(new QSpdLogModel) 22 | , _proxyModel(new QSpdLogProxyModel) 23 | , _view(new QTreeView) 24 | { 25 | Q_INIT_RESOURCE(qspdlog_resources); 26 | _view->setModel(_proxyModel); 27 | _view->setObjectName("qspdlogTreeView"); 28 | 29 | QHeaderView* header = _view->header(); 30 | header->setContextMenuPolicy(Qt::CustomContextMenu); 31 | connect( 32 | header, 33 | &QHeaderView::customContextMenuRequested, 34 | this, 35 | [ this, header ](const QPoint& pos) { 36 | QMenu contextMenu; 37 | contextMenu.setObjectName("qspdlogHeaderContextMenu"); 38 | for (int i = 0; i < _sourceModel->columnCount(); ++i) { 39 | QString columnHeader = 40 | _sourceModel->headerData(i, Qt::Horizontal).toString(); 41 | QAction* action = contextMenu.addAction(columnHeader); 42 | action->setCheckable(true); 43 | action->setChecked(!header->isSectionHidden(i)); 44 | action->setData(i); 45 | 46 | connect( 47 | action, 48 | &QAction::toggled, 49 | this, 50 | [ this, header ](bool checked) { 51 | QAction* action = qobject_cast(sender()); 52 | if (action) 53 | header->setSectionHidden(action->data().toInt(), !checked); 54 | }); 55 | } 56 | 57 | contextMenu.exec(header->mapToGlobal(pos)); 58 | }); 59 | 60 | _proxyModel->setSourceModel(_sourceModel); 61 | 62 | connect( 63 | _sourceModel, 64 | &QAbstractItemModel::rowsAboutToBeInserted, 65 | this, 66 | [ this ](const QModelIndex& parent, int first, int last) { 67 | auto bar = _view->verticalScrollBar(); 68 | _scrollIsAtBottom = bar ? (bar->value() == bar->maximum()) : false; 69 | }); 70 | 71 | _view->setRootIsDecorated(false); 72 | 73 | _sink = std::make_shared(_sourceModel); 74 | 75 | setLayout(new QHBoxLayout); 76 | layout()->setContentsMargins(0, 0, 0, 0); 77 | layout()->addWidget(_view); 78 | } 79 | 80 | QSpdLog::~QSpdLog() 81 | { 82 | std::static_pointer_cast(_sink)->invalidate(); 83 | } 84 | 85 | void QSpdLog::clear() { _sourceModel->clear(); } 86 | 87 | void QSpdLog::registerToolbar(QAbstractSpdLogToolBar* toolbarInterface) 88 | { 89 | toolbarInterface->setParent(this); 90 | _toolbars.push_back(toolbarInterface); 91 | 92 | QLineEdit* filter = toolbarInterface->filter(); 93 | QAction* regex = toolbarInterface->regex(); 94 | QAction* caseSensitive = toolbarInterface->caseSensitive(); 95 | QAction* style = toolbarInterface->style(); 96 | QComboBox* autoScrollPolicyCombo = toolbarInterface->autoScrollPolicy(); 97 | 98 | auto updateFilter = [ this, filter, regex, caseSensitive ]() { 99 | filterData( 100 | filter->text(), regex->isChecked(), caseSensitive->isChecked() 101 | ); 102 | }; 103 | 104 | connect(filter, &QLineEdit::textChanged, this, updateFilter); 105 | connect(regex, &QAction::toggled, this, updateFilter); 106 | connect(caseSensitive, &QAction::toggled, this, updateFilter); 107 | connect(style, &QAction::triggered, this, [ this ]() { 108 | QSpdLogStyleDialog dialog; 109 | dialog.setModel(_sourceModel); 110 | dialog.setObjectName("qspdlogStyleDialog"); 111 | if (!dialog.exec()) 112 | return; 113 | 114 | QSpdLogStyleDialog::Style value = dialog.result(); 115 | 116 | _sourceModel->setLoggerBackground( 117 | value.loggerName, value.backgroundColor 118 | ); 119 | 120 | _sourceModel->setLoggerForeground(value.loggerName, value.textColor); 121 | 122 | QFont f; 123 | f.setBold(value.fontBold); 124 | _sourceModel->setLoggerFont(value.loggerName, f); 125 | }); 126 | connect( 127 | autoScrollPolicyCombo, 128 | QOverload::of(&QComboBox::currentIndexChanged), 129 | this, 130 | &QSpdLog::updateAutoScrollPolicy 131 | ); 132 | } 133 | 134 | void QSpdLog::removeToolbar(QAbstractSpdLogToolBar* toolbarInterface) 135 | { 136 | _toolbars.erase( 137 | std::remove(_toolbars.begin(), _toolbars.end(), toolbarInterface), 138 | _toolbars.end() 139 | ); 140 | } 141 | 142 | void QSpdLog::filterData( 143 | const QString& text, bool isRegularExpression, bool isCaseSensitive 144 | ) 145 | { 146 | _proxyModel->setFilterCaseSensitivity( 147 | isCaseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive 148 | ); 149 | 150 | if (isRegularExpression) { 151 | QRegularExpression regex(text); 152 | 153 | if (!regex.isValid()) 154 | return; 155 | 156 | _proxyModel->setFilterRegularExpression(text); 157 | } else { 158 | _proxyModel->setFilterFixedString(text); 159 | } 160 | } 161 | 162 | void QSpdLog::setAutoScrollPolicy(AutoScrollPolicy policy) 163 | { 164 | QObject::disconnect(_scrollConnection); 165 | 166 | switch (policy) { 167 | case AutoScrollPolicy::AutoScrollPolicyEnabled: { 168 | _scrollConnection = connect( 169 | _sourceModel, 170 | &QSpdLogModel::rowsInserted, 171 | _view, 172 | &QTreeView::scrollToBottom 173 | ); 174 | break; 175 | } 176 | 177 | case AutoScrollPolicy::AutoScrollPolicyEnabledIfBottom: { 178 | _scrollConnection = connect( 179 | _sourceModel, 180 | &QSpdLogModel::rowsInserted, 181 | this, 182 | [ this ]() { 183 | // We can't check if the scrollbar is at the bottom here because 184 | // the new rows are already inserted and the position of the 185 | // scrollbar may not be at the bottom of the widget anymore. 186 | // That's why the scroll position is checked before actually 187 | // adding the rows (AKA in the rowsAboutToBeInserted signal). 188 | if (_scrollIsAtBottom) 189 | _view->scrollToBottom(); 190 | }); 191 | break; 192 | } 193 | 194 | default: { 195 | // The connection is already disconnected. No need for handling the 196 | // AutoScrollPolicyDisabled case. 197 | break; 198 | } 199 | } 200 | 201 | for (auto& toolbar : _toolbars) { 202 | QComboBox* policyComboBox = toolbar->autoScrollPolicy(); 203 | if (!policyComboBox) 204 | continue; 205 | 206 | auto blocked = policyComboBox->blockSignals(true); 207 | policyComboBox->setCurrentIndex(static_cast(policy)); 208 | policyComboBox->blockSignals(blocked); 209 | } 210 | } 211 | 212 | void QSpdLog::updateAutoScrollPolicy(int index) 213 | { 214 | AutoScrollPolicy policy = static_cast(index); 215 | setAutoScrollPolicy(policy); 216 | } 217 | 218 | spdlog::sink_ptr QSpdLog::sink() { return _sink; } 219 | 220 | std::size_t QSpdLog::itemsCount() const 221 | { 222 | return static_cast(_proxyModel->rowCount()); 223 | } 224 | 225 | void QSpdLog::setMaxEntries(std::optional maxEntries) 226 | { 227 | _sourceModel->setMaxEntries(maxEntries); 228 | } 229 | 230 | std::optional QSpdLog::getMaxEntries() const 231 | { 232 | return _sourceModel->getMaxEntries(); 233 | } 234 | 235 | void QSpdLog::setLoggerForeground( 236 | std::string_view loggerName, std::optional brush 237 | ) 238 | { 239 | _sourceModel->setLoggerForeground(loggerName, brush); 240 | } 241 | 242 | std::optional QSpdLog::getLoggerForeground(std::string_view loggerName 243 | ) const 244 | { 245 | return _sourceModel->getLoggerForeground(loggerName); 246 | } 247 | 248 | void QSpdLog::setLoggerBackground( 249 | std::string_view loggerName, std::optional brush 250 | ) 251 | { 252 | _sourceModel->setLoggerBackground(loggerName, brush); 253 | } 254 | 255 | std::optional QSpdLog::getLoggerBackground(std::string_view loggerName 256 | ) const 257 | { 258 | return _sourceModel->getLoggerBackground(loggerName); 259 | } 260 | 261 | void QSpdLog::setLoggerFont( 262 | std::string_view loggerName, std::optional font 263 | ) 264 | { 265 | _sourceModel->setLoggerFont(loggerName, font); 266 | } 267 | 268 | std::optional QSpdLog::getLoggerFont(std::string_view loggerName) const 269 | { 270 | return _sourceModel->getLoggerFont(loggerName); 271 | } -------------------------------------------------------------------------------- /src/qspdlog_model.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include "qspdlog_model.hpp" 8 | 9 | namespace 10 | { 11 | 12 | static constexpr std::array icon_names = { 13 | ":/res/trace.png", ":/res/debug.png", ":/res/info.png", 14 | ":/res/warn.png", ":/res/error.png", ":/res/critical.png" 15 | }; 16 | 17 | static constexpr std::array level_names = { 18 | "Trace", "Debug", "Info", "Warning", "Error", "Critical", "Off" 19 | }; 20 | 21 | enum class Column { Level = 0, Logger, Time, Message, Last }; 22 | 23 | static constexpr std::array column_names = { 24 | "Level", "Logger", "Time", "Message" 25 | }; 26 | 27 | } // namespace 28 | 29 | QSpdLogModel::QSpdLogModel(QObject* parent) 30 | : QAbstractListModel(parent) 31 | { 32 | } 33 | 34 | void QSpdLogModel::addEntry(entry_t entry) 35 | { 36 | if (_maxEntries > 0 && _items.size() == _maxEntries) { 37 | beginRemoveRows(QModelIndex(), 0, 0); 38 | _items.pop_front(); 39 | endRemoveRows(); 40 | } 41 | 42 | beginInsertRows(QModelIndex(), rowCount(), rowCount()); 43 | 44 | _items.push_back(std::move(entry)); 45 | 46 | endInsertRows(); 47 | } 48 | 49 | void QSpdLogModel::setMaxEntries(std::optional maxEntries) 50 | { 51 | _maxEntries = maxEntries; 52 | // Incase the new maximum is below the current amount of items. 53 | if (_maxEntries > 0 && _items.size() > _maxEntries) { 54 | std::size_t offset = _items.size() - _maxEntries.value(); 55 | beginRemoveRows(QModelIndex(), 0, offset - 1); 56 | _items.erase(_items.begin(), _items.begin() + offset); 57 | endRemoveRows(); 58 | } 59 | } 60 | 61 | std::optional QSpdLogModel::getMaxEntries() const 62 | { 63 | return _maxEntries; 64 | } 65 | 66 | void QSpdLogModel::clear() 67 | { 68 | beginResetModel(); 69 | _items.clear(); 70 | endResetModel(); 71 | } 72 | 73 | int QSpdLogModel::rowCount(const QModelIndex& parent) const 74 | { 75 | return _items.size(); 76 | } 77 | 78 | int QSpdLogModel::columnCount(const QModelIndex& parent) const 79 | { 80 | return static_cast(Column::Last); 81 | } 82 | 83 | QVariant QSpdLogModel::data(const QModelIndex& index, int role) const 84 | { 85 | if (!index.isValid() || index.row() >= _items.size()) 86 | return QVariant(); 87 | 88 | switch (role) { 89 | case Qt::DisplayRole: { 90 | auto const& item = _items[ index.row() ]; 91 | switch (static_cast(index.column())) { 92 | case Column::Level: { 93 | return QString(level_names[ item.level ]); 94 | } 95 | 96 | case Column::Logger: { 97 | return QString::fromStdString(item.loggerName); 98 | } 99 | 100 | case Column::Time: { 101 | return QDateTime::fromMSecsSinceEpoch( 102 | std::chrono::duration_cast( 103 | item.time 104 | ) 105 | .count() 106 | ); 107 | } 108 | 109 | case Column::Message: { 110 | return QString::fromStdString(item.message); 111 | } 112 | 113 | default: { 114 | break; 115 | } 116 | } 117 | 118 | break; 119 | } 120 | 121 | case Qt::DecorationRole: { 122 | if (index.column() == 0) { 123 | const auto& item = _items[ index.row() ]; 124 | if (item.level >= 0 && item.level < icon_names.size()) { 125 | return QIcon( 126 | QString(icon_names[ _items[ index.row() ].level ]) 127 | ); 128 | } 129 | } 130 | 131 | break; 132 | } 133 | 134 | case Qt::BackgroundRole: { 135 | std::string loggerName = _items[ index.row() ].loggerName; 136 | if (_backgroundMappings.contains(loggerName)) 137 | return _backgroundMappings.at(loggerName); 138 | 139 | break; 140 | } 141 | 142 | case Qt::ForegroundRole: { 143 | std::string loggerName = _items[ index.row() ].loggerName; 144 | if (_foregroundMappings.contains(loggerName)) 145 | return _foregroundMappings.at(loggerName); 146 | 147 | break; 148 | } 149 | 150 | case Qt::FontRole: { 151 | std::string loggerName = _items[ index.row() ].loggerName; 152 | if (_fontMappings.contains(loggerName)) 153 | return _fontMappings.at(loggerName); 154 | 155 | break; 156 | } 157 | 158 | default: { 159 | break; 160 | } 161 | } 162 | 163 | return QVariant(); 164 | } 165 | 166 | QVariant QSpdLogModel::headerData( 167 | int section, Qt::Orientation orientation, int role 168 | ) const 169 | { 170 | if (role == Qt::DisplayRole && orientation == Qt::Horizontal) 171 | return QString(column_names[ section ]); 172 | 173 | return QVariant(); 174 | } 175 | 176 | void QSpdLogModel::setLoggerForeground( 177 | std::string_view loggerName, std::optional color 178 | ) 179 | { 180 | int lastRow = this->rowCount() - 1; 181 | if (lastRow < 0) 182 | lastRow = 0; 183 | int lastColumn = this->columnCount() - 1; 184 | if (lastColumn < 0) 185 | lastColumn = 0; 186 | if (color.has_value()) { 187 | _foregroundMappings[ std::string(loggerName) ] = color.value(); 188 | emit dataChanged( 189 | this->index(0), 190 | this->index(lastRow, lastColumn), 191 | { Qt::ForegroundRole } 192 | ); 193 | } else if (_foregroundMappings.contains(std::string(loggerName))) { 194 | _foregroundMappings.erase(std::string(loggerName)); 195 | emit dataChanged( 196 | this->index(0), 197 | this->index(lastRow, lastColumn), 198 | { Qt::ForegroundRole } 199 | ); 200 | } 201 | } 202 | 203 | std::optional QSpdLogModel::getLoggerForeground( 204 | std::string_view loggerName 205 | ) const 206 | { 207 | if (_foregroundMappings.contains(std::string(loggerName))) 208 | return _foregroundMappings.at(std::string(loggerName)); 209 | 210 | return std::nullopt; 211 | } 212 | 213 | void QSpdLogModel::setLoggerBackground( 214 | std::string_view loggerName, std::optional brush 215 | ) 216 | { 217 | int lastRow = this->rowCount() - 1; 218 | if (lastRow < 0) 219 | lastRow = 0; 220 | int lastColumn = this->columnCount() - 1; 221 | if (lastColumn < 0) 222 | lastColumn = 0; 223 | if (brush.has_value()) { 224 | _backgroundMappings[ std::string(loggerName) ] = brush.value(); 225 | emit dataChanged( 226 | this->index(0), 227 | this->index(lastRow, lastColumn), 228 | { Qt::BackgroundRole } 229 | ); 230 | } else if (_backgroundMappings.contains(std::string(loggerName))) { 231 | _backgroundMappings.erase(std::string(loggerName)); 232 | emit dataChanged( 233 | this->index(0), 234 | this->index(lastRow, lastColumn), 235 | { Qt::BackgroundRole } 236 | ); 237 | } 238 | } 239 | 240 | std::optional QSpdLogModel::getLoggerBackground( 241 | std::string_view loggerName 242 | ) const 243 | { 244 | if (_backgroundMappings.contains(std::string(loggerName))) 245 | return _backgroundMappings.at(std::string(loggerName)); 246 | 247 | return std::nullopt; 248 | } 249 | 250 | void QSpdLogModel::setLoggerFont( 251 | std::string_view loggerName, std::optional font 252 | ) 253 | { 254 | int lastRow = this->rowCount() - 1; 255 | if (lastRow < 0) 256 | lastRow = 0; 257 | int lastColumn = this->columnCount() - 1; 258 | if (lastColumn < 0) 259 | lastColumn = 0; 260 | if (font.has_value()) { 261 | _fontMappings[ std::string(loggerName) ] = font.value(); 262 | emit dataChanged( 263 | this->index(0), this->index(lastRow, lastColumn), { Qt::FontRole } 264 | ); 265 | } else if (_fontMappings.contains(std::string(loggerName))) { 266 | _fontMappings.erase(std::string(loggerName)); 267 | emit dataChanged( 268 | this->index(0), this->index(lastRow, lastColumn), { Qt::FontRole } 269 | ); 270 | } 271 | } 272 | 273 | std::optional QSpdLogModel::getLoggerFont(std::string_view loggerName 274 | ) const 275 | { 276 | if (_fontMappings.contains(std::string(loggerName))) 277 | return _fontMappings.at(std::string(loggerName)); 278 | 279 | return std::nullopt; 280 | } -------------------------------------------------------------------------------- /src/qspdlog_model.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | class QSpdLogModel : public QAbstractListModel 9 | { 10 | public: 11 | Q_OBJECT 12 | public: 13 | struct entry_t { 14 | std::chrono::duration time; 15 | int level; 16 | std::string message; 17 | std::string loggerName; 18 | }; 19 | 20 | public: 21 | QSpdLogModel(QObject* parent = nullptr); 22 | ~QSpdLogModel() override = default; 23 | 24 | void addEntry(entry_t entry); 25 | void clear(); 26 | 27 | void setMaxEntries(std::optional maxEntries); 28 | std::optional getMaxEntries() const; 29 | 30 | void setLoggerForeground(std::string_view loggerName, std::optional color); 31 | std::optional getLoggerForeground(std::string_view loggerName) const; 32 | 33 | void setLoggerBackground(std::string_view loggerName, std::optional brush); 34 | std::optional getLoggerBackground(std::string_view loggerName) const; 35 | 36 | void setLoggerFont(std::string_view loggerName, std::optional font); 37 | std::optional getLoggerFont(std::string_view loggerName) const; 38 | 39 | #pragma region QAbstractListModel 40 | int rowCount(const QModelIndex& parent = QModelIndex()) const override; 41 | int columnCount(const QModelIndex& parent = QModelIndex()) const override; 42 | QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) 43 | const override; 44 | QVariant headerData( 45 | int section, Qt::Orientation orientation, int role = Qt::DisplayRole 46 | ) const override; 47 | #pragma endregion 48 | 49 | private: 50 | std::deque _items; 51 | std::optional _maxEntries; 52 | std::map _backgroundMappings; 53 | std::map _foregroundMappings; 54 | std::map _fontMappings; 55 | }; 56 | -------------------------------------------------------------------------------- /src/qspdlog_proxy_model.cpp: -------------------------------------------------------------------------------- 1 | #include "qspdlog_model.hpp" 2 | #include "qspdlog_proxy_model.hpp" 3 | 4 | QSpdLogProxyModel::QSpdLogProxyModel(QObject* parent) 5 | : QSortFilterProxyModel(parent) 6 | { 7 | setFilterKeyColumn(-1); 8 | } 9 | -------------------------------------------------------------------------------- /src/qspdlog_proxy_model.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | class QSpdLogProxyModel : public QSortFilterProxyModel 6 | { 7 | Q_OBJECT 8 | 9 | public: 10 | QSpdLogProxyModel(QObject* parent = nullptr); 11 | }; 12 | -------------------------------------------------------------------------------- /src/qspdlog_resources.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | res/critical.png 4 | res/error.png 5 | res/warn.png 6 | res/info.png 7 | res/debug.png 8 | res/trace.png 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/qspdlog_style_dialog.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "qspdlog_style_dialog.hpp" 7 | 8 | #include "qspdlog_model.hpp" 9 | 10 | QSpdLogStyleDialog::QSpdLogStyleDialog(QWidget* parent) 11 | : QDialog(parent) 12 | { 13 | QVBoxLayout* layout = new QVBoxLayout(this); 14 | QLineEdit* loggerNameEdit = new QLineEdit(); 15 | loggerNameEdit->setPlaceholderText("Logger name"); 16 | loggerNameEdit->setObjectName("loggerNameEdit"); 17 | QLineEdit* backgroundColorEdit = new QLineEdit(); 18 | backgroundColorEdit->setPlaceholderText("Background color"); 19 | backgroundColorEdit->setObjectName("backgroundColorEdit"); 20 | QLineEdit* textColorEdit = new QLineEdit(); 21 | textColorEdit->setPlaceholderText("Text color"); 22 | textColorEdit->setObjectName("textColorEdit"); 23 | QCheckBox* checkBoxBold = new QCheckBox("Bold"); 24 | checkBoxBold->setObjectName("checkBoxBold"); 25 | 26 | layout->addWidget(loggerNameEdit); 27 | layout->addWidget(backgroundColorEdit); 28 | layout->addWidget(textColorEdit); 29 | layout->addWidget(checkBoxBold); 30 | 31 | QDialogButtonBox* buttonBox = 32 | new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); 33 | layout->addWidget(buttonBox); 34 | buttonBox->setObjectName("buttonBox"); 35 | 36 | connect( 37 | loggerNameEdit, 38 | &QLineEdit::textChanged, 39 | this, 40 | [ this, backgroundColorEdit, textColorEdit, checkBoxBold ]( 41 | const QString& name 42 | ) { 43 | std::string namestdstr = name.toStdString(); 44 | auto bg = _model->getLoggerBackground(namestdstr); 45 | auto fg = _model->getLoggerForeground(namestdstr); 46 | auto fnt = _model->getLoggerFont(namestdstr); 47 | 48 | if (bg) 49 | backgroundColorEdit->setText(bg.value().color().name()); 50 | else 51 | backgroundColorEdit->setText(""); 52 | 53 | if (fg) 54 | textColorEdit->setText(fg.value().name()); 55 | else 56 | textColorEdit->setText(""); 57 | 58 | if (fnt) { 59 | bool isBold = fnt->bold(); 60 | checkBoxBold->setChecked(isBold); 61 | } else { 62 | checkBoxBold->setChecked(false); 63 | } 64 | }); 65 | 66 | connect( 67 | buttonBox, 68 | &QDialogButtonBox::accepted, 69 | this, 70 | [ this, 71 | loggerNameEdit, 72 | backgroundColorEdit, 73 | textColorEdit, 74 | checkBoxBold ]() { 75 | if (!loggerNameEdit->text().isEmpty()) 76 | reject(); 77 | 78 | _result.loggerName = loggerNameEdit->text().toStdString(); 79 | 80 | if (!backgroundColorEdit->text().isEmpty()) 81 | _result.backgroundColor = QColor(backgroundColorEdit->text()); 82 | else 83 | _result.backgroundColor = std::nullopt; 84 | 85 | if (!textColorEdit->text().isEmpty()) 86 | _result.textColor = QColor(textColorEdit->text()); 87 | else 88 | _result.textColor = std::nullopt; 89 | 90 | _result.fontBold = checkBoxBold->isChecked(); 91 | 92 | accept(); 93 | }); 94 | 95 | connect(buttonBox, &QDialogButtonBox::rejected, this, [ this ]() { 96 | reject(); 97 | }); 98 | } 99 | 100 | QSpdLogStyleDialog::~QSpdLogStyleDialog() = default; 101 | 102 | QSpdLogStyleDialog::Style QSpdLogStyleDialog::result() const { return _result; } 103 | 104 | void QSpdLogStyleDialog::setModel(const QSpdLogModel* model) { _model = model; } 105 | -------------------------------------------------------------------------------- /src/qspdlog_style_dialog.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | class QSpdLogModel; 7 | 8 | class QSpdLogStyleDialog : public QDialog 9 | { 10 | Q_OBJECT 11 | 12 | public: 13 | struct Style { 14 | std::string loggerName; 15 | std::optional backgroundColor; 16 | std::optional textColor; 17 | bool fontBold; 18 | }; 19 | 20 | public: 21 | explicit QSpdLogStyleDialog(QWidget* parent = nullptr); 22 | ~QSpdLogStyleDialog() override; 23 | 24 | Style result() const; 25 | void setModel(const QSpdLogModel* model); 26 | 27 | private: 28 | Style _result; 29 | const QSpdLogModel* _model; 30 | }; 31 | -------------------------------------------------------------------------------- /src/qspdlog_toolbar.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "qspdlog_toolbar.hpp" 10 | 11 | QSpdLogToolBar::QSpdLogToolBar(QWidget* parent) 12 | : QToolBar(parent) 13 | , _filterWidget(new QLineEdit(this)) 14 | , _clearHistory(new QAction("Clear History", this)) 15 | , _autoScrollPolicy(new QComboBox(this)) 16 | , _completerData(new QStringListModel(this)) 17 | , _completer(new QCompleter(_completerData, this)) 18 | { 19 | addWidget(_filterWidget); 20 | _filterWidget->setObjectName("filterText"); 21 | 22 | _caseAction = addAction("Aa"); 23 | _caseAction->setCheckable(true); 24 | _caseAction->setObjectName("caseSensitiveAction"); 25 | 26 | _regexAction = addAction(".*"); 27 | _regexAction->setCheckable(true); 28 | _regexAction->setObjectName("regexAction"); 29 | 30 | _clearHistory->setObjectName("clearHistoryAction"); 31 | 32 | _styleAction = addAction("Set style"); 33 | _styleAction->setObjectName("styleAction"); 34 | 35 | _autoScrollPolicy->setObjectName("_autoScrollPolicy"); 36 | _autoScrollPolicy->addItems( 37 | { "Manual Scroll", "Scroll To Bottom", "Smart Scroll" } 38 | ); 39 | addWidget(_autoScrollPolicy); 40 | 41 | auto lineEdit = static_cast(_filterWidget); 42 | 43 | lineEdit->setPlaceholderText("Filter"); 44 | 45 | _completer->setCaseSensitivity(Qt::CaseInsensitive); 46 | _completer->setCompletionMode(QCompleter::PopupCompletion); 47 | lineEdit->setCompleter(_completer); 48 | 49 | connect( 50 | lineEdit, &QLineEdit::textChanged, this, &QSpdLogToolBar::filterChanged 51 | ); 52 | connect(lineEdit, &QLineEdit::editingFinished, this, [ this ]() { 53 | QStringListModel* model = 54 | static_cast(_completerData); 55 | QString text = static_cast(_filterWidget)->text(); 56 | if (text.isEmpty() || model->stringList().contains(text)) 57 | return; 58 | 59 | if (model->insertRow(model->rowCount())) { 60 | QModelIndex index = model->index(model->rowCount() - 1, 0); 61 | model->setData(index, text); 62 | } 63 | saveCompleterHistory(); 64 | }); 65 | connect( 66 | _caseAction, &QAction::toggled, this, &QSpdLogToolBar::filterChanged 67 | ); 68 | connect( 69 | _regexAction, &QAction::toggled, this, &QSpdLogToolBar::filterChanged 70 | ); 71 | connect(_styleAction, &QAction::triggered, this, [ this ]() { 72 | emit styleChangeRequested(); 73 | }); 74 | connect( 75 | _autoScrollPolicy, 76 | QOverload::of(&QComboBox::currentIndexChanged), 77 | this, 78 | &QSpdLogToolBar::autoScrollPolicyChanged 79 | ); 80 | connect( 81 | this, 82 | &QSpdLogToolBar::filterChanged, 83 | this, 84 | &QSpdLogToolBar::checkInputValidity 85 | ); 86 | connect( 87 | _clearHistory, 88 | &QAction::triggered, 89 | this, 90 | &QSpdLogToolBar::clearCompleterHistory 91 | ); 92 | loadCompleterHistory(); 93 | } 94 | 95 | QSpdLogToolBar::~QSpdLogToolBar() { } 96 | 97 | #pragma region QAbstractSpdLogToolBar 98 | 99 | QLineEdit* QSpdLogToolBar::filter() 100 | { 101 | return static_cast(_filterWidget); 102 | } 103 | 104 | QAction* QSpdLogToolBar::caseSensitive() { return _caseAction; } 105 | 106 | QAction* QSpdLogToolBar::regex() { return _regexAction; } 107 | 108 | QAction* QSpdLogToolBar::clearHistory() { return _clearHistory; } 109 | 110 | QAction* QSpdLogToolBar::style() { return _styleAction; } 111 | 112 | QComboBox* QSpdLogToolBar::autoScrollPolicy() { return _autoScrollPolicy; } 113 | 114 | #pragma endregion 115 | 116 | QSpdLogToolBar::FilteringSettings QSpdLogToolBar::filteringSettings() const 117 | { 118 | return { static_cast(_filterWidget)->text(), 119 | _regexAction->isChecked(), 120 | _caseAction->isChecked() }; 121 | } 122 | 123 | void QSpdLogToolBar::checkInputValidity() 124 | { 125 | FilteringSettings settings = filteringSettings(); 126 | 127 | if (!settings.isRegularExpression) { 128 | // everything is ok, the input text is valid 129 | _filterWidget->setPalette(QWidget::palette()); 130 | _filterWidget->setToolTip(""); 131 | return; 132 | } 133 | 134 | QRegularExpression regex(settings.text); 135 | if (regex.isValid()) { 136 | _filterWidget->setPalette(QWidget::palette()); 137 | _filterWidget->setToolTip(""); 138 | return; 139 | } 140 | 141 | QPalette palette = _filterWidget->palette(); 142 | palette.setColor(QPalette::Text, Qt::red); 143 | _filterWidget->setPalette(palette); 144 | _filterWidget->setToolTip(regex.errorString()); 145 | } 146 | 147 | void QSpdLogToolBar::clearCompleterHistory() 148 | { 149 | QStringListModel* model = static_cast(_completerData); 150 | model->setStringList({}); 151 | saveCompleterHistory(); 152 | } 153 | 154 | void QSpdLogToolBar::loadCompleterHistory() 155 | { 156 | QStringListModel* model = static_cast(_completerData); 157 | model->setStringList( 158 | QSettings("./qspdlog_filter_history", QSettings::NativeFormat) 159 | .value("completerHistory") 160 | .toStringList() 161 | ); 162 | } 163 | 164 | void QSpdLogToolBar::saveCompleterHistory() 165 | { 166 | QStringListModel* model = static_cast(_completerData); 167 | QSettings("./qspdlog_filter_history", QSettings::NativeFormat) 168 | .setValue("completerHistory", model->stringList()); 169 | } 170 | 171 | extern QAbstractSpdLogToolBar* createToolBar() { return new QSpdLogToolBar(); } 172 | -------------------------------------------------------------------------------- /src/qspdlog_toolbar.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "qspdlog/qabstract_spdlog_toolbar.hpp" 6 | 7 | class QWidget; 8 | class QAction; 9 | class QCompleter; 10 | class QAbstractItemModel; 11 | class QSettings; 12 | 13 | class QSpdLogToolBar 14 | : public QToolBar 15 | , public QAbstractSpdLogToolBar 16 | { 17 | Q_OBJECT 18 | 19 | public: 20 | struct FilteringSettings { 21 | QString text; // The text to filter by. 22 | bool isRegularExpression; // Whether the text is a regular expression. 23 | bool isCaseSensitive; // Whether the filtering is case sensitive. 24 | }; 25 | 26 | public: 27 | QSpdLogToolBar(QWidget* parent = nullptr); 28 | ~QSpdLogToolBar(); 29 | 30 | #pragma region QAbstractSpdLogToolBar 31 | QLineEdit* filter() override; 32 | QAction* caseSensitive() override; 33 | QAction* regex() override; 34 | QAction* clearHistory() override; 35 | QAction* style() override; 36 | QComboBox* autoScrollPolicy() override; 37 | #pragma endregion 38 | 39 | FilteringSettings filteringSettings() const; 40 | void checkInputValidity(); 41 | void clearCompleterHistory(); 42 | 43 | signals: 44 | void styleChangeRequested(); 45 | void filterChanged(); 46 | void autoScrollPolicyChanged(int index); 47 | 48 | private: 49 | void loadCompleterHistory(); 50 | void saveCompleterHistory(); 51 | 52 | private: 53 | QWidget* _filterWidget; 54 | QAction* _caseAction; 55 | QAction* _regexAction; 56 | QAction* _clearHistory; 57 | QAction* _styleAction; 58 | QComboBox* _autoScrollPolicy; 59 | QAbstractItemModel* _completerData; 60 | QCompleter* _completer; 61 | }; 62 | -------------------------------------------------------------------------------- /src/qt_logger_sink.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "qspdlog_model.hpp" 6 | 7 | template 8 | class qt_logger_sink : public spdlog::sinks::base_sink 9 | { 10 | public: 11 | qt_logger_sink(QSpdLogModel* model) 12 | : _model(model) 13 | { 14 | } 15 | 16 | void invalidate() { _model = nullptr; } 17 | 18 | protected: 19 | void sink_it_(const spdlog::details::log_msg& msg) override 20 | { 21 | if (!_model) 22 | return; 23 | 24 | _model->addEntry({ msg.time.time_since_epoch(), 25 | msg.level, 26 | fmt::to_string(msg.payload), 27 | fmt::to_string(msg.logger_name) }); 28 | } 29 | 30 | void flush_() override { } 31 | 32 | private: 33 | QSpdLogModel* _model; 34 | }; 35 | 36 | #include 37 | 38 | #include "spdlog/details/null_mutex.h" 39 | using qt_logger_sink_mt = qt_logger_sink; 40 | using qt_logger_sink_st = qt_logger_sink; 41 | -------------------------------------------------------------------------------- /src/res/critical.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsdever/qspdlog/7db53413c4b3d16a52b1c22f281a8c39e2148851/src/res/critical.png -------------------------------------------------------------------------------- /src/res/debug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsdever/qspdlog/7db53413c4b3d16a52b1c22f281a8c39e2148851/src/res/debug.png -------------------------------------------------------------------------------- /src/res/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsdever/qspdlog/7db53413c4b3d16a52b1c22f281a8c39e2148851/src/res/error.png -------------------------------------------------------------------------------- /src/res/info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsdever/qspdlog/7db53413c4b3d16a52b1c22f281a8c39e2148851/src/res/info.png -------------------------------------------------------------------------------- /src/res/trace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsdever/qspdlog/7db53413c4b3d16a52b1c22f281a8c39e2148851/src/res/trace.png -------------------------------------------------------------------------------- /src/res/warn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsdever/qspdlog/7db53413c4b3d16a52b1c22f281a8c39e2148851/src/res/warn.png -------------------------------------------------------------------------------- /test/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | find_package(Qt${QT_VERSION} REQUIRED COMPONENTS Test) 2 | 3 | add_executable(qspdlog_test_ui test_qspdlog.cpp) 4 | add_executable(qspdlog::test::ui ALIAS qspdlog_test_ui) 5 | 6 | target_link_libraries(qspdlog_test_ui PUBLIC Qt::Test qspdlog::lib) 7 | 8 | add_test(NAME qspdlog_test_ui COMMAND qspdlog_test_ui) 9 | -------------------------------------------------------------------------------- /test/test_qspdlog.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | #include "qspdlog/qabstract_spdlog_toolbar.hpp" 21 | #include "qspdlog/qspdlog.hpp" 22 | #include "spdlog/spdlog.h" 23 | 24 | class QTestToolBar : public QAbstractSpdLogToolBar 25 | { 26 | public: 27 | explicit QTestToolBar() 28 | { 29 | _autoScrollPolicy->addItems( 30 | { "Manual Scroll", "Scroll To Bottom", "Smart Scroll" } 31 | ); 32 | _regex->setCheckable(true); 33 | _caseSensitive->setCheckable(true); 34 | } 35 | ~QTestToolBar() override = default; 36 | 37 | #pragma region QAbstractSpdLogToolBar 38 | 39 | public: 40 | QLineEdit* filter() override { return _filter; } 41 | QAction* regex() override { return _regex; } 42 | QAction* caseSensitive() override { return _caseSensitive; } 43 | QAction* clearHistory() override { return _clearHistory; } 44 | QAction* style() override { return _style; } 45 | QComboBox* autoScrollPolicy() override { return _autoScrollPolicy; } 46 | 47 | #pragma endregion 48 | 49 | public: 50 | QLineEdit* _filter = new QLineEdit; 51 | QAction* _regex = new QAction; 52 | QAction* _caseSensitive = new QAction; 53 | QAction* _clearHistory = new QAction; 54 | QAction* _style = new QAction; 55 | QComboBox* _autoScrollPolicy = new QComboBox; 56 | }; 57 | 58 | class QSpdLogTest : public QObject 59 | { 60 | Q_OBJECT 61 | 62 | public: 63 | QSpdLogTest() { } 64 | 65 | private slots: 66 | void checkMessageCountAllLevelsEnabled() 67 | { 68 | QSpdLog widget; 69 | std::shared_ptr logger = 70 | std::make_shared("test"); 71 | 72 | logger->sinks().push_back(widget.sink()); 73 | logger->flush_on(spdlog::level::trace); 74 | logger->set_level(spdlog::level::trace); 75 | logger->trace("test"); 76 | logger->debug("test"); 77 | logger->info("test"); 78 | logger->warn("test"); 79 | logger->error("test"); 80 | logger->critical("test"); 81 | 82 | QCOMPARE(widget.itemsCount(), 6); 83 | } 84 | 85 | void disconnectionOfTheSink() 86 | { 87 | std::unique_ptr widget = std::make_unique(); 88 | std::shared_ptr logger = 89 | std::make_shared("test"); 90 | 91 | logger->sinks().push_back(widget->sink()); 92 | logger->flush_on(spdlog::level::trace); 93 | logger->info("test"); 94 | QCOMPARE(widget->itemsCount(), 1); 95 | widget.reset(); 96 | logger->info("test"); 97 | logger->flush(); 98 | } 99 | 100 | void addSinkToLoggerAndDestroy() 101 | { 102 | std::shared_ptr logger = 103 | std::make_shared("test"); 104 | { 105 | std::unique_ptr widget = std::make_unique(); 106 | logger->sinks().push_back(widget->sink()); 107 | logger->flush_on(spdlog::level::trace); 108 | logger->info("test"); 109 | QCOMPARE(widget->itemsCount(), 1); 110 | } 111 | logger->info("test"); 112 | logger->flush(); 113 | } 114 | 115 | void clearLogHistory() 116 | { 117 | QSpdLog widget; 118 | std::shared_ptr logger = 119 | std::make_shared("test"); 120 | 121 | logger->sinks().push_back(widget.sink()); 122 | logger->flush_on(spdlog::level::trace); 123 | logger->info("test"); 124 | QCOMPARE(widget.itemsCount(), 1); 125 | widget.clear(); 126 | QCOMPARE(widget.itemsCount(), 0); 127 | } 128 | 129 | void limitLogTest() 130 | { 131 | QSpdLog widget; 132 | QCOMPARE(widget.getMaxEntries(), std::nullopt); 133 | widget.setMaxEntries(20); 134 | QCOMPARE(widget.getMaxEntries(), 20); 135 | auto logger = std::make_shared("test"); 136 | 137 | logger->sinks().push_back(widget.sink()); 138 | logger->flush_on(spdlog::level::trace); 139 | logger->info("test"); 140 | QCOMPARE(widget.itemsCount(), 1); 141 | for (int i = 0; i < 100; i++) 142 | logger->info("test {0}", i); 143 | logger->flush(); 144 | QCOMPARE(widget.itemsCount(), 20); 145 | widget.setMaxEntries(std::nullopt); 146 | QCOMPARE(widget.getMaxEntries(), std::nullopt); 147 | for (int i = 0; i < 50; i++) 148 | logger->info("test2 {0}", i); 149 | QCOMPARE(widget.itemsCount(), 70); 150 | widget.setMaxEntries(20); 151 | QCOMPARE(widget.getMaxEntries(), 20); 152 | QCOMPARE(widget.itemsCount(), 20); 153 | } 154 | 155 | void backgroundForegroundColorTest() 156 | { 157 | QSpdLog widget; 158 | QCOMPARE(widget.getLoggerBackground("test"), std::nullopt); 159 | widget.setLoggerBackground("test", QBrush(Qt::red)); 160 | QCOMPARE(widget.getLoggerBackground("test"), QBrush(Qt::red)); 161 | QCOMPARE(widget.getLoggerBackground("test2"), std::nullopt); 162 | widget.setLoggerBackground("test", std::nullopt); 163 | QCOMPARE(widget.getLoggerBackground("test"), std::nullopt); 164 | 165 | QCOMPARE(widget.getLoggerForeground("test"), std::nullopt); 166 | widget.setLoggerForeground("test", Qt::white); 167 | QCOMPARE(widget.getLoggerForeground("test"), Qt::white); 168 | QCOMPARE(widget.getLoggerForeground("test2"), std::nullopt); 169 | widget.setLoggerForeground("test", std::nullopt); 170 | QCOMPARE(widget.getLoggerForeground("test"), std::nullopt); 171 | } 172 | 173 | void fontTest() 174 | { 175 | QSpdLog widget; 176 | QFont testFont; 177 | testFont.setBold(true); 178 | QCOMPARE(widget.getLoggerBackground("test"), std::nullopt); 179 | widget.setLoggerFont("test", testFont); 180 | QCOMPARE(widget.getLoggerFont("test"), testFont); 181 | QCOMPARE(widget.getLoggerFont("test2"), std::nullopt); 182 | widget.setLoggerFont("test", std::nullopt); 183 | widget.setLoggerFont("test2", testFont); 184 | QCOMPARE(widget.getLoggerFont("test"), std::nullopt); 185 | QCOMPARE(widget.getLoggerFont("test2"), testFont); 186 | } 187 | 188 | void runToolbarTests() 189 | { 190 | std::vector> toolbars; 191 | toolbars.push_back(std::make_unique()); 192 | toolbars.push_back( 193 | std::unique_ptr(createToolBar()) 194 | ); 195 | 196 | for (auto& toolbar : toolbars) { 197 | filterMessageAndCompletionHistory(toolbar.get()); 198 | filterCaseDependant(toolbar.get()); 199 | filterRegularExpressions(toolbar.get()); 200 | autoScrollPolicyDefault(toolbar.get()); 201 | autoScrollPolicyAutoScroll(toolbar.get()); 202 | autoScrollPolicySmartScroll(toolbar.get()); 203 | toolbar->setParent(nullptr); 204 | } 205 | } 206 | 207 | void filterMessageAndCompletionHistory(QAbstractSpdLogToolBar* toolbar) 208 | { 209 | QSpdLog widget; 210 | std::shared_ptr logger = 211 | std::make_shared("test"); 212 | 213 | logger->sinks().push_back(widget.sink()); 214 | logger->flush_on(spdlog::level::trace); 215 | logger->info("Lorem ipsum dolor sit amet, consectetur adipiscing elit"); 216 | logger->info("Another message"); 217 | widget.registerToolbar(toolbar); 218 | 219 | QCOMPARE(widget.itemsCount(), 2); 220 | QTest::keyClicks(toolbar->filter(), "ipsum"); 221 | QTest::keyClick(toolbar->filter(), Qt::Key_Enter); 222 | QCOMPARE(widget.itemsCount(), 1); 223 | toolbar->filter()->setText("Another"); 224 | QCOMPARE(widget.itemsCount(), 1); 225 | toolbar->filter()->setText(""); 226 | QTest::keyClick(toolbar->filter(), Qt::Key_Enter); 227 | QCOMPARE(widget.itemsCount(), 2); 228 | } 229 | 230 | void filterCaseDependant(QAbstractSpdLogToolBar* toolbar) 231 | { 232 | QSpdLog widget; 233 | std::shared_ptr logger = 234 | std::make_shared("test"); 235 | 236 | logger->sinks().push_back(widget.sink()); 237 | logger->flush_on(spdlog::level::trace); 238 | logger->info("Lorem ipsum dolor sit amet, consectetur adipiscing elit"); 239 | logger->info("Another message"); 240 | widget.registerToolbar(toolbar); 241 | 242 | QLineEdit* filter = toolbar->filter(); 243 | QAction* caseSensitive = toolbar->caseSensitive(); 244 | 245 | QCOMPARE(widget.itemsCount(), 2); 246 | filter->setText("Ipsum"); 247 | QCOMPARE(widget.itemsCount(), 1); 248 | caseSensitive->trigger(); 249 | QCOMPARE(widget.itemsCount(), 0); 250 | filter->setText("ipsum"); 251 | QCOMPARE(widget.itemsCount(), 1); 252 | filter->setText("nonexistent"); 253 | QCOMPARE(widget.itemsCount(), 0); 254 | filter->setText("Ipsum"); 255 | QCOMPARE(widget.itemsCount(), 0); 256 | caseSensitive->trigger(); 257 | QCOMPARE(widget.itemsCount(), 1); 258 | filter->setText(""); 259 | QCOMPARE(widget.itemsCount(), 2); 260 | caseSensitive->trigger(); 261 | QCOMPARE(widget.itemsCount(), 2); 262 | } 263 | 264 | void filterRegularExpressions(QAbstractSpdLogToolBar* toolbar) 265 | { 266 | QSpdLog widget; 267 | std::shared_ptr logger = 268 | std::make_shared("test"); 269 | 270 | logger->sinks().push_back(widget.sink()); 271 | logger->flush_on(spdlog::level::trace); 272 | logger->info("Lorem ipsum dolor sit amet, consectetur adipiscing elit"); 273 | logger->info("Another message"); 274 | widget.registerToolbar(toolbar); 275 | 276 | QLineEdit* filter = toolbar->filter(); 277 | QAction* regex = toolbar->regex(); 278 | 279 | QCOMPARE(widget.itemsCount(), 2); 280 | filter->setText("ipsum"); 281 | QCOMPARE(widget.itemsCount(), 1); 282 | regex->trigger(); 283 | QCOMPARE(widget.itemsCount(), 1); 284 | filter->setText(".*"); 285 | QCOMPARE(widget.itemsCount(), 2); 286 | filter->setText(".*amet"); 287 | QCOMPARE(widget.itemsCount(), 1); 288 | regex->trigger(); 289 | QCOMPARE(widget.itemsCount(), 0); 290 | filter->setText(".*"); 291 | QCOMPARE(widget.itemsCount(), 0); 292 | regex->trigger(); 293 | QCOMPARE(widget.itemsCount(), 2); 294 | // TODO: base implementation of the toolbar should change the color of 295 | // the invalid regex text filter->setText("\(.*"); QColor color = 296 | // filter->palette().color(QPalette::Text); QCOMPARE(color, Qt::red); 297 | // QRegularExpression re("\(.*"); 298 | // QCOMPARE(filter->toolTip(), re.errorString()); 299 | } 300 | 301 | void autoScrollPolicyDefault(QAbstractSpdLogToolBar* toolbar) 302 | { 303 | QSpdLog widget; 304 | std::shared_ptr logger = 305 | std::make_shared("test"); 306 | 307 | logger->sinks().push_back(widget.sink()); 308 | logger->set_level(spdlog::level::trace); 309 | logger->flush_on(spdlog::level::trace); 310 | widget.registerToolbar(toolbar); 311 | 312 | QComboBox* autoScrollPolicy = toolbar->autoScrollPolicy(); 313 | QTreeView* treeView = widget.findChild("qspdlogTreeView"); 314 | QScrollBar* scrollBar = treeView->verticalScrollBar(); 315 | 316 | treeView->resize(100, 100); 317 | 318 | autoScrollPolicy->setCurrentIndex( 319 | static_cast(AutoScrollPolicy::AutoScrollPolicyDisabled) 320 | ); 321 | 322 | QCOMPARE(scrollBar->value(), scrollBar->maximum()); 323 | 324 | for (int i = 0; i < 10; ++i) 325 | logger->info("test"); 326 | 327 | auto actualValue = scrollBar->value(); 328 | treeView->scrollToBottom(); 329 | auto maximumValue = scrollBar->value(); 330 | 331 | QVERIFY(actualValue != maximumValue); 332 | } 333 | 334 | void autoScrollPolicyAutoScroll(QAbstractSpdLogToolBar* toolbar) 335 | { 336 | QSpdLog widget; 337 | std::shared_ptr logger = 338 | std::make_shared("test"); 339 | 340 | logger->sinks().push_back(widget.sink()); 341 | logger->set_level(spdlog::level::trace); 342 | logger->flush_on(spdlog::level::trace); 343 | widget.registerToolbar(toolbar); 344 | 345 | QComboBox* autoScrollPolicy = toolbar->autoScrollPolicy(); 346 | QTreeView* treeView = widget.findChild("qspdlogTreeView"); 347 | QScrollBar* scrollBar = treeView->verticalScrollBar(); 348 | 349 | treeView->resize(100, 100); 350 | 351 | autoScrollPolicy->setCurrentIndex( 352 | static_cast(AutoScrollPolicy::AutoScrollPolicyEnabled) 353 | ); 354 | 355 | // fill the visible area 356 | for (int i = 0; i < 5; ++i) 357 | logger->info("test"); 358 | 359 | QCOMPARE(scrollBar->value(), scrollBar->maximum()); 360 | 361 | for (int i = 0; i < 3; ++i) 362 | logger->info("test"); 363 | 364 | QCOMPARE(scrollBar->value(), scrollBar->maximum()); 365 | 366 | widget.setAutoScrollPolicy(AutoScrollPolicy::AutoScrollPolicyDisabled); 367 | QCOMPARE( 368 | autoScrollPolicy->currentIndex(), 369 | static_cast(AutoScrollPolicy::AutoScrollPolicyDisabled) 370 | ); 371 | } 372 | 373 | void autoScrollPolicySmartScroll(QAbstractSpdLogToolBar* toolbar) 374 | { 375 | QSpdLog widget; 376 | std::shared_ptr logger = 377 | std::make_shared("test"); 378 | 379 | logger->sinks().push_back(widget.sink()); 380 | logger->set_level(spdlog::level::trace); 381 | logger->flush_on(spdlog::level::trace); 382 | widget.registerToolbar(toolbar); 383 | 384 | QComboBox* autoScrollPolicy = toolbar->autoScrollPolicy(); 385 | QTreeView* treeView = widget.findChild("qspdlogTreeView"); 386 | QScrollBar* scrollBar = treeView->verticalScrollBar(); 387 | 388 | treeView->resize(100, 100); 389 | 390 | autoScrollPolicy->setCurrentIndex( 391 | static_cast(AutoScrollPolicy::AutoScrollPolicyEnabledIfBottom) 392 | ); 393 | 394 | // fill the visible area 395 | for (int i = 0; i < 5; ++i) 396 | logger->info("test"); 397 | 398 | QCOMPARE(scrollBar->value(), scrollBar->maximum()); 399 | 400 | for (int i = 0; i < 3; ++i) 401 | logger->info("test"); 402 | 403 | QCOMPARE(scrollBar->value(), scrollBar->maximum()); 404 | 405 | scrollBar->setValue(0); 406 | for (int i = 0; i < 3; ++i) 407 | logger->info("test"); 408 | 409 | QVERIFY(scrollBar->value() != scrollBar->maximum()); 410 | 411 | scrollBar->setValue(scrollBar->maximum()); 412 | for (int i = 0; i < 3; ++i) 413 | logger->info("test"); 414 | 415 | QCOMPARE(scrollBar->value(), scrollBar->maximum()); 416 | } 417 | 418 | void headerColumnShowHide() 419 | { 420 | QSpdLog widget; 421 | QTreeView* treeView = widget.findChild("qspdlogTreeView"); 422 | QHeaderView* headerView = treeView->header(); 423 | QCOMPARE(headerView->count(), 4); 424 | QMetaObject::invokeMethod( 425 | headerView, 426 | [] { 427 | QList topLevelWidgets = QApplication::topLevelWidgets(); 428 | auto widgetsIt = std::find_if( 429 | topLevelWidgets.begin(), 430 | topLevelWidgets.end(), 431 | [](QWidget* widget) { 432 | return widget->objectName() == "qspdlogHeaderContextMenu"; 433 | }); 434 | QVERIFY(widgetsIt != topLevelWidgets.end()); 435 | QMenu* menu = qobject_cast(*widgetsIt); 436 | QTest::mouseClick( 437 | menu, Qt::LeftButton, Qt::NoModifier, QPoint(5, 5) 438 | ); 439 | }, 440 | Qt::QueuedConnection); 441 | headerView->customContextMenuRequested(QPoint(5, 5)); 442 | QCOMPARE(headerView->count(), 4); 443 | QCOMPARE(headerView->hiddenSectionCount(), 1); 444 | QMetaObject::invokeMethod( 445 | headerView, 446 | [] { 447 | QList topLevelWidgets = QApplication::topLevelWidgets(); 448 | auto widgetsIt = std::find_if( 449 | topLevelWidgets.begin(), 450 | topLevelWidgets.end(), 451 | [](QWidget* widget) { 452 | return widget->objectName() == "qspdlogHeaderContextMenu"; 453 | }); 454 | QVERIFY(widgetsIt != topLevelWidgets.end()); 455 | QMenu* menu = qobject_cast(*widgetsIt); 456 | QTest::mouseClick( 457 | menu, Qt::LeftButton, Qt::NoModifier, QPoint(5, 25) 458 | ); 459 | }, 460 | Qt::QueuedConnection); 461 | headerView->customContextMenuRequested(QPoint(5, 5)); 462 | QCOMPARE(headerView->count(), 4); 463 | QCOMPARE(headerView->hiddenSectionCount(), 2); 464 | QMetaObject::invokeMethod( 465 | headerView, 466 | [] { 467 | QList topLevelWidgets = QApplication::topLevelWidgets(); 468 | auto widgetsIt = std::find_if( 469 | topLevelWidgets.begin(), 470 | topLevelWidgets.end(), 471 | [](QWidget* widget) { 472 | return widget->objectName() == "qspdlogHeaderContextMenu"; 473 | }); 474 | QVERIFY(widgetsIt != topLevelWidgets.end()); 475 | QMenu* menu = qobject_cast(*widgetsIt); 476 | QTest::mouseClick( 477 | menu, Qt::LeftButton, Qt::NoModifier, QPoint(5, 5) 478 | ); 479 | }, 480 | Qt::QueuedConnection); 481 | headerView->customContextMenuRequested(QPoint(5, 5)); 482 | QCOMPARE(headerView->count(), 4); 483 | QCOMPARE(headerView->hiddenSectionCount(), 1); 484 | } 485 | 486 | void setStyleFromToolbar() 487 | { 488 | QSpdLog widget; 489 | auto testLogger = std::make_shared("test"); 490 | auto testLogger1 = std::make_shared("test1"); 491 | 492 | testLogger->sinks().push_back(widget.sink()); 493 | testLogger1->sinks().push_back(widget.sink()); 494 | 495 | std::unique_ptr toolbar(createToolBar()); 496 | widget.registerToolbar(toolbar.get()); 497 | QAction* style = toolbar->style(); 498 | 499 | QFont f; 500 | f.setBold(true); 501 | auto dialogManipThread = 502 | manipulateStyleDialog("test", "#ff0000", "#00ff00", f, true); 503 | style->trigger(); 504 | dialogManipThread.join(); 505 | 506 | testLogger->info("test"); 507 | testLogger1->info("test1"); 508 | 509 | const QTreeView* treeView = 510 | widget.findChild("qspdlogTreeView"); 511 | const QAbstractItemModel* model = treeView->model(); 512 | QModelIndex index = model->index(0, 3); 513 | QCOMPARE( 514 | model->data(index, Qt::DisplayRole).value(), 515 | QString("test") 516 | ); 517 | QCOMPARE( 518 | model->data(index, Qt::BackgroundRole).value(), 519 | QColor("#ff0000") 520 | ); 521 | QCOMPARE( 522 | model->data(index, Qt::ForegroundRole).value(), 523 | QColor("#00ff00") 524 | ); 525 | QCOMPARE( 526 | model->data(index, Qt::FontRole).value(), 527 | f 528 | ); 529 | 530 | index = model->index(1, 3); 531 | QCOMPARE( 532 | model->data(index, Qt::DisplayRole).value(), 533 | QString("test1") 534 | ); 535 | QCOMPARE( 536 | model->data(index, Qt::BackgroundRole).value(), QColor {} 537 | ); 538 | QCOMPARE( 539 | model->data(index, Qt::ForegroundRole).value(), QColor {} 540 | ); 541 | QCOMPARE( 542 | model->data(index, Qt::FontRole).value(), QFont {} 543 | ); 544 | 545 | dialogManipThread = 546 | manipulateStyleDialog("test", std::nullopt, std::nullopt, std::nullopt, true); 547 | style->trigger(); 548 | dialogManipThread.join(); 549 | 550 | index = model->index(0, 3); 551 | QCOMPARE( 552 | model->data(index, Qt::DisplayRole).value(), 553 | QString("test") 554 | ); 555 | QCOMPARE( 556 | model->data(index, Qt::BackgroundRole).value(), 557 | QColor("#ff0000") 558 | ); 559 | QCOMPARE( 560 | model->data(index, Qt::ForegroundRole).value(), 561 | QColor("#00ff00") 562 | ); 563 | QCOMPARE( 564 | model->data(index, Qt::FontRole).value(), 565 | f 566 | ); 567 | 568 | dialogManipThread = manipulateStyleDialog("test", "", "", QFont(), true); 569 | style->trigger(); 570 | dialogManipThread.join(); 571 | 572 | index = model->index(0, 3); 573 | QCOMPARE( 574 | model->data(index, Qt::DisplayRole).value(), 575 | QString("test") 576 | ); 577 | QCOMPARE( 578 | model->data(index, Qt::BackgroundRole).value(), QColor {} 579 | ); 580 | QCOMPARE( 581 | model->data(index, Qt::ForegroundRole).value(), QColor {} 582 | ); 583 | QCOMPARE( 584 | model->data(index, Qt::FontRole).value(), QFont {} 585 | ); 586 | 587 | dialogManipThread = manipulateStyleDialog("test", "", "", QFont(), false); 588 | style->trigger(); 589 | dialogManipThread.join(); 590 | 591 | index = model->index(0, 3); 592 | QCOMPARE( 593 | model->data(index, Qt::DisplayRole).value(), 594 | QString("test") 595 | ); 596 | QCOMPARE( 597 | model->data(index, Qt::BackgroundRole).value(), QColor {} 598 | ); 599 | QCOMPARE( 600 | model->data(index, Qt::ForegroundRole).value(), QColor {} 601 | ); 602 | QCOMPARE( 603 | model->data(index, Qt::FontRole).value(), QFont {} 604 | ); 605 | index = model->index(1, 3); 606 | QCOMPARE( 607 | model->data(index, Qt::DisplayRole).value(), 608 | QString("test1") 609 | ); 610 | QCOMPARE( 611 | model->data(index, Qt::BackgroundRole).value(), QColor {} 612 | ); 613 | QCOMPARE( 614 | model->data(index, Qt::ForegroundRole).value(), QColor {} 615 | ); 616 | QCOMPARE( 617 | model->data(index, Qt::FontRole).value(), QFont {} 618 | ); 619 | } 620 | 621 | private: 622 | std::thread manipulateStyleDialog( 623 | std::optional name, 624 | std::optional background, 625 | std::optional foreground, 626 | std::optional font, 627 | bool accept 628 | ) const 629 | { 630 | return std::thread([ n = std::move(name), 631 | bg = std::move(background), 632 | fg = std::move(foreground), 633 | fnt = std::move(font), 634 | accept ] { 635 | QDialog* dialog; 636 | bool success = QTest::qWaitFor( 637 | [ &dialog ]() -> bool { 638 | auto widgets = qApp->topLevelWidgets(); 639 | auto it = std::find_if( 640 | widgets.begin(), 641 | widgets.end(), 642 | [](QWidget* widget) { 643 | return widget->objectName() == "qspdlogStyleDialog"; 644 | }); 645 | 646 | if (it == widgets.end()) 647 | return false; 648 | 649 | dialog = qobject_cast(*it); 650 | return true; 651 | }, 652 | 1000 653 | ); 654 | 655 | QVERIFY(success); 656 | 657 | QMetaObject::invokeMethod( 658 | dialog, 659 | [ name = std::move(n), 660 | background = std::move(bg), 661 | foreground = std::move(fg), 662 | font = std::move(fnt), 663 | accept, 664 | dialog ] { 665 | QVERIFY(dialog); 666 | QLineEdit* loggerNameEdit = 667 | dialog->findChild("loggerNameEdit"); 668 | QVERIFY(loggerNameEdit); 669 | QLineEdit* backgroundColorEdit = 670 | dialog->findChild("backgroundColorEdit"); 671 | QVERIFY(backgroundColorEdit); 672 | QLineEdit* textColorEdit = 673 | dialog->findChild("textColorEdit"); 674 | QVERIFY(textColorEdit); 675 | QCheckBox* checkBoxBold = 676 | dialog->findChild("checkBoxBold"); 677 | QVERIFY(checkBoxBold); 678 | 679 | if (name) 680 | loggerNameEdit->setText(name.value()); 681 | 682 | if (background) 683 | backgroundColorEdit->setText(background.value()); 684 | 685 | if (foreground) 686 | textColorEdit->setText(foreground.value()); 687 | 688 | if (font) 689 | checkBoxBold->setChecked(font.value().bold()); 690 | 691 | QDialogButtonBox* buttonBox = 692 | dialog->findChild("buttonBox"); 693 | QVERIFY(buttonBox); 694 | if (accept) 695 | buttonBox->button(QDialogButtonBox::Ok)->click(); 696 | else 697 | buttonBox->button(QDialogButtonBox::Cancel)->click(); 698 | }, 699 | Qt::QueuedConnection); 700 | }); 701 | } 702 | }; 703 | 704 | QTEST_MAIN(QSpdLogTest); 705 | #include "test_qspdlog.moc" 706 | --------------------------------------------------------------------------------