├── .github ├── FUNDING.yml └── workflows │ ├── macos.yml │ ├── reuse-check.yml │ ├── ubuntu.yml │ └── windows.yml ├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── LICENSES ├── CC0-1.0.txt └── MIT.txt ├── NEWS ├── README.md ├── README.zh_CN.md ├── REUSE.toml ├── app ├── aboutdialog.cpp ├── aboutdialog.h ├── actionmanager.cpp ├── actionmanager.h ├── bottombuttongroup.cpp ├── bottombuttongroup.h ├── exiv2wrapper.cpp ├── exiv2wrapper.h ├── fileopeneventhandler.cpp ├── fileopeneventhandler.h ├── framelesswindow.cpp ├── framelesswindow.h ├── graphicsscene.cpp ├── graphicsscene.h ├── graphicsview.cpp ├── graphicsview.h ├── main.cpp ├── mainwindow.cpp ├── mainwindow.h ├── metadatadialog.cpp ├── metadatadialog.h ├── metadatamodel.cpp ├── metadatamodel.h ├── navigatorview.cpp ├── navigatorview.h ├── opacityhelper.cpp ├── opacityhelper.h ├── playlistmanager.cpp ├── playlistmanager.h ├── settings.cpp ├── settings.h ├── settingsdialog.cpp ├── settingsdialog.h ├── shortcutedit.cpp ├── shortcutedit.h ├── toolbutton.cpp ├── toolbutton.h └── translations │ ├── PineapplePictures_ca.ts │ ├── PineapplePictures_de.ts │ ├── PineapplePictures_en.ts │ ├── PineapplePictures_es.ts │ ├── PineapplePictures_fr.ts │ ├── PineapplePictures_id.ts │ ├── PineapplePictures_it.ts │ ├── PineapplePictures_ja.ts │ ├── PineapplePictures_ko.ts │ ├── PineapplePictures_nb_NO.ts │ ├── PineapplePictures_nl.ts │ ├── PineapplePictures_pa_PK.ts │ ├── PineapplePictures_ru.ts │ ├── PineapplePictures_si.ts │ ├── PineapplePictures_ta.ts │ ├── PineapplePictures_tr.ts │ ├── PineapplePictures_uk.ts │ └── PineapplePictures_zh_CN.ts ├── appveyor.yml ├── assets ├── icons │ ├── app-icon.icns │ ├── app-icon.ico │ ├── app-icon.svg │ ├── go-next.svg │ ├── go-previous.svg │ ├── object-rotate-right.svg │ ├── view-background-checkerboard.svg │ ├── view-fullscreen.svg │ ├── window-close.svg │ ├── zoom-in.svg │ ├── zoom-original.svg │ └── zoom-out.svg ├── pineapple-pictures.rc ├── plain │ └── translators.html └── resources.qrc ├── dist ├── MacOSXBundleInfo.plist.in ├── appstream │ ├── net.blumia.pineapple-pictures.metainfo.xml │ └── po │ │ ├── net.blumia.pineapple-pictures.metainfo.es.po │ │ ├── net.blumia.pineapple-pictures.metainfo.ja.po │ │ ├── net.blumia.pineapple-pictures.metainfo.nl.po │ │ ├── net.blumia.pineapple-pictures.metainfo.pot │ │ ├── net.blumia.pineapple-pictures.metainfo.ru.po │ │ ├── net.blumia.pineapple-pictures.metainfo.ta.po │ │ ├── net.blumia.pineapple-pictures.metainfo.uk.po │ │ └── net.blumia.pineapple-pictures.metainfo.zh_CN.po └── net.blumia.pineapple-pictures.desktop └── pineapple-pictures.pro /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: blumia 2 | custom: ["https://blumia.itch.io/pineapple-pictures"] 3 | -------------------------------------------------------------------------------- /.github/workflows/macos.yml: -------------------------------------------------------------------------------- 1 | name: macOS CI 2 | 3 | on: [push, pull_request, workflow_dispatch] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: macos-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Install Qt 13 | uses: jurplel/install-qt-action@v4 14 | with: 15 | version: '6.9.1' 16 | modules: 'qtimageformats' 17 | - name: Install Conan and Dependencies 18 | id: conan 19 | working-directory: ./ 20 | shell: bash 21 | run: | 22 | pip3 install wheel setuptools 23 | pip3 install conan --upgrade 24 | conan --version 25 | conan profile detect 26 | conan install --requires=exiv2/0.28.3 --generator CMakeDeps --generator CMakeToolchain --build=missing 27 | - name: Build 28 | run: | 29 | cmake . -DTRANSLATION_RESOURCE_EMBEDDING=ON --preset conan-release 30 | cmake --build --preset conan-release 31 | - name: Deploy 32 | run: | 33 | macdeployqt ./ppic.app -dmg 34 | ls 35 | - uses: actions/upload-artifact@v4 36 | with: 37 | name: "macos-bundle" 38 | path: "*.dmg" 39 | -------------------------------------------------------------------------------- /.github/workflows/reuse-check.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Free Software Foundation Europe e.V. 2 | # 3 | # SPDX-License-Identifier: CC0-1.0 4 | 5 | name: REUSE Compliance Check 6 | 7 | on: [push, pull_request] 8 | 9 | jobs: 10 | reuse-compliance-check: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - name: REUSE Compliance Check 17 | uses: fsfe/reuse-action@v5 18 | -------------------------------------------------------------------------------- /.github/workflows/ubuntu.yml: -------------------------------------------------------------------------------- 1 | name: Ubuntu CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | ubuntu-24-04-build: 7 | runs-on: ubuntu-24.04 8 | steps: 9 | - uses: actions/checkout@v4 10 | - name: Get build dept. 11 | run: | 12 | sudo apt update 13 | sudo apt install cmake qt6-base-dev qt6-svg-dev qt6-tools-dev libexiv2-dev 14 | - name: Build it 15 | run: | 16 | mkdir build 17 | cd build 18 | cmake ../ 19 | make 20 | cpack -G DEB 21 | - name: Try install it 22 | run: | 23 | cd build 24 | sudo apt install ./*.deb 25 | - uses: actions/upload-artifact@v4 26 | with: 27 | name: ubuntu-24.04-deb-package 28 | path: build/*.deb 29 | -------------------------------------------------------------------------------- /.github/workflows/windows.yml: -------------------------------------------------------------------------------- 1 | name: Windows CI 2 | 3 | on: [push, pull_request, workflow_dispatch] 4 | 5 | jobs: 6 | msvc-qmake-build: 7 | 8 | strategy: 9 | matrix: 10 | include: 11 | - qt_ver: '6.9.1' 12 | vs: '2022' 13 | aqt_arch: 'win64_msvc2022_64' 14 | msvc_arch: 'x64' 15 | 16 | runs-on: windows-2022 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Install Qt 21 | uses: jurplel/install-qt-action@v4 22 | with: 23 | arch: ${{ matrix.aqt_arch }} 24 | version: ${{ matrix.qt_ver }} 25 | modules: 'qtimageformats' 26 | - name: Build 27 | shell: cmd 28 | run: | 29 | set VS=${{ matrix.vs }} 30 | set VCVARS="C:\Program Files (x86)\Microsoft Visual Studio\%VS%\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" 31 | if not exist %VCVARS% set VCVARS="C:\Program Files\Microsoft Visual Studio\%VS%\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" 32 | call %VCVARS% ${{ matrix.msvc_arch }} 33 | qmake pineapple-pictures.pro 34 | nmake 35 | nmake clean 36 | windeployqt --verbose=2 --no-quick-import --no-translations --no-opengl-sw --no-system-d3d-compiler --no-system-dxc-compiler --skip-plugin-types tls,networkinformation release\ppic.exe 37 | - uses: actions/upload-artifact@v4 38 | with: 39 | name: "windows-msvc${{ matrix.vs }}-qt${{ matrix.qt_ver }}-qmake-package" 40 | path: release/* 41 | 42 | msvc-cmake-build: 43 | 44 | strategy: 45 | matrix: 46 | include: 47 | - qt_ver: '6.9.1' 48 | vs: '2022' 49 | aqt_arch: 'win64_msvc2022_64' 50 | msvc_arch: 'x64' 51 | 52 | runs-on: windows-2022 53 | 54 | steps: 55 | - uses: actions/checkout@v4 56 | - name: Install Qt 57 | uses: jurplel/install-qt-action@v4 58 | with: 59 | arch: ${{ matrix.aqt_arch }} 60 | version: ${{ matrix.qt_ver }} 61 | modules: 'qtimageformats' 62 | - name: Build 63 | shell: cmd 64 | run: | 65 | :: ------ env ------ 66 | set PWD=%cd% 67 | set VS=${{ matrix.vs }} 68 | set VCVARS="C:\Program Files (x86)\Microsoft Visual Studio\%VS%\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" 69 | if not exist %VCVARS% set VCVARS="C:\Program Files\Microsoft Visual Studio\%VS%\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" 70 | call %VCVARS% ${{ matrix.msvc_arch }} 71 | :: ------ dep ------ 72 | set CMAKE_PREFIX_PATH=%PWD%/dependencies_bin 73 | mkdir dependencies_src 74 | echo ::group::===== exiv2 ===== 75 | curl -fsSL -o exiv2_bin.zip https://github.com/Exiv2/exiv2/releases/download/v0.28.3/exiv2-0.28.3-2019msvc64.zip 76 | 7z x exiv2_bin.zip -y 77 | ren .\exiv2-0.28.3-2019msvc64 dependencies_bin 78 | echo ::endgroup:: 79 | echo ::group::===== zlib ===== 80 | curl -fsSL -o zlib_src.zip https://zlib.net/zlib131.zip 81 | 7z x zlib_src.zip -y -o"dependencies_src" 82 | ren .\dependencies_src\zlib-1.3.1 zlib || goto :error 83 | cmake ./dependencies_src/zlib -Bbuild_dependencies/zlib -DCMAKE_INSTALL_PREFIX="dependencies_bin" || goto :error 84 | cmake --build build_dependencies/zlib --config Release --target=install || goto :error 85 | curl -fsSL -o expat_src.zip https://github.com/libexpat/libexpat/archive/R_2_6_2.zip 86 | echo ::endgroup:: 87 | echo ::group::===== AOM for libavif AVI decoding support ===== 88 | git clone -q -b v3.12.0 --depth 1 https://aomedia.googlesource.com/aom dependencies_src/aom 89 | cmake ./dependencies_src/aom -Bbuild_dependencies/aom -DCMAKE_INSTALL_PREFIX="dependencies_bin" -DENABLE_DOCS=OFF -DBUILD_SHARED_LIBS=ON -DAOM_TARGET_CPU=generic -DENABLE_TESTS=OFF -DENABLE_TESTDATA=OFF -DENABLE_TOOLS=OFF -DENABLE_EXAMPLES=0 || goto :error 90 | cmake --build build_dependencies/aom --config Release --target=install || goto :error 91 | echo ::endgroup:: 92 | echo ::group::===== libavif ===== 93 | curl -fsSL -o libavif-v1_2_1.zip https://github.com/AOMediaCodec/libavif/archive/v1.2.1.zip 94 | 7z x libavif-v1_2_1.zip -y -o"dependencies_src" 95 | ren .\dependencies_src\libavif-1.2.1 libavif || goto :error 96 | cmake ./dependencies_src/libavif -Bbuild_dependencies/libavif -DCMAKE_INSTALL_PREFIX="dependencies_bin" -DAVIF_CODEC_AOM=ON -DAVIF_LIBYUV=LOCAL 97 | cmake --build build_dependencies/libavif --config Release --target=install || goto :error 98 | echo ::endgroup:: 99 | echo ::group::===== expat ===== 100 | 7z x expat_src.zip -y -o"dependencies_src" 101 | ren .\dependencies_src\libexpat-R_2_6_2 expat || goto :error 102 | cmake ./dependencies_src/expat/expat -Bbuild_dependencies/expat -DCMAKE_INSTALL_PREFIX="dependencies_bin" || goto :error 103 | cmake --build build_dependencies/expat --config Release --target=install || goto :error 104 | echo ::endgroup:: 105 | echo ::group::===== ECM ===== 106 | git clone -q https://invent.kde.org/frameworks/extra-cmake-modules.git dependencies_src/extra-cmake-modules 107 | cmake .\dependencies_src\extra-cmake-modules -Bbuild_dependencies/extra-cmake-modules -DCMAKE_INSTALL_PREFIX="dependencies_bin" -DBUILD_TESTING=OFF || goto :error 108 | cmake --build build_dependencies/extra-cmake-modules --config Release --target=install || goto :error 109 | echo ::endgroup:: 110 | echo ::group::===== KArchive ===== 111 | git clone -q https://invent.kde.org/frameworks/karchive.git dependencies_src/karchive 112 | cmake .\dependencies_src\karchive -Bbuild_dependencies/karchive -DWITH_LIBZSTD=OFF -DWITH_BZIP2=OFF -DWITH_LIBLZMA=OFF -DCMAKE_INSTALL_PREFIX="dependencies_bin" || goto :error 113 | cmake --build build_dependencies/karchive --config Release --target=install || goto :error 114 | echo ::endgroup:: 115 | echo ::group::===== KImageFormats ===== 116 | git clone -q https://invent.kde.org/frameworks/kimageformats.git dependencies_src/kimageformats 117 | cmake .\dependencies_src\kimageformats -Bbuild_dependencies/kimageformats -DKDE_INSTALL_QTPLUGINDIR=%QT_ROOT_DIR%\plugins || goto :error 118 | cmake --build build_dependencies/kimageformats --config Release --target=install || goto :error 119 | echo ::endgroup:: 120 | :: ------ app ------ 121 | cmake -Bbuild . -DCMAKE_INSTALL_PREFIX="%PWD%\build\" 122 | cmake --build build --config Release 123 | cmake --build build --config Release --target=install 124 | :: ------ pkg ------ 125 | windeployqt --verbose=2 --no-quick-import --no-translations --no-opengl-sw --no-system-d3d-compiler --no-system-dxc-compiler --skip-plugin-types tls,networkinformation build\bin\ppic.exe 126 | robocopy ./dependencies_bin/bin build/bin *.dll 127 | if ErrorLevel 8 (exit /B 1) 128 | copy LICENSE build\bin 129 | exit /B 0 130 | - uses: actions/upload-artifact@v4 131 | with: 132 | name: "windows-msvc${{ matrix.vs }}-qt${{ matrix.qt_ver }}-cmake-package" 133 | path: build/bin/* 134 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # User files 2 | *.user 3 | *.user.* 4 | 5 | # Why, macOS, why? 6 | .DS_Store 7 | 8 | # Translation files 9 | *.qm 10 | *.mo 11 | 12 | # Generic Build Dir 13 | [Bb]uild/ 14 | 15 | # IDE/Editor config folders 16 | .vscode/ 17 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 - 2025 Gary Wang 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | cmake_minimum_required(VERSION 3.16) 6 | 7 | project(pineapple-pictures VERSION 1.0.0) # don't forget to update NEWS file and AppStream metadata. 8 | 9 | include(GNUInstallDirs) 10 | include(FeatureSummary) 11 | 12 | option (EXIV2_METADATA_SUPPORT "Better image metadata support via libexiv2" ON) 13 | option (TRANSLATION_RESOURCE_EMBEDDING "Embedding .qm translation files inside resource" OFF) 14 | 15 | set (CMAKE_CXX_STANDARD 17) 16 | set (CMAKE_CXX_STANDARD_REQUIRED ON) 17 | set (CMAKE_AUTOMOC ON) 18 | set (CMAKE_AUTORCC ON) 19 | 20 | find_package(QT NAMES Qt6 REQUIRED COMPONENTS Core) 21 | 22 | set (QT_MINIMUM_VERSION "6.4") 23 | 24 | find_package(Qt${QT_VERSION_MAJOR} ${QT_MINIMUM_VERSION} REQUIRED 25 | COMPONENTS Widgets Svg SvgWidgets LinguistTools 26 | OPTIONAL_COMPONENTS DBus 27 | ) 28 | 29 | if (EXIV2_METADATA_SUPPORT) 30 | find_package(exiv2) 31 | set_package_properties(exiv2 PROPERTIES 32 | URL "https://www.exiv2.org" 33 | DESCRIPTION "image metadata support" 34 | TYPE RECOMMENDED 35 | PURPOSE "Bring better image metadata support" 36 | ) 37 | endif () 38 | 39 | set (PPIC_CPP_FILES 40 | app/main.cpp 41 | app/framelesswindow.cpp 42 | app/mainwindow.cpp 43 | app/actionmanager.cpp 44 | app/graphicsview.cpp 45 | app/graphicsscene.cpp 46 | app/bottombuttongroup.cpp 47 | app/navigatorview.cpp 48 | app/opacityhelper.cpp 49 | app/toolbutton.cpp 50 | app/settings.cpp 51 | app/settingsdialog.cpp 52 | app/aboutdialog.cpp 53 | app/metadatamodel.cpp 54 | app/metadatadialog.cpp 55 | app/exiv2wrapper.cpp 56 | app/playlistmanager.cpp 57 | app/shortcutedit.cpp 58 | app/fileopeneventhandler.cpp 59 | ) 60 | 61 | set (PPIC_HEADER_FILES 62 | app/framelesswindow.h 63 | app/mainwindow.h 64 | app/actionmanager.h 65 | app/graphicsview.h 66 | app/graphicsscene.h 67 | app/bottombuttongroup.h 68 | app/navigatorview.h 69 | app/opacityhelper.h 70 | app/toolbutton.h 71 | app/settings.h 72 | app/settingsdialog.h 73 | app/aboutdialog.h 74 | app/metadatamodel.h 75 | app/metadatadialog.h 76 | app/exiv2wrapper.h 77 | app/playlistmanager.h 78 | app/shortcutedit.h 79 | app/fileopeneventhandler.h 80 | ) 81 | 82 | set (PPIC_QRC_FILES 83 | assets/resources.qrc 84 | ) 85 | 86 | set (PPIC_RC_FILES 87 | # yeah, it's empty. 88 | ) 89 | 90 | set (EXE_NAME ppic) 91 | 92 | # Translation 93 | file (GLOB PPIC_TS_FILES app/translations/*.ts) 94 | set (PPIC_CPP_FILES_FOR_I18N ${PPIC_CPP_FILES}) 95 | 96 | if (WIN32) 97 | list(APPEND PPIC_RC_FILES assets/pineapple-pictures.rc) 98 | endif () 99 | 100 | add_executable (${EXE_NAME} 101 | ${PPIC_HEADER_FILES} 102 | ${PPIC_CPP_FILES} 103 | ${PPIC_QRC_FILES} 104 | ${PPIC_RC_FILES} 105 | ) 106 | 107 | set(ADD_TRANSLATIONS_ADDITIONAL_ARGS) 108 | 109 | if (Qt6_VERSION VERSION_GREATER_EQUAL "6.9.0") 110 | set(ADD_TRANSLATIONS_ADDITIONAL_ARGS MERGE_QT_TRANSLATIONS) 111 | endif() 112 | 113 | if (TRANSLATION_RESOURCE_EMBEDDING) 114 | qt_add_translations(${EXE_NAME} ${ADD_TRANSLATIONS_ADDITIONAL_ARGS} TS_FILES ${PPIC_TS_FILES}) 115 | else() 116 | qt_add_translations(${EXE_NAME} ${ADD_TRANSLATIONS_ADDITIONAL_ARGS} TS_FILES ${PPIC_TS_FILES} QM_FILES_OUTPUT_VARIABLE PPIC_QM_FILES) 117 | endif() 118 | 119 | target_sources(${EXE_NAME} PRIVATE ${PPIC_QM_FILES}) 120 | 121 | target_link_libraries (${EXE_NAME} Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Svg Qt${QT_VERSION_MAJOR}::SvgWidgets) 122 | 123 | if (exiv2_FOUND) 124 | if(NOT TARGET Exiv2::exiv2lib AND TARGET exiv2lib) 125 | # for exiv2 0.27.x and (macOS?) conan build 126 | add_library(Exiv2::exiv2lib ALIAS exiv2lib) 127 | endif() 128 | target_link_libraries (${EXE_NAME} 129 | Exiv2::exiv2lib 130 | ) 131 | target_compile_definitions(${EXE_NAME} PRIVATE 132 | HAVE_EXIV2_VERSION="${exiv2_VERSION}" 133 | ) 134 | endif () 135 | 136 | if (TARGET Qt6::DBus) 137 | target_link_libraries (${EXE_NAME} 138 | Qt${QT_VERSION_MAJOR}::DBus 139 | ) 140 | target_compile_definitions(${EXE_NAME} PRIVATE 141 | HAVE_QTDBUS 142 | ) 143 | endif() 144 | 145 | # Extra build settings 146 | if (WIN32) 147 | target_compile_definitions(${EXE_NAME} PRIVATE 148 | FLAG_PORTABLE_MODE_SUPPORT=1 149 | ) 150 | endif () 151 | 152 | # Helper macros for parsing and setting project version from `git describe --long` result 153 | macro (ppic_set_version_via_describe _describe_long) 154 | string ( 155 | REGEX REPLACE 156 | "^([0-9a-z.]*)-[0-9]+-g[0-9a-f]*$" 157 | "\\1" 158 | _tag_parts 159 | "${_describe_long}" 160 | ) 161 | list (GET _tag_parts 0 _matched_tag_version) 162 | if ("${_matched_tag_version}" MATCHES "^[0-9]+\\.[0-9]+\\.[0-9]+$") 163 | string ( 164 | REGEX REPLACE 165 | "^([0-9]+)\\.([0-9]+)\\.([0-9]+).*$" 166 | "\\1;\\2;\\3" 167 | _ver_parts 168 | "${_matched_tag_version}" 169 | ) 170 | list (GET _ver_parts 0 CPACK_PACKAGE_VERSION_MAJOR) 171 | list (GET _ver_parts 1 CPACK_PACKAGE_VERSION_MINOR) 172 | list (GET _ver_parts 2 CPACK_PACKAGE_VERSION_PATCH) 173 | endif () 174 | endmacro () 175 | 176 | # Version setup 177 | target_compile_definitions(${EXE_NAME} PRIVATE PPIC_VERSION_STRING="${CMAKE_PROJECT_VERSION}") 178 | if (EXISTS "${CMAKE_SOURCE_DIR}/.git") 179 | find_package(Git) 180 | set_package_properties(Git PROPERTIES TYPE OPTIONAL PURPOSE "Determine exact build version.") 181 | if (GIT_FOUND) 182 | execute_process ( 183 | COMMAND ${GIT_EXECUTABLE} describe --tags --always --long 184 | WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} 185 | OUTPUT_VARIABLE _git_describe_long 186 | ) 187 | string (REGEX REPLACE "\n" "" _git_describe_long "${_git_describe_long}") 188 | ppic_set_version_via_describe(${_git_describe_long}) 189 | target_compile_definitions(${EXE_NAME} PRIVATE 190 | GIT_DESCRIBE_VERSION_STRING="${_git_describe_long}" 191 | ) 192 | endif () 193 | endif () 194 | 195 | # Install settings 196 | if (WIN32) 197 | set_target_properties(${EXE_NAME} PROPERTIES 198 | WIN32_EXECUTABLE TRUE 199 | ) 200 | elseif (APPLE) 201 | set_source_files_properties(assets/icons/app-icon.icns PROPERTIES 202 | MACOSX_PACKAGE_LOCATION "Resources" 203 | ) 204 | target_sources(${EXE_NAME} PUBLIC assets/icons/app-icon.icns) 205 | # See https://cmake.org/cmake/help/v3.15/prop_tgt/MACOSX_BUNDLE_INFO_PLIST.html 206 | set_target_properties(${EXE_NAME} PROPERTIES 207 | MACOSX_BUNDLE TRUE 208 | MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/dist/MacOSXBundleInfo.plist.in 209 | MACOSX_BUNDLE_BUNDLE_NAME "Pineapple Pictures" 210 | MACOSX_BUNDLE_GUI_IDENTIFIER net.blumia.pineapple-pictures 211 | MACOSX_BUNDLE_ICON_FILE app-icon.icns # contains the .icns file name, *without* the path. 212 | MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION} 213 | MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR} 214 | ) 215 | elseif (UNIX) 216 | if (CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) 217 | set(CMAKE_INSTALL_PREFIX /usr) 218 | endif () 219 | 220 | # install icon 221 | install ( 222 | FILES assets/icons/app-icon.svg 223 | DESTINATION "${CMAKE_INSTALL_DATADIR}/icons/hicolor/scalable/apps" 224 | RENAME net.blumia.pineapple-pictures.svg 225 | ) 226 | 227 | # install shortcut 228 | install ( 229 | FILES dist/net.blumia.pineapple-pictures.desktop 230 | DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/applications" 231 | ) 232 | 233 | # install app metadata file for appstream (and some other stuff using this metadata like snapcraft) 234 | install ( 235 | FILES dist/appstream/net.blumia.pineapple-pictures.metainfo.xml 236 | DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/metainfo" 237 | ) 238 | endif() 239 | 240 | set (INSTALL_TARGETS_DEFAULT_ARGS 241 | BUNDLE DESTINATION . 242 | RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} 243 | LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} 244 | ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT Devel 245 | ) 246 | 247 | install ( 248 | TARGETS ${EXE_NAME} 249 | ${INSTALL_TARGETS_DEFAULT_ARGS} 250 | ) 251 | 252 | if (TRANSLATION_RESOURCE_EMBEDDING) 253 | target_compile_definitions(${EXE_NAME} 254 | PRIVATE TRANSLATION_RESOURCE_EMBEDDING 255 | ) 256 | elseif (WIN32) 257 | set (QM_FILE_INSTALL_DIR "${CMAKE_INSTALL_BINDIR}/translations") 258 | else() 259 | set (QM_FILE_INSTALL_DIR "${CMAKE_INSTALL_FULL_DATADIR}/pineapple-pictures/translations") 260 | target_compile_definitions(${EXE_NAME} 261 | PRIVATE QM_FILE_INSTALL_ABSOLUTE_DIR=${QM_FILE_INSTALL_DIR} 262 | ) 263 | endif() 264 | 265 | if (DEFINED QM_FILE_INSTALL_DIR) 266 | install( 267 | FILES ${PPIC_QM_FILES} 268 | DESTINATION ${QM_FILE_INSTALL_DIR} 269 | ) 270 | endif() 271 | 272 | feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES) 273 | 274 | # CPACK: General Settings 275 | set (CPACK_GENERATOR "TBZ2") 276 | set (CPACK_PACKAGE_NAME "pineapple-pictures") 277 | set (CPACK_PACKAGE_DESCRIPTION_SUMMARY "Yet another image viewer") 278 | set (CPACK_PACKAGE_VENDOR "Gary Wang") 279 | set (CPACK_PACKAGE_CONTACT "https://github.com/BLumia/pineapple-pictures/issues/") 280 | if (WIN32) 281 | # ... 282 | elseif (APPLE) 283 | # ... 284 | elseif (UNIX) 285 | set (CPACK_SYSTEM_NAME "${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}") 286 | set (CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON) 287 | set (CPACK_DEBIAN_PACKAGE_RECOMMENDS "kimageformat-plugins") 288 | endif() 289 | 290 | include(CPack) 291 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 BLumia 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 | -------------------------------------------------------------------------------- /LICENSES/CC0-1.0.txt: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /LICENSES/MIT.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /NEWS: -------------------------------------------------------------------------------- 1 | Version 1.0.0 2 | ~~~~~~~~~~~~~ 3 | Released: 2025-05-03 4 | 5 | Features: 6 | * Support enforces windowed mode on start-up 7 | * Reload image automatically when current image gets updated 8 | 9 | Bugfixes: 10 | * Display correct text language on macOS 11 | 12 | Miscellaneous: 13 | * Use native text for shortcut editor's label 14 | * Display native commandline message when possible 15 | * Merge Qt translations into app applications as well 16 | 17 | Contributors: 18 | Heimen Stoffels, albanobattistella, mmahhi 19 | 20 | Version 0.9.2 21 | ~~~~~~~~~~~~~ 22 | Released: 2025-03-05 23 | 24 | Bugfixes: 25 | * Refer to the right exiv2 CMake module so it can be found on Linux 26 | 27 | Miscellaneous: 28 | * Convert DEP5 to REUSE.toml for better REUSE compliance 29 | * Update translations 30 | 31 | Contributors: 32 | Pino Toscano, TamilNeram 33 | 34 | Version 0.9.1 35 | ~~~~~~~~~~~~~ 36 | Released: 2025-01-25 37 | 38 | Features: 39 | * Option to double-click to fullscreen 40 | * Build-time option to embed translation resources 41 | 42 | Bugfixes: 43 | * Fix window size not adjusted when open file on macOS 44 | * Should center window according to available screen geometry 45 | 46 | Miscellaneous: 47 | * Change close window bahavior on macOS 48 | * Update translations 49 | 50 | Contributors: 51 | albanobattistella, Sabri Ünal 52 | 53 | Version 0.9.0 54 | ~~~~~~~~~~~~~ 55 | Released: 2024-12-08 56 | 57 | Features: 58 | * Support custom shortcuts for existing actions 59 | * Actions for frame-by-frame animated image playback support 60 | 61 | Miscellaneous: 62 | * Initial macOS bundle support 63 | * bump minimum required CMake version to 3.16 64 | * Update translations 65 | 66 | Contributors: 67 | albanobattistella, VenusGirl, gallegonovato, Sabri Ünal 68 | 69 | Version 0.8.2.1 70 | ~~~~~~~~~~~~~ 71 | Released: 2024-10-27 72 | 73 | Bugfixes: 74 | * Cannot load translations caused by a change in 0.8.2 75 | 76 | Version 0.8.2 77 | ~~~~~~~~~~~~~ 78 | Released: 2024-10-26 79 | 80 | Features: 81 | * New option to allow use light-color checkerboard by default 82 | 83 | Contributors: 84 | albanobattistella, mmahhi, gallegonovato 85 | 86 | Version 0.8.1 87 | ~~~~~~~~~~~~~ 88 | Released: 2024-08-25 89 | 90 | Features: 91 | * New command line option to list all supported formats 92 | 93 | Contributors: 94 | albanobattistella, mmahhi, ovl-1, gallegonovato, Oğuz Ersen 95 | 96 | Version 0.8.0 97 | ~~~~~~~~~~~~~ 98 | Released: 2024-06-29 99 | 100 | Features: 101 | * Support move image file to trash 102 | 103 | Contributors: 104 | albanobattistella, mmahhi, gallegonovato, Oğuz Ersen 105 | 106 | Version 0.7.4 107 | ~~~~~~~~~~~~~ 108 | Released: 2024-04-04 109 | 110 | Features: 111 | * Add some icons for corresponding menu actions 112 | 113 | Contributors: 114 | Reza Almanda, mmahhi, Oğuz Ersen, volkov, Сергій 115 | 116 | Version 0.7.3 117 | ~~~~~~~~~~~~~ 118 | Released: 2023-10-24 119 | 120 | Features: 121 | * Add "Keep transformation" to menu 122 | 123 | Contributors: 124 | mmahhi, VenusGirl, albanobattistella, gallegonovato, Heimen Stoffels 125 | 126 | Version 0.7.2 127 | ~~~~~~~~~~~~~ 128 | Released: 2023-08-27 129 | 130 | Features: 131 | * Add an option in setting dialog to tweak the High-DPI scaling rounding policy (might only works in Qt 6 build) 132 | 133 | Bugfixes: 134 | * Remove image size limit for Qt 6 build 135 | * Fix application icon install location under Linux 136 | 137 | Contributors: 138 | Heimen Stoffels, Andrey, Dan, gallegonovato, albanobattistella, Sabri Ünal 139 | 140 | Version 0.7.1 141 | ~~~~~~~~~~~~~ 142 | Released: 2023-07-08 143 | 144 | Features: 145 | * TIF and TIFF format files in the same folder will now be automatically added to the gallery 146 | * Built-in window resizing now also supports Linux desktop. (macOS might also works as well) 147 | 148 | Bugfixes: 149 | * Settings dialog will automatedly use a suitable size instead of a hard-coded one 150 | * Fix default configuration file location under Linux. (was `~/.config/config.ini`, now it's `~/.config/Pineapple Pictures/config.ini`) 151 | 152 | Contributors: 153 | yyc12345 154 | 155 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Yet another image viewer. 2 | 3 | |CI|Build Status| 4 | |---|---| 5 | |Windows Build|[![Windows CI](https://github.com/BLumia/pineapple-pictures/actions/workflows/windows.yml/badge.svg)](https://github.com/BLumia/pineapple-pictures/actions/workflows/windows.yml)| 6 | |macOS Build|[![macOS CI](https://github.com/BLumia/pineapple-pictures/actions/workflows/macos.yml/badge.svg)](https://github.com/BLumia/pineapple-pictures/actions/workflows/macos.yml)| 7 | |Ubuntu Build|[![Ubuntu CI](https://github.com/BLumia/pineapple-pictures/actions/workflows/ubuntu.yml/badge.svg)](https://github.com/BLumia/pineapple-pictures/actions/workflows/ubuntu.yml)| 8 | 9 | ![Pineapple Pictures - Main Window](https://repository-images.githubusercontent.com/211888654/e8697600-e370-11eb-9b2a-b71e05262954) 10 | 11 | ## Summary 12 | 13 | Pineapple Pictures is a lightweight image viewer that allows you view JPEG, PNG, GIF, SVG, PSD, KRA, XCF, TGA, HDR, AVIF and some other frequently used image formats files quickly and easily, and also provide a Stay-on-Top window setting that allows you pin the window so you can use it to pin a reference image at the top and then you can work with other software. 14 | 15 | ## Get it! 16 | 17 | ### Maintained by the original author 18 | 19 | - [GitHub Release Page](https://github.com/BLumia/pineapple-pictures/releases) 20 | - [SourceForge](https://sourceforge.net/projects/pineapple-pictures/) 21 | - Archlinux AUR: [pineapple-pictures](https://aur.archlinux.org/packages/pineapple-pictures/) | [pineapple-pictures-git](https://aur.archlinux.org/packages/pineapple-pictures-git/) 22 | - [Itch.io Store](https://blumia.itch.io/pineapple-pictures) 23 | 24 | ### Maintained by contributors / certain distro's package maintainers 25 | 26 | - Debian (since bullseye) or Ubuntu (since 21.04): `sudo apt install pineapple-pictures` 27 | - Nix / NixOS: [pineapple-pictures](https://search.nixos.org/packages?channel=unstable&show=pineapple-pictures&from=0&size=50&sort=relevance&type=packages&query=pineapple-pictures) (maintained by @wineee) 28 | 29 | ## Help Translation! 30 | 31 | [Translate this project on Weblate!](https://hosted.weblate.org/projects/pineapple-pictures/) 32 | 33 | ## Build it manually: 34 | 35 | Current state, we need: 36 | 37 | - `cmake`: as the build system. 38 | - `qt6` with `qt6-svg` and `qt6-tools`: since the app is using Qt. 39 | - `libexiv2`: able to display more image metadata. (optional, but recommended) 40 | 41 | Then we can build it with any proper c++ compiler like g++ or msvc. 42 | 43 | Building it just requires normal cmake building steps: 44 | 45 | ``` bash 46 | $ mkdir build && cd build 47 | $ cmake .. 48 | $ cmake --build . # or simply using `make` if you are using Makefile as the cmake generator. 49 | ``` 50 | 51 | After that, a `ppic` executable file will be available to use. You can also optionally install it by using the target `install` (or simply `make install` in case you are using Makefile). After the build process, you can also use `cpack` to make a package. 52 | 53 | The project will try to build with `exiv2` when it's available at build time, if you would like to build the project without `exiv2`, pass `-DEXIV2_METADATA_SUPPORT=OFF` to `cmake`. The project will also not use `exiv2` if it's not found, the `EXIV2_METADATA_SUPPORT` option can be useful if you have `exiv2` but specifically don't want to use it. 54 | 55 | Image formats supports rely on Qt's imageformats plugins, just get the plugins you need from your distro's package manager will be fine. For Windows user, you may need build and install the imageformats plugin manually, read the content below. 56 | 57 | It's possible to build it under Windows, Linux, macOS, and maybe other desktop platforms that Qt is ported to. For platform specific build instructions, please read the [related wiki page](https://github.com/BLumia/pineapple-pictures/wiki/Platform-Specific-Build-Instructions). 58 | 59 | > [!NOTE] 60 | > Although there is a `pineapple-pictures.pro` file which can be used for QMake build, it's only for testing purpose and it doesn't have `exiv2` support included. Using QMake to build this project is NOT supported, please use CMake if possible. 61 | 62 | ## License 63 | 64 | Pineapple Pictures as a whole is licensed under MIT license. Individual files may have a different, but compatible license. 65 | -------------------------------------------------------------------------------- /README.zh_CN.md: -------------------------------------------------------------------------------- 1 | 简单轻量的跨平台看图工具。 2 | 3 | |CI|构建状态| 4 | |---|---| 5 | |Windows Build|[![Windows CI](https://github.com/BLumia/pineapple-pictures/actions/workflows/windows.yml/badge.svg)](https://github.com/BLumia/pineapple-pictures/actions/workflows/windows.yml)| 6 | |macOS Build|[![macOS CI](https://github.com/BLumia/pineapple-pictures/actions/workflows/macos.yml/badge.svg)](https://github.com/BLumia/pineapple-pictures/actions/workflows/macos.yml)| 7 | |Ubuntu Build|[![Ubuntu CI](https://github.com/BLumia/pineapple-pictures/actions/workflows/ubuntu.yml/badge.svg)](https://github.com/BLumia/pineapple-pictures/actions/workflows/ubuntu.yml)| 8 | 9 | ![Pineapple Pictures - Main Window](https://repository-images.githubusercontent.com/211888654/e8697600-e370-11eb-9b2a-b71e05262954) 10 | 11 | ## 简介 12 | 13 | 菠萝看图是一个轻量图像查看器,允许你简单快捷的查看 JPEG, PNG, GIF, SVG, PSD, KRA, XCF, TGA, HDR, AVIF 等常用格式的图像文件,并提供了置顶窗口的选项以便你在使用其它软件时也可以将参考图片固定在顶端。 14 | 15 | ## 立即获取! 16 | 17 | ### 由原作者维护 18 | 19 | - [GitHub Release 页面](https://github.com/BLumia/pineapple-pictures/releases) | [gitee 发布页面](https://gitee.com/blumia/pineapple-pictures/releases) 20 | - [SourceForge](https://sourceforge.net/projects/pineapple-pictures/) 21 | - Archlinux AUR: [pineapple-pictures](https://aur.archlinux.org/packages/pineapple-pictures/) | [pineapple-pictures-git](https://aur.archlinux.org/packages/pineapple-pictures-git/) 22 | - [Itch.io 商店](https://blumia.itch.io/pineapple-pictures) 23 | - Flatpak (于 FlatHub): [net.blumia.pineapple-pictures](https://flathub.org/apps/net.blumia.pineapple-pictures) *([我应当使用 flatpak 版吗?](https://github.com/BLumia/pineapple-pictures/wiki/Container%E2%80%90based-Packaging-Solutions-Support))* 24 | 25 | ### 由贡献者/对应发行版的打包人员维护 26 | 27 | - Debian (自 bullseye 起) 或 Ubuntu (自 21.04 起): `sudo apt install pineapple-pictures` 28 | - Nix / NixOS: [pineapple-pictures](https://search.nixos.org/packages?channel=unstable&show=pineapple-pictures&from=0&size=50&sort=relevance&type=packages&query=pineapple-pictures) (由 [@wineee](https://github.com/wineee) 维护) 29 | 30 | ## 帮助翻译! 31 | 32 | [在 Weblate 上帮助此项目翻译到更多语言!](https://hosted.weblate.org/projects/pineapple-pictures/) 33 | 34 | ## 手动构建步骤: 35 | 36 | 当前状态,我们需要先确保如下依赖可用: 37 | 38 | - `cmake`: 我们所使用的构建系统 39 | - 包含 `qt6-svg` 与 `qt6-tools` 组件的 `qt6`: 此应用基于 Qt 40 | - `libexiv2`: 用以获取和显示更多的图像元信息(可选,推荐) 41 | 42 | 然后我们就可以使用任何常规的 c++ 编译器如 g++ 或 msvc 来进行构建了 43 | 44 | 构建过程就是常规的 CMake 应用构建过程: 45 | 46 | ``` bash 47 | $ mkdir build && cd build 48 | $ cmake .. 49 | $ cmake --build . # 如果你使用 Makefile 作为 CMake 生成器,也可以直接简单的使用 `make` 50 | ``` 51 | 52 | 完毕后,一个名为 `ppic` 的可执行程序即会被生成以供使用。您也可以选择通过使用 CMake 生成的 `install` 目标继续将其安装到您的设备上(假设您使用 Makefile,即可执行 `make install` 来进行安装)。构建步骤完毕后,您也可以使用 `cpack` 来对应用程序进行打包。 53 | 54 | 当 `exiv2` 在构建时可用时,此项目将尝试使用其进行构建,若您不希望使用 `exiv2`,请传递 `-DEXIV2_METADATA_SUPPORT=OFF` 参数给 `cmake`。此项目在找不到 `exiv2` 时并不会使用 `exiv2`,`EXIV2_METADATA_SUPPORT` 选项可供尽管存在可用的 `exiv2` 但您明确不希望启用其支持时使用。 55 | 56 | 此应用的图片格式支持依赖于 Qt 的 imageformats 插件,直接从您所用的发行版获取对应的图像格式插件即可。对于 Windows 用户,您可能需要手动构建和使用图像格式插件。下方给出了进一步的说明。 57 | 58 | 在 Windows、Linux 以及 macOS 系统均可构建此应用,其它有移植 Qt 支持的平台也可能可以进行构建。若要了解一些平台相关的构建指引,请参阅[相关的 Wiki 页面](https://github.com/BLumia/pineapple-pictures/wiki/Platform-Specific-Build-Instructions)。 59 | 60 | > [!NOTE] 61 | > 尽管存在一个可用于 QMake 构建的 `pineapple-pictures.pro` 文件,但其仅供简单测试所用且其并不包含 `exiv2` 支持。使用 QMake 构建此项目是 **不受支持** 的,请尽可能考虑使用 CMake。 62 | 63 | ## 许可协议 64 | 65 | 菠萝看图整体使用 MIT 协议进行发布。项目所随的部分源文件可能具备不同但与之兼容的许可协议。 66 | -------------------------------------------------------------------------------- /REUSE.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | SPDX-PackageName = "Pineapple Pictures" 3 | SPDX-PackageDownloadLocation = "https://github.com/BLumia/pineapple-pictures" 4 | 5 | [[annotations]] 6 | path = [".gitignore", "appveyor.yml", ".github/**"] 7 | precedence = "aggregate" 8 | SPDX-FileCopyrightText = "None" 9 | SPDX-License-Identifier = "CC0-1.0" 10 | 11 | [[annotations]] 12 | path = ["README**.md", "NEWS", "assets/**.rc", "assets/**.qrc", "dist/**"] 13 | precedence = "aggregate" 14 | SPDX-FileCopyrightText = "None" 15 | SPDX-License-Identifier = "CC0-1.0" 16 | 17 | [[annotations]] 18 | path = ["app/translations/**.ts", "assets/plain/translators.html"] 19 | precedence = "aggregate" 20 | SPDX-FileCopyrightText = "Translators from hosted.weblate.org" 21 | SPDX-License-Identifier = "MIT" 22 | 23 | [[annotations]] 24 | path = "assets/icons/**.svg" 25 | precedence = "aggregate" 26 | SPDX-FileCopyrightText = "2022 Gary Wang" 27 | SPDX-License-Identifier = "MIT" 28 | 29 | [[annotations]] 30 | path = "assets/icons/app-icon.**" 31 | precedence = "aggregate" 32 | SPDX-FileCopyrightText = "2020 Lovelyblack" 33 | SPDX-License-Identifier = "MIT" 34 | -------------------------------------------------------------------------------- /app/aboutdialog.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Gary Wang 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | #include "aboutdialog.h" 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | using namespace Qt::Literals::StringLiterals; 18 | 19 | AboutDialog::AboutDialog(QWidget *parent) 20 | : QDialog(parent) 21 | , m_tabWidget(new QTabWidget) 22 | , m_buttonBox(new QDialogButtonBox) 23 | , m_helpTextEdit(new QTextBrowser) 24 | , m_aboutTextEdit(new QTextBrowser) 25 | , m_specialThanksTextEdit(new QTextBrowser) 26 | , m_licenseTextEdit(new QTextBrowser) 27 | , m_3rdPartyLibsTextEdit(new QTextBrowser) 28 | { 29 | this->setWindowTitle(tr("About")); 30 | 31 | const QStringList helpStr { 32 | u"

%1

"_s.arg(tr("Launch application with image file path as argument to load the file.")), 33 | u"

%1

"_s.arg(tr("Drag and drop image file onto the window is also supported.")), 34 | u"

%1

"_s.arg(tr("None of the operations in this application will alter the pictures on disk.")), 35 | u"

%1

"_s.arg(tr("Context menu option explanation:")), 36 | u"
    "_s, 37 | // blumia: Chain two arg() here since it seems lupdate will remove one of them if we use 38 | // the old `arg(QCoreApp::translate(), tr())` way, but it's worth to mention 39 | // `arg(QCoreApp::translate(), this->tr())` works, but lupdate will complain about the usage. 40 | u"
  • %1:
    %2
  • "_s 41 | .arg(QCoreApplication::translate("MainWindow", "Stay on top")) 42 | .arg(tr("Make window stay on top of all other windows.")), 43 | u"
  • %1:
    %2
  • "_s 44 | .arg(QCoreApplication::translate("MainWindow", "Protected mode")) 45 | .arg(tr("Avoid close window accidentally. (eg. by double clicking the window)")), 46 | u"
  • %1:
    %2
  • "_s 47 | .arg(QCoreApplication::translate("MainWindow", "Keep transformation", "The 'transformation' means the flip/rotation status that currently applied to the image view")) 48 | .arg(tr("Avoid resetting the zoom/rotation/flip state that was applied to the image view when switching between images.")), 49 | u"
"_s 50 | }; 51 | 52 | const QStringList aboutStr { 53 | u"

"_s, 54 | qApp->applicationDisplayName(), 55 | (u"
"_s + tr("Version: %1").arg( 56 | #ifdef GIT_DESCRIBE_VERSION_STRING 57 | GIT_DESCRIBE_VERSION_STRING 58 | #else 59 | qApp->applicationVersion() 60 | #endif // GIT_DESCRIBE_VERSION_STRING 61 | )), 62 | u"
"_s, 63 | tr("Copyright (c) %1 %2", "%1 is year, %2 is the name of copyright holder(s)") 64 | .arg(u"2025"_s, u"@BLumia"_s), 65 | u"
"_s, 66 | tr("Logo designed by %1").arg(u"@Lovelyblack"_s), 67 | u"
"_s, 68 | tr("Built with Qt %1 (%2)").arg(QT_VERSION_STR, QSysInfo::buildCpuArchitecture()), 69 | QStringLiteral("
%2").arg("https://github.com/BLumia/pineapple-pictures", tr("Source code")), 70 | u"
"_s 71 | }; 72 | 73 | QFile translaterHtml(u":/plain/translators.html"_s); 74 | bool canOpenFile = translaterHtml.open(QIODevice::ReadOnly); 75 | const QByteArray & translatorList = canOpenFile ? translaterHtml.readAll() : QByteArrayLiteral(""); 76 | 77 | const QStringList specialThanksStr { 78 | u"

%1

%3

%4

"_s.arg( 79 | tr("Contributors"), 80 | u"https://github.com/BLumia/pineapple-pictures/graphs/contributors"_s, 81 | tr("List of contributors on GitHub"), 82 | tr("Thanks to all people who contributed to this project.") 83 | ), 84 | 85 | u"

%1

%2

%3"_s.arg( 86 | tr("Translators"), 87 | tr("I would like to thank the following people who volunteered to translate this application."), 88 | translatorList 89 | ) 90 | }; 91 | 92 | const QStringList licenseStr { 93 | u"

%1

"_s.arg(tr("Your Rights")), 94 | u"

%1

%2

  • %3
  • %4
  • %5
  • %6
"_s.arg( 95 | tr("%1 is released under the MIT License."), // %1 96 | tr("This license grants people a number of freedoms:"), // %2 97 | tr("You are free to use %1, for any purpose"), // %3 98 | tr("You are free to distribute %1"), // %4 99 | tr("You can study how %1 works and change it"), // %5 100 | tr("You can distribute changed versions of %1") // %6 101 | ).arg(u"%1"_s), 102 | u"

%1

"_s.arg(tr("The MIT license guarantees you this freedom. Nobody is ever permitted to take it away.")), 103 | u"
%2
"_s 104 | }; 105 | 106 | const QString mitLicense(QStringLiteral(R"(Expat/MIT License 107 | 108 | Copyright (c) 2025 BLumia 109 | 110 | Permission is hereby granted, free of charge, to any person obtaining a copy 111 | of this software and associated documentation files (the "Software"), to deal 112 | in the Software without restriction, including without limitation the rights 113 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 114 | copies of the Software, and to permit persons to whom the Software is 115 | furnished to do so, subject to the following conditions: 116 | 117 | The above copyright notice and this permission notice shall be included in all 118 | copies or substantial portions of the Software. 119 | 120 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 121 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 122 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 123 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 124 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 125 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 126 | SOFTWARE. 127 | )")); 128 | 129 | const QStringList thirdPartyLibsStr { 130 | u"

%1

"_s.arg(tr("Third-party Libraries used by %1")), 131 | tr("%1 is built on the following free software libraries:", "Free as in freedom"), 132 | u"
    "_s, 133 | #ifdef HAVE_EXIV2_VERSION 134 | u"
  • %2: %3
  • "_s.arg("https://www.exiv2.org/", "Exiv2", "GPLv2"), 135 | #endif // EXIV2_VERSION 136 | u"
  • %2: %3
  • "_s.arg("https://www.qt.io/", "Qt", "GPLv2 + GPLv3 + LGPLv2.1 + LGPLv3"), 137 | u"
"_s 138 | }; 139 | 140 | m_helpTextEdit->setText(helpStr.join('\n')); 141 | 142 | m_aboutTextEdit->setText(aboutStr.join('\n')); 143 | m_aboutTextEdit->setOpenExternalLinks(true); 144 | 145 | m_specialThanksTextEdit->setText(specialThanksStr.join('\n')); 146 | m_specialThanksTextEdit->setOpenExternalLinks(true); 147 | 148 | m_licenseTextEdit->setText(licenseStr.join('\n').arg(qApp->applicationDisplayName(), mitLicense)); 149 | 150 | m_3rdPartyLibsTextEdit->setText(thirdPartyLibsStr.join('\n').arg(u"%1"_s).arg(qApp->applicationDisplayName())); 151 | m_3rdPartyLibsTextEdit->setOpenExternalLinks(true); 152 | 153 | m_tabWidget->addTab(m_helpTextEdit, tr("&Help")); 154 | m_tabWidget->addTab(m_aboutTextEdit, tr("&About")); 155 | m_tabWidget->addTab(m_specialThanksTextEdit, tr("&Special Thanks")); 156 | m_tabWidget->addTab(m_licenseTextEdit, tr("&License")); 157 | m_tabWidget->addTab(m_3rdPartyLibsTextEdit, tr("&Third-party Libraries")); 158 | 159 | m_buttonBox->setStandardButtons(QDialogButtonBox::Close); 160 | connect(m_buttonBox, QOverload::of(&QDialogButtonBox::clicked), this, [this](){ 161 | this->close(); 162 | }); 163 | 164 | setLayout(new QVBoxLayout); 165 | 166 | layout()->addWidget(m_tabWidget); 167 | layout()->addWidget(m_buttonBox); 168 | 169 | setMinimumSize(361, 161); // not sure why it complain "Unable to set geometry" 170 | setWindowFlag(Qt::WindowContextHelpButtonHint, false); 171 | } 172 | 173 | AboutDialog::~AboutDialog() 174 | { 175 | 176 | } 177 | 178 | QSize AboutDialog::sizeHint() const 179 | { 180 | return QSize(520, 350); 181 | } 182 | -------------------------------------------------------------------------------- /app/aboutdialog.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Gary Wang 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | #ifndef ABOUTDIALOG_H 6 | #define ABOUTDIALOG_H 7 | 8 | #include 9 | 10 | QT_BEGIN_NAMESPACE 11 | class QTextBrowser; 12 | class QTabWidget; 13 | class QDialogButtonBox; 14 | QT_END_NAMESPACE 15 | 16 | class AboutDialog : public QDialog 17 | { 18 | Q_OBJECT 19 | public: 20 | explicit AboutDialog(QWidget *parent = nullptr); 21 | ~AboutDialog() override; 22 | 23 | QSize sizeHint() const override; 24 | 25 | private: 26 | QTabWidget * m_tabWidget = nullptr; 27 | QDialogButtonBox * m_buttonBox = nullptr; 28 | 29 | QTextBrowser * m_helpTextEdit = nullptr; 30 | QTextBrowser * m_aboutTextEdit = nullptr; 31 | QTextBrowser * m_specialThanksTextEdit = nullptr; 32 | QTextBrowser * m_licenseTextEdit = nullptr; 33 | QTextBrowser * m_3rdPartyLibsTextEdit = nullptr; 34 | }; 35 | 36 | #endif // ABOUTDIALOG_H 37 | -------------------------------------------------------------------------------- /app/actionmanager.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Gary Wang 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | #include "actionmanager.h" 6 | 7 | #include "mainwindow.h" 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | #define ICON_NAME(name)\ 14 | QStringLiteral(":/icons/" #name ".svg") 15 | 16 | #define ACTION_NAME(s) QStringLiteral(STRIFY(s)) 17 | #define STRIFY(s) #s 18 | 19 | ActionManager::ActionManager() 20 | { 21 | 22 | } 23 | 24 | ActionManager::~ActionManager() 25 | { 26 | 27 | } 28 | 29 | QIcon ActionManager::loadHidpiIcon(const QString &resp, QSize sz) 30 | { 31 | QSvgRenderer r(resp); 32 | QPixmap pm = QPixmap(sz * qApp->devicePixelRatio()); 33 | pm.fill(Qt::transparent); 34 | QPainter p(&pm); 35 | r.render(&p); 36 | pm.setDevicePixelRatio(qApp->devicePixelRatio()); 37 | return QIcon(pm); 38 | } 39 | 40 | void ActionManager::setupAction(MainWindow *mainWindow) 41 | { 42 | auto create_action = [] (QWidget *w, QAction **a, QString i, QString an, bool iconFromTheme = false) { 43 | *a = new QAction(w); 44 | if (!i.isNull()) 45 | (*a)->setIcon(iconFromTheme ? QIcon::fromTheme(i) : ActionManager::loadHidpiIcon(i)); 46 | (*a)->setObjectName(an); 47 | w->addAction(*a); 48 | }; 49 | #define CREATE_NEW_ICON_ACTION(w, a, i) create_action(w, &a, ICON_NAME(i), ACTION_NAME(a)) 50 | CREATE_NEW_ICON_ACTION(mainWindow, actionActualSize, zoom-original); 51 | CREATE_NEW_ICON_ACTION(mainWindow, actionToggleMaximize, view-fullscreen); 52 | CREATE_NEW_ICON_ACTION(mainWindow, actionZoomIn, zoom-in); 53 | CREATE_NEW_ICON_ACTION(mainWindow, actionZoomOut, zoom-out); 54 | CREATE_NEW_ICON_ACTION(mainWindow, actionToggleCheckerboard, view-background-checkerboard); 55 | CREATE_NEW_ICON_ACTION(mainWindow, actionRotateClockwise, object-rotate-right); 56 | #undef CREATE_NEW_ICON_ACTION 57 | 58 | #define CREATE_NEW_ACTION(w, a) create_action(w, &a, QString(), ACTION_NAME(a)) 59 | #define CREATE_NEW_THEMEICON_ACTION(w, a, i) create_action(w, &a, QLatin1String(STRIFY(i)), ACTION_NAME(a), true) 60 | CREATE_NEW_ACTION(mainWindow, actionRotateCounterClockwise); 61 | CREATE_NEW_ACTION(mainWindow, actionPrevPicture); 62 | CREATE_NEW_ACTION(mainWindow, actionNextPicture); 63 | 64 | CREATE_NEW_ACTION(mainWindow, actionTogglePauseAnimation); 65 | CREATE_NEW_ACTION(mainWindow, actionAnimationNextFrame); 66 | 67 | CREATE_NEW_THEMEICON_ACTION(mainWindow, actionOpen, document-open); 68 | CREATE_NEW_ACTION(mainWindow, actionHorizontalFlip); 69 | CREATE_NEW_ACTION(mainWindow, actionFitInView); 70 | CREATE_NEW_ACTION(mainWindow, actionFitByWidth); 71 | CREATE_NEW_THEMEICON_ACTION(mainWindow, actionCopyPixmap, edit-copy); 72 | CREATE_NEW_ACTION(mainWindow, actionCopyFilePath); 73 | CREATE_NEW_THEMEICON_ACTION(mainWindow, actionPaste, edit-paste); 74 | CREATE_NEW_THEMEICON_ACTION(mainWindow, actionTrash, edit-delete); 75 | CREATE_NEW_ACTION(mainWindow, actionToggleStayOnTop); 76 | CREATE_NEW_ACTION(mainWindow, actionToggleProtectMode); 77 | CREATE_NEW_ACTION(mainWindow, actionToggleAvoidResetTransform); 78 | CREATE_NEW_ACTION(mainWindow, actionSettings); 79 | CREATE_NEW_THEMEICON_ACTION(mainWindow, actionHelp, system-help); 80 | CREATE_NEW_THEMEICON_ACTION(mainWindow, actionLocateInFileManager, system-file-manager); 81 | CREATE_NEW_THEMEICON_ACTION(mainWindow, actionProperties, document-properties); 82 | CREATE_NEW_ACTION(mainWindow, actionQuitApp); 83 | #undef CREATE_NEW_ACTION 84 | #undef CREATE_NEW_THEMEICON_ACTION 85 | 86 | retranslateUi(mainWindow); 87 | 88 | QMetaObject::connectSlotsByName(mainWindow); 89 | } 90 | 91 | void ActionManager::retranslateUi(MainWindow *mainWindow) 92 | { 93 | Q_UNUSED(mainWindow); 94 | 95 | actionOpen->setText(QCoreApplication::translate("MainWindow", "&Open...", nullptr)); 96 | 97 | actionActualSize->setText(QCoreApplication::translate("MainWindow", "Actual size", nullptr)); 98 | actionToggleMaximize->setText(QCoreApplication::translate("MainWindow", "Toggle maximize", nullptr)); 99 | actionZoomIn->setText(QCoreApplication::translate("MainWindow", "Zoom in", nullptr)); 100 | actionZoomOut->setText(QCoreApplication::translate("MainWindow", "Zoom out", nullptr)); 101 | actionToggleCheckerboard->setText(QCoreApplication::translate("MainWindow", "Toggle Checkerboard", nullptr)); 102 | actionRotateClockwise->setText(QCoreApplication::translate("MainWindow", "Rotate right", nullptr)); 103 | actionRotateCounterClockwise->setText(QCoreApplication::translate("MainWindow", "Rotate left", nullptr)); 104 | 105 | actionPrevPicture->setText(QCoreApplication::translate("MainWindow", "Previous image", nullptr)); 106 | actionNextPicture->setText(QCoreApplication::translate("MainWindow", "Next image", nullptr)); 107 | 108 | actionTogglePauseAnimation->setText(QCoreApplication::translate("MainWindow", "Pause/Resume Animation", nullptr)); 109 | actionAnimationNextFrame->setText(QCoreApplication::translate("MainWindow", "Animation Go to Next Frame", nullptr)); 110 | 111 | actionHorizontalFlip->setText(QCoreApplication::translate("MainWindow", "Flip &Horizontally", nullptr)); 112 | actionFitInView->setText(QCoreApplication::translate("MainWindow", "Fit to view", nullptr)); 113 | actionFitByWidth->setText(QCoreApplication::translate("MainWindow", "Fit to width", nullptr)); 114 | actionCopyPixmap->setText(QCoreApplication::translate("MainWindow", "Copy P&ixmap", nullptr)); 115 | actionCopyFilePath->setText(QCoreApplication::translate("MainWindow", "Copy &File Path", nullptr)); 116 | actionPaste->setText(QCoreApplication::translate("MainWindow", "&Paste", nullptr)); 117 | actionTrash->setText(QCoreApplication::translate("MainWindow", "Move to Trash", nullptr)); 118 | actionToggleStayOnTop->setText(QCoreApplication::translate("MainWindow", "Stay on top", nullptr)); 119 | actionToggleProtectMode->setText(QCoreApplication::translate("MainWindow", "Protected mode", nullptr)); 120 | actionToggleAvoidResetTransform->setText(QCoreApplication::translate("MainWindow", "Keep transformation", "The 'transformation' means the flip/rotation status that currently applied to the image view")); 121 | actionSettings->setText(QCoreApplication::translate("MainWindow", "Configure...", nullptr)); 122 | actionHelp->setText(QCoreApplication::translate("MainWindow", "Help", nullptr)); 123 | #ifdef Q_OS_WIN 124 | actionLocateInFileManager->setText( 125 | QCoreApplication::translate( 126 | "MainWindow", "Show in File Explorer", 127 | "File Explorer is the name of explorer.exe under Windows" 128 | ) 129 | ); 130 | #else 131 | actionLocateInFileManager->setText(QCoreApplication::translate("MainWindow", "Show in directory", nullptr)); 132 | #endif // Q_OS_WIN 133 | actionProperties->setText(QCoreApplication::translate("MainWindow", "Properties", nullptr)); 134 | actionQuitApp->setText(QCoreApplication::translate("MainWindow", "Quit", nullptr)); 135 | } 136 | 137 | void ActionManager::setupShortcuts() 138 | { 139 | actionOpen->setShortcut(QKeySequence::Open); 140 | actionActualSize->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_0)); 141 | actionZoomIn->setShortcut(QKeySequence::ZoomIn); 142 | actionZoomOut->setShortcut(QKeySequence::ZoomOut); 143 | actionPrevPicture->setShortcuts({ 144 | QKeySequence(Qt::Key_PageUp), 145 | QKeySequence(Qt::Key_Left), 146 | }); 147 | actionNextPicture->setShortcuts({ 148 | QKeySequence(Qt::Key_PageDown), 149 | QKeySequence(Qt::Key_Right), 150 | }); 151 | actionHorizontalFlip->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_R)); 152 | actionCopyPixmap->setShortcut(QKeySequence::Copy); 153 | actionPaste->setShortcut(QKeySequence::Paste); 154 | actionTrash->setShortcut(QKeySequence::Delete); 155 | actionHelp->setShortcut(QKeySequence::HelpContents); 156 | actionSettings->setShortcut(QKeySequence::Preferences); 157 | actionProperties->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_I)); 158 | actionQuitApp->setShortcuts({ 159 | QKeySequence(Qt::Key_Space), 160 | QKeySequence(Qt::Key_Escape) 161 | }); 162 | } 163 | 164 | -------------------------------------------------------------------------------- /app/actionmanager.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Gary Wang 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | #ifndef ACTIONMANAGER_H 6 | #define ACTIONMANAGER_H 7 | 8 | #include 9 | 10 | class MainWindow; 11 | 12 | class ActionManager 13 | { 14 | public: 15 | ActionManager(); 16 | ~ActionManager(); 17 | 18 | void setupAction(MainWindow * mainWindow); 19 | void retranslateUi(MainWindow *MainWindow); 20 | void setupShortcuts(); 21 | 22 | static QIcon loadHidpiIcon(const QString &resp, QSize sz = QSize(32, 32)); 23 | 24 | public: 25 | QAction *actionOpen; 26 | 27 | QAction *actionActualSize; 28 | QAction *actionToggleMaximize; 29 | QAction *actionZoomIn; 30 | QAction *actionZoomOut; 31 | QAction *actionToggleCheckerboard; 32 | QAction *actionRotateClockwise; 33 | QAction *actionRotateCounterClockwise; 34 | 35 | QAction *actionPrevPicture; 36 | QAction *actionNextPicture; 37 | 38 | QAction *actionTogglePauseAnimation; 39 | QAction *actionAnimationNextFrame; 40 | 41 | QAction *actionHorizontalFlip; 42 | QAction *actionFitInView; 43 | QAction *actionFitByWidth; 44 | QAction *actionCopyPixmap; 45 | QAction *actionCopyFilePath; 46 | QAction *actionPaste; 47 | QAction *actionTrash; 48 | QAction *actionToggleStayOnTop; 49 | QAction *actionToggleProtectMode; 50 | QAction *actionToggleAvoidResetTransform; 51 | QAction *actionSettings; 52 | QAction *actionHelp; 53 | QAction *actionLocateInFileManager; 54 | QAction *actionProperties; 55 | QAction *actionQuitApp; 56 | }; 57 | 58 | #endif // ACTIONMANAGER_H 59 | -------------------------------------------------------------------------------- /app/bottombuttongroup.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Gary Wang 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | #include "bottombuttongroup.h" 6 | 7 | #include "opacityhelper.h" 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | BottomButtonGroup::BottomButtonGroup(const std::vector &actionList, QWidget *parent) 14 | : QGroupBox (parent) 15 | , m_opacityHelper(new OpacityHelper(this)) 16 | { 17 | QHBoxLayout * mainLayout = new QHBoxLayout(this); 18 | mainLayout->setSizeConstraint(QLayout::SetFixedSize); 19 | this->setLayout(mainLayout); 20 | this->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); 21 | this->setStyleSheet("BottomButtonGroup {" 22 | "border: 1px solid gray;" 23 | "border-top-left-radius: 10px;" 24 | "border-top-right-radius: 10px;" 25 | "border-style: none;" 26 | "background-color:rgba(0,0,0,120)" 27 | "}" 28 | "QToolButton {" 29 | "background:transparent;" 30 | "}" 31 | "QToolButton:!focus {" 32 | "border-style: none;" 33 | "}"); 34 | 35 | auto newActionBtn = [this](QAction * action) -> QToolButton * { 36 | QToolButton * btn = new QToolButton(this); 37 | btn->setDefaultAction(action); 38 | btn->setIconSize(QSize(32, 32)); 39 | btn->setFixedSize(40, 40); 40 | return btn; 41 | }; 42 | 43 | for (QAction * action : actionList) { 44 | addButton(newActionBtn(action)); 45 | } 46 | } 47 | 48 | void BottomButtonGroup::setOpacity(qreal opacity, bool animated) 49 | { 50 | m_opacityHelper->setOpacity(opacity, animated); 51 | } 52 | 53 | void BottomButtonGroup::addButton(QAbstractButton *button) 54 | { 55 | layout()->addWidget(button); 56 | updateGeometry(); 57 | } 58 | -------------------------------------------------------------------------------- /app/bottombuttongroup.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Gary Wang 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | #ifndef BOTTOMBUTTONGROUP_H 6 | #define BOTTOMBUTTONGROUP_H 7 | 8 | #include 9 | 10 | #include 11 | #include 12 | 13 | class OpacityHelper; 14 | class BottomButtonGroup : public QGroupBox 15 | { 16 | Q_OBJECT 17 | public: 18 | explicit BottomButtonGroup(const std::vector & actionList, QWidget *parent = nullptr); 19 | 20 | void setOpacity(qreal opacity, bool animated = true); 21 | void addButton(QAbstractButton *button); 22 | 23 | private: 24 | OpacityHelper * m_opacityHelper; 25 | }; 26 | 27 | #endif // BOTTOMBUTTONGROUP_H 28 | -------------------------------------------------------------------------------- /app/exiv2wrapper.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Gary Wang 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | #include "exiv2wrapper.h" 6 | 7 | #ifdef HAVE_EXIV2_VERSION 8 | #include 9 | #else // HAVE_EXIV2_VERSION 10 | namespace Exiv2 { 11 | class Image {}; 12 | } 13 | #endif // HAVE_EXIV2_VERSION 14 | 15 | #include 16 | 17 | #include 18 | #include 19 | 20 | Exiv2Wrapper::Exiv2Wrapper() 21 | { 22 | 23 | } 24 | 25 | Exiv2Wrapper::~Exiv2Wrapper() 26 | { 27 | 28 | } 29 | 30 | #ifdef HAVE_EXIV2_VERSION // stupid AppleClang... 31 | template 32 | void Exiv2Wrapper::cacheSection(Collection collection) 33 | { 34 | const Collection& exifData = collection; 35 | Iterator it = exifData.begin(), end = exifData.end(); 36 | for (; it != end; ++it) { 37 | QString key = QString::fromUtf8(it->key().c_str()); 38 | if (it->tagName().substr(0, 2) == "0x") continue; 39 | // We might get exceptions like "No namespace info available for XMP prefix `Item'" 40 | // when trying to get tagLabel() data from a Xmpdatum if the tag is not common-used. 41 | // We don't care for those rare tags so let's just use a try-cache... 42 | try { 43 | QString label = QString::fromLocal8Bit(it->tagLabel().c_str()); 44 | std::ostringstream stream; 45 | stream << *it; 46 | QString value = QString::fromUtf8(stream.str().c_str()); 47 | m_metadataValue.insert(key, value); 48 | m_metadataLabel.insert(key, label); 49 | qDebug() << key << label << value; 50 | #if EXIV2_TEST_VERSION(0, 28, 0) 51 | } catch (Exiv2::Error & err) { 52 | #else // 0.27.x 53 | } catch (Exiv2::AnyError & err) { 54 | #endif // EXIV2_TEST_VERSION(0, 28, 0) 55 | qWarning() << "Error loading key" << key << ":" << err.what(); 56 | } 57 | } 58 | } 59 | #endif // HAVE_EXIV2_VERSION 60 | 61 | bool Exiv2Wrapper::load(const QString &filePath) 62 | { 63 | #ifdef HAVE_EXIV2_VERSION 64 | QByteArray filePathByteArray = QFile::encodeName(filePath); 65 | try { 66 | m_exivImage.reset(Exiv2::ImageFactory::open(filePathByteArray.constData()).release()); 67 | m_exivImage->readMetadata(); 68 | } catch (const Exiv2::Error& error) { 69 | m_errMsg = QString::fromUtf8(error.what()); 70 | return false; 71 | } 72 | return true; 73 | #else // HAVE_EXIV2_VERSION 74 | Q_UNUSED(filePath); 75 | return false; 76 | #endif // HAVE_EXIV2_VERSION 77 | } 78 | 79 | void Exiv2Wrapper::cacheSections() 80 | { 81 | #ifdef HAVE_EXIV2_VERSION 82 | if (m_exivImage->checkMode(Exiv2::mdExif) & Exiv2::amRead) { 83 | cacheSection(m_exivImage->exifData()); 84 | } 85 | 86 | if (m_exivImage->checkMode(Exiv2::mdIptc) & Exiv2::amRead) { 87 | cacheSection(m_exivImage->iptcData()); 88 | } 89 | 90 | if (m_exivImage->checkMode(Exiv2::mdXmp) & Exiv2::amRead) { 91 | cacheSection(m_exivImage->xmpData()); 92 | } 93 | 94 | // qDebug() << m_metadataValue; 95 | // qDebug() << m_metadataLabel; 96 | #endif // HAVE_EXIV2_VERSION 97 | } 98 | 99 | QString Exiv2Wrapper::comment() const 100 | { 101 | #ifdef HAVE_EXIV2_VERSION 102 | return m_exivImage->comment().c_str(); 103 | #else // HAVE_EXIV2_VERSION 104 | return QString(); 105 | #endif // HAVE_EXIV2_VERSION 106 | } 107 | 108 | QString Exiv2Wrapper::label(const QString &key) const 109 | { 110 | return m_metadataLabel.value(key); 111 | } 112 | 113 | QString Exiv2Wrapper::value(const QString &key) const 114 | { 115 | return m_metadataValue.value(key); 116 | } 117 | 118 | QString Exiv2Wrapper::XmpValue(const QString &rawValue) 119 | { 120 | QString ignored; 121 | return Exiv2Wrapper::XmpValue(rawValue, ignored); 122 | } 123 | 124 | QString Exiv2Wrapper::XmpValue(const QString &rawValue, QString &language) 125 | { 126 | if (rawValue.size() > 6 && rawValue.startsWith(QLatin1String("lang=\""))) { 127 | int pos = rawValue.indexOf('"', 6); 128 | 129 | if (pos != -1) { 130 | language = rawValue.mid(6, pos - 6); 131 | return (rawValue.mid(pos + 2)); 132 | } 133 | } 134 | 135 | language.clear(); 136 | return rawValue; 137 | } 138 | 139 | -------------------------------------------------------------------------------- /app/exiv2wrapper.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Gary Wang 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | #ifndef EXIV2WRAPPER_H 6 | #define EXIV2WRAPPER_H 7 | 8 | #include 9 | 10 | #include 11 | #include 12 | 13 | namespace Exiv2 { 14 | class Image; 15 | } 16 | 17 | class Exiv2Wrapper 18 | { 19 | public: 20 | Exiv2Wrapper(); 21 | ~Exiv2Wrapper(); 22 | 23 | bool load(const QString& filePath); 24 | void cacheSections(); 25 | 26 | QString comment() const; 27 | QString label(const QString & key) const; 28 | QString value(const QString & key) const; 29 | 30 | static QString XmpValue(const QString &rawValue); 31 | static QString XmpValue(const QString &rawValue, QString & language); 32 | 33 | private: 34 | std::unique_ptr m_exivImage; 35 | QMap m_metadataValue; 36 | QMap m_metadataLabel; 37 | QString m_errMsg; 38 | 39 | template 40 | void cacheSection(Collection collection); 41 | }; 42 | 43 | #endif // EXIV2WRAPPER_H 44 | -------------------------------------------------------------------------------- /app/fileopeneventhandler.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 Gary Wang 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | #include "fileopeneventhandler.h" 6 | 7 | #include 8 | 9 | FileOpenEventHandler::FileOpenEventHandler(QObject *parent) 10 | : QObject(parent) 11 | { 12 | } 13 | 14 | bool FileOpenEventHandler::eventFilter(QObject *obj, QEvent *event) 15 | { 16 | if (event->type() == QEvent::FileOpen) { 17 | QFileOpenEvent *fileOpenEvent = static_cast(event); 18 | emit fileOpen(fileOpenEvent->url()); 19 | return true; 20 | } 21 | return QObject::eventFilter(obj, event); 22 | } 23 | -------------------------------------------------------------------------------- /app/fileopeneventhandler.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 Gary Wang 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | #pragma once 6 | 7 | #include 8 | 9 | class FileOpenEventHandler : public QObject 10 | { 11 | Q_OBJECT 12 | 13 | public: 14 | explicit FileOpenEventHandler(QObject *parent = nullptr); 15 | 16 | protected: 17 | bool eventFilter(QObject *obj, QEvent *event) override; 18 | 19 | signals: 20 | void fileOpen(const QUrl &url); 21 | }; 22 | -------------------------------------------------------------------------------- /app/framelesswindow.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Gary Wang 2 | // SPDX-FileCopyrightText: 2023 Tad Young 3 | // 4 | // SPDX-License-Identifier: MIT 5 | 6 | #include "framelesswindow.h" 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | FramelessWindow::FramelessWindow(QWidget *parent) 15 | : QWidget(parent) 16 | , m_centralLayout(new QVBoxLayout(this)) 17 | , m_oldCursorShape(Qt::ArrowCursor) 18 | , m_oldEdges() 19 | { 20 | this->setWindowFlags(Qt::Window | Qt::FramelessWindowHint | Qt::WindowMinMaxButtonsHint); 21 | this->setMouseTracking(true); 22 | this->setAttribute(Qt::WA_Hover, true); 23 | this->installEventFilter(this); 24 | 25 | m_centralLayout->setContentsMargins(QMargins()); 26 | } 27 | 28 | void FramelessWindow::setCentralWidget(QWidget *widget) 29 | { 30 | if (m_centralWidget) { 31 | m_centralLayout->removeWidget(m_centralWidget); 32 | m_centralWidget->deleteLater(); 33 | } 34 | 35 | m_centralLayout->addWidget(widget); 36 | m_centralWidget = widget; 37 | } 38 | 39 | void FramelessWindow::installResizeCapture(QObject* widget) 40 | { 41 | widget->installEventFilter(this); 42 | } 43 | 44 | bool FramelessWindow::eventFilter(QObject* o, QEvent* e) 45 | { 46 | switch (e->type()) { 47 | case QEvent::HoverMove: 48 | { 49 | QWidget* wg = qobject_cast(o); 50 | if (wg != nullptr) 51 | return mouseHover(static_cast(e), wg); 52 | 53 | break; 54 | } 55 | case QEvent::MouseButtonPress: 56 | return mousePress(static_cast(e)); 57 | } 58 | 59 | return QWidget::eventFilter(o, e); 60 | } 61 | 62 | bool FramelessWindow::mouseHover(QHoverEvent* event, QWidget* wg) 63 | { 64 | if (!isMaximized() && !isFullScreen()) { 65 | QWindow* win = window()->windowHandle(); 66 | Qt::Edges edges = this->getEdgesByPos(wg->mapToGlobal(event->oldPos()), win->frameGeometry()); 67 | 68 | // backup & restore cursor shape 69 | if (edges && !m_oldEdges) 70 | // entering the edge. backup cursor shape 71 | m_oldCursorShape = win->cursor().shape(); 72 | if (!edges && m_oldEdges) 73 | // leaving the edge. restore cursor shape 74 | win->setCursor(m_oldCursorShape); 75 | 76 | // save the latest edges status 77 | m_oldEdges = edges; 78 | 79 | // show resize cursor shape if cursor is within border 80 | if (edges) { 81 | win->setCursor(this->getCursorByEdge(edges, Qt::ArrowCursor)); 82 | return true; 83 | } 84 | } 85 | 86 | return false; 87 | } 88 | 89 | bool FramelessWindow::mousePress(QMouseEvent* event) 90 | { 91 | if (event->buttons() & Qt::LeftButton && !isMaximized() && !isFullScreen()) { 92 | QWindow* win = window()->windowHandle(); 93 | Qt::Edges edges = this->getEdgesByPos(event->globalPosition().toPoint(), win->frameGeometry()); 94 | if (edges) { 95 | win->startSystemResize(edges); 96 | return true; 97 | } 98 | } 99 | 100 | return false; 101 | } 102 | 103 | Qt::CursorShape FramelessWindow::getCursorByEdge(const Qt::Edges& edges, Qt::CursorShape default_cursor) 104 | { 105 | if ((edges == (Qt::TopEdge | Qt::LeftEdge)) || (edges == (Qt::RightEdge | Qt::BottomEdge))) 106 | return Qt::SizeFDiagCursor; 107 | else if ((edges == (Qt::TopEdge | Qt::RightEdge)) || (edges == (Qt::LeftEdge | Qt::BottomEdge))) 108 | return Qt::SizeBDiagCursor; 109 | else if (edges & (Qt::TopEdge | Qt::BottomEdge)) 110 | return Qt::SizeVerCursor; 111 | else if (edges & (Qt::LeftEdge | Qt::RightEdge)) 112 | return Qt::SizeHorCursor; 113 | else 114 | return default_cursor; 115 | } 116 | 117 | Qt::Edges FramelessWindow::getEdgesByPos(const QPoint gpos, const QRect& winrect) 118 | { 119 | const int borderWidth = 8; 120 | Qt::Edges edges; 121 | 122 | int x = gpos.x() - winrect.x(); 123 | int y = gpos.y() - winrect.y(); 124 | 125 | if (x < borderWidth) 126 | edges |= Qt::LeftEdge; 127 | if (x > (winrect.width() - borderWidth)) 128 | edges |= Qt::RightEdge; 129 | if (y < borderWidth) 130 | edges |= Qt::TopEdge; 131 | if (y > (winrect.height() - borderWidth)) 132 | edges |= Qt::BottomEdge; 133 | 134 | return edges; 135 | } 136 | 137 | -------------------------------------------------------------------------------- /app/framelesswindow.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Gary Wang 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | #ifndef FRAMELESSWINDOW_H 6 | #define FRAMELESSWINDOW_H 7 | 8 | #include 9 | 10 | QT_BEGIN_NAMESPACE 11 | class QVBoxLayout; 12 | QT_END_NAMESPACE 13 | 14 | class FramelessWindow : public QWidget 15 | { 16 | Q_OBJECT 17 | public: 18 | explicit FramelessWindow(QWidget *parent = nullptr); 19 | 20 | void setCentralWidget(QWidget * widget); 21 | void installResizeCapture(QObject* widget); 22 | 23 | protected: 24 | bool eventFilter(QObject *o, QEvent *e) override; 25 | bool mouseHover(QHoverEvent* event, QWidget* wg); 26 | bool mousePress(QMouseEvent* event); 27 | 28 | private: 29 | Qt::Edges m_oldEdges; 30 | Qt::CursorShape m_oldCursorShape; 31 | 32 | Qt::CursorShape getCursorByEdge(const Qt::Edges& edges, Qt::CursorShape default_cursor); 33 | Qt::Edges getEdgesByPos(const QPoint pos, const QRect& winrect); 34 | 35 | QVBoxLayout * m_centralLayout = nullptr; 36 | QWidget * m_centralWidget = nullptr; // just a pointer, doesn't take the ownership. 37 | }; 38 | 39 | #endif // FRAMELESSWINDOW_H 40 | -------------------------------------------------------------------------------- /app/graphicsscene.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Gary Wang 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | #include "graphicsscene.h" 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | class PGraphicsPixmapItem : public QGraphicsPixmapItem 18 | { 19 | public: 20 | PGraphicsPixmapItem(const QPixmap &pixmap, QGraphicsItem *parent = nullptr) 21 | : QGraphicsPixmapItem(pixmap, parent) 22 | {} 23 | 24 | enum { Type = UserType + 1 }; 25 | int type() const override { return Type; } 26 | 27 | void setScaleHint(float scaleHint) { 28 | m_scaleHint = scaleHint; 29 | } 30 | 31 | const QPixmap & scaledPixmap(float scaleHint) { 32 | if (qFuzzyCompare(scaleHint, m_cachedScaleHint)) return m_cachedPixmap; 33 | QSizeF resizedScale(boundingRect().size()); 34 | resizedScale *= scaleHint; 35 | QPixmap && sourcePixmap = pixmap(); 36 | m_cachedPixmap = sourcePixmap.scaled( 37 | resizedScale.toSize() * sourcePixmap.devicePixelRatioF(), 38 | Qt::KeepAspectRatio, 39 | Qt::SmoothTransformation); 40 | m_cachedScaleHint = scaleHint; 41 | return m_cachedPixmap; 42 | } 43 | 44 | void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, 45 | QWidget *widget) override 46 | { 47 | if (transformationMode() == Qt::FastTransformation) { 48 | return QGraphicsPixmapItem::paint(painter, option, widget); 49 | } else { 50 | // painter->setRenderHints(QPainter::Antialiasing); 51 | painter->drawPixmap(QRectF(offset(), boundingRect().size()).toRect(), 52 | scaledPixmap(m_scaleHint)); 53 | } 54 | } 55 | 56 | private: 57 | float m_scaleHint = 1; 58 | float m_cachedScaleHint = -1; 59 | QPixmap m_cachedPixmap; 60 | }; 61 | 62 | class PGraphicsMovieItem : public QGraphicsItem 63 | { 64 | public: 65 | PGraphicsMovieItem(QGraphicsItem *parent = nullptr) : QGraphicsItem(parent) {} 66 | 67 | enum { Type = UserType + 2 }; 68 | int type() const override { return Type; } 69 | 70 | void setMovie(QMovie* movie) { 71 | if (m_movie) m_movie->disconnect(); 72 | m_movie.reset(movie); 73 | m_movie->connect(m_movie.data(), &QMovie::updated, [this](){ 74 | this->update(); 75 | }); 76 | } 77 | 78 | QRectF boundingRect() const override { 79 | if (m_movie) { return m_movie->frameRect(); } 80 | else { return QRectF(); } 81 | } 82 | 83 | void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) override { 84 | if (m_movie) { 85 | painter->drawPixmap(m_movie->frameRect(), m_movie->currentPixmap(), m_movie->frameRect()); 86 | } 87 | } 88 | 89 | inline QMovie * movie() const { 90 | return m_movie.data(); 91 | } 92 | 93 | private: 94 | QScopedPointer m_movie; 95 | }; 96 | 97 | GraphicsScene::GraphicsScene(QObject *parent) 98 | : QGraphicsScene(parent) 99 | { 100 | showText(tr("Drag image here")); 101 | } 102 | 103 | GraphicsScene::~GraphicsScene() 104 | { 105 | 106 | } 107 | 108 | void GraphicsScene::showImage(const QPixmap &pixmap) 109 | { 110 | this->clear(); 111 | PGraphicsPixmapItem * pixmapItem = new PGraphicsPixmapItem(pixmap); 112 | this->addItem(pixmapItem); 113 | pixmapItem->setShapeMode(QGraphicsPixmapItem::BoundingRectShape); 114 | m_theThing = pixmapItem; 115 | this->setSceneRect(m_theThing->boundingRect()); 116 | } 117 | 118 | void GraphicsScene::showText(const QString &text) 119 | { 120 | this->clear(); 121 | QGraphicsTextItem * textItem = this->addText(text); 122 | textItem->setDefaultTextColor(QColor("White")); 123 | m_theThing = textItem; 124 | this->setSceneRect(m_theThing->boundingRect()); 125 | } 126 | 127 | void GraphicsScene::showSvg(const QString &filepath) 128 | { 129 | this->clear(); 130 | QGraphicsSvgItem * svgItem = new QGraphicsSvgItem(); 131 | QSvgRenderer * render = new QSvgRenderer(svgItem); 132 | #if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) 133 | // Qt 6.7.0's SVG support is terrible caused by huge memory usage, see QTBUG-124287 134 | // Qt 6.7.1's is somewhat better, memory issue seems fixed, but still laggy when zoom in, 135 | // see QTBUG-126771. Anyway let's disable it for now. 136 | render->setOptions(QtSvg::Tiny12FeaturesOnly); 137 | #endif // QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) 138 | render->load(filepath); 139 | svgItem->setSharedRenderer(render); 140 | this->addItem(svgItem); 141 | m_theThing = svgItem; 142 | this->setSceneRect(m_theThing->boundingRect()); 143 | } 144 | 145 | void GraphicsScene::showAnimated(const QString &filepath) 146 | { 147 | this->clear(); 148 | 149 | PGraphicsMovieItem * animatedItem = new PGraphicsMovieItem(); 150 | QMovie * movie = new QMovie(filepath); 151 | movie->start(); 152 | animatedItem->setMovie(movie); 153 | this->addItem(animatedItem); 154 | m_theThing = animatedItem; 155 | 156 | this->setSceneRect(m_theThing->boundingRect()); 157 | } 158 | 159 | bool GraphicsScene::trySetTransformationMode(Qt::TransformationMode mode, float scaleHint) 160 | { 161 | PGraphicsPixmapItem * pixmapItem = qgraphicsitem_cast(m_theThing); 162 | if (pixmapItem) { 163 | pixmapItem->setTransformationMode(mode); 164 | pixmapItem->setScaleHint(scaleHint); 165 | return true; 166 | } 167 | 168 | return false; 169 | } 170 | 171 | bool GraphicsScene::togglePauseAnimation() 172 | { 173 | PGraphicsMovieItem * animatedItem = qgraphicsitem_cast(m_theThing); 174 | if (animatedItem) { 175 | animatedItem->movie()->setPaused(animatedItem->movie()->state() != QMovie::Paused); 176 | return true; 177 | } 178 | return false; 179 | } 180 | 181 | bool GraphicsScene::skipAnimationFrame(int delta) 182 | { 183 | PGraphicsMovieItem * animatedItem = qgraphicsitem_cast(m_theThing); 184 | if (animatedItem) { 185 | const int frameCount = animatedItem->movie()->frameCount(); 186 | const int currentFrame = animatedItem->movie()->currentFrameNumber(); 187 | const int targetFrame = (currentFrame + delta) % frameCount; 188 | animatedItem->movie()->setPaused(true); 189 | return animatedItem->movie()->jumpToFrame(targetFrame); 190 | } 191 | return false; 192 | } 193 | 194 | QPixmap GraphicsScene::renderToPixmap() 195 | { 196 | PGraphicsPixmapItem * pixmapItem = qgraphicsitem_cast(m_theThing); 197 | if (pixmapItem) { 198 | return pixmapItem->pixmap(); 199 | } 200 | 201 | QPixmap pixmap(sceneRect().toRect().size()); 202 | pixmap.fill(Qt::transparent); 203 | QPainter p(&pixmap); 204 | render(&p, sceneRect()); 205 | 206 | return pixmap; 207 | } 208 | -------------------------------------------------------------------------------- /app/graphicsscene.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Gary Wang 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | #ifndef GRAPHICSSCENE_H 6 | #define GRAPHICSSCENE_H 7 | 8 | #include 9 | 10 | class GraphicsScene : public QGraphicsScene 11 | { 12 | Q_OBJECT 13 | public: 14 | GraphicsScene(QObject *parent = nullptr); 15 | ~GraphicsScene(); 16 | 17 | void showImage(const QPixmap &pixmap); 18 | void showText(const QString &text); 19 | void showSvg(const QString &filepath); 20 | void showAnimated(const QString &filepath); 21 | 22 | bool trySetTransformationMode(Qt::TransformationMode mode, float scaleHint); 23 | 24 | bool togglePauseAnimation(); 25 | bool skipAnimationFrame(int delta = 1); 26 | 27 | QPixmap renderToPixmap(); 28 | 29 | private: 30 | QGraphicsItem * m_theThing = nullptr; 31 | }; 32 | 33 | #endif // GRAPHICSSCENE_H 34 | -------------------------------------------------------------------------------- /app/graphicsview.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Gary Wang 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | #include "graphicsview.h" 6 | 7 | #include "graphicsscene.h" 8 | #include "settings.h" 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | GraphicsView::GraphicsView(QWidget *parent) 18 | : QGraphicsView (parent) 19 | { 20 | setDragMode(QGraphicsView::ScrollHandDrag); 21 | setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); 22 | setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); 23 | setResizeAnchor(QGraphicsView::AnchorUnderMouse); 24 | setTransformationAnchor(QGraphicsView::AnchorUnderMouse); 25 | setStyleSheet("background-color: rgba(0, 0, 0, 220);" 26 | "border-radius: 3px;"); 27 | setAcceptDrops(false); 28 | setCheckerboardEnabled(false); 29 | 30 | connect(horizontalScrollBar(), &QScrollBar::valueChanged, this, &GraphicsView::viewportRectChanged); 31 | connect(verticalScrollBar(), &QScrollBar::valueChanged, this, &GraphicsView::viewportRectChanged); 32 | } 33 | 34 | void GraphicsView::showFileFromPath(const QString &filePath) 35 | { 36 | emit navigatorViewRequired(false, transform()); 37 | 38 | if (filePath.endsWith(".svg")) { 39 | showSvg(filePath); 40 | } else { 41 | QImageReader imageReader(filePath); 42 | imageReader.setAutoTransform(true); 43 | imageReader.setDecideFormatFromContent(true); 44 | imageReader.setAllocationLimit(0); 45 | 46 | // Since if the image format / plugin does not support this feature, imageFormat() will returns an invalid format. 47 | // So we cannot use imageFormat() and check if it returns QImage::Format_Invalid to detect if we support the file. 48 | // QImage::Format imageFormat = imageReader.imageFormat(); 49 | if (imageReader.format().isEmpty()) { 50 | showText(tr("File is not a valid image")); 51 | } else if (imageReader.supportsAnimation() && imageReader.imageCount() > 1) { 52 | showAnimated(filePath); 53 | } else if (!imageReader.canRead()) { 54 | showText(tr("Image data is invalid or currently unsupported")); 55 | } else { 56 | QPixmap && pixmap = QPixmap::fromImageReader(&imageReader); 57 | if (pixmap.isNull()) { 58 | showText(tr("Image data is invalid or currently unsupported")); 59 | } else { 60 | pixmap.setDevicePixelRatio(devicePixelRatioF()); 61 | showImage(pixmap); 62 | } 63 | } 64 | } 65 | } 66 | 67 | void GraphicsView::showImage(const QPixmap &pixmap) 68 | { 69 | resetTransform(); 70 | scene()->showImage(pixmap); 71 | displayScene(); 72 | } 73 | 74 | void GraphicsView::showImage(const QImage &image) 75 | { 76 | resetTransform(); 77 | scene()->showImage(QPixmap::fromImage(image)); 78 | displayScene(); 79 | } 80 | 81 | void GraphicsView::showText(const QString &text) 82 | { 83 | resetTransform(); 84 | scene()->showText(text); 85 | displayScene(); 86 | } 87 | 88 | void GraphicsView::showSvg(const QString &filepath) 89 | { 90 | resetTransform(); 91 | scene()->showSvg(filepath); 92 | displayScene(); 93 | } 94 | 95 | void GraphicsView::showAnimated(const QString &filepath) 96 | { 97 | resetTransform(); 98 | scene()->showAnimated(filepath); 99 | displayScene(); 100 | } 101 | 102 | GraphicsScene *GraphicsView::scene() const 103 | { 104 | return qobject_cast(QGraphicsView::scene()); 105 | } 106 | 107 | void GraphicsView::setScene(GraphicsScene *scene) 108 | { 109 | return QGraphicsView::setScene(scene); 110 | } 111 | 112 | qreal GraphicsView::scaleFactor() const 113 | { 114 | return QStyleOptionGraphicsItem::levelOfDetailFromTransform(transform()); 115 | } 116 | 117 | void GraphicsView::resetTransform() 118 | { 119 | if (!shouldAvoidTransform()) { 120 | QGraphicsView::resetTransform(); 121 | } 122 | } 123 | 124 | void GraphicsView::zoomView(qreal scaleFactor) 125 | { 126 | m_enableFitInView = false; 127 | scale(scaleFactor, scaleFactor); 128 | applyTransformationModeByScaleFactor(); 129 | emit navigatorViewRequired(!isThingSmallerThanWindowWith(transform()), transform()); 130 | } 131 | 132 | // This is always according to user's view. 133 | // the direction of the rotation will NOT be clockwise because the y-axis points downwards. 134 | void GraphicsView::rotateView(bool clockwise) 135 | { 136 | resetScale(); 137 | 138 | QTransform tf(0, clockwise ? 1 : -1, 0, 139 | clockwise ? -1 : 1, 0, 0, 140 | 0, 0, 1); 141 | tf = transform() * tf; 142 | setTransform(tf); 143 | } 144 | 145 | void GraphicsView::flipView(bool horizontal) 146 | { 147 | QTransform tf(horizontal ? -1 : 1, 0, 0, 148 | 0, horizontal ? 1 : -1, 0, 149 | 0, 0, 1); 150 | tf = transform() * tf; 151 | setTransform(tf); 152 | 153 | // Ensure the navigation view is also flipped. 154 | emit navigatorViewRequired(!isThingSmallerThanWindowWith(transform()), transform()); 155 | } 156 | 157 | void GraphicsView::resetScale() 158 | { 159 | setTransform(resetScale(transform())); 160 | applyTransformationModeByScaleFactor(); 161 | emit navigatorViewRequired(!isThingSmallerThanWindowWith(transform()), transform()); 162 | } 163 | 164 | void GraphicsView::fitInView(const QRectF &rect, Qt::AspectRatioMode aspectRadioMode) 165 | { 166 | QGraphicsView::fitInView(rect, aspectRadioMode); 167 | applyTransformationModeByScaleFactor(); 168 | } 169 | 170 | void GraphicsView::fitByOrientation(Qt::Orientation ori, bool scaleDownOnly) 171 | { 172 | resetScale(); 173 | 174 | QRectF viewRect = this->viewport()->rect().adjusted(2, 2, -2, -2); 175 | QRectF imageRect = transform().mapRect(sceneRect()); 176 | 177 | qreal ratio; 178 | 179 | if (ori == Qt::Horizontal) { 180 | ratio = viewRect.width() / imageRect.width(); 181 | } else { 182 | ratio = viewRect.height() / imageRect.height(); 183 | } 184 | 185 | if (scaleDownOnly && ratio > 1) ratio = 1; 186 | 187 | scale(ratio, ratio); 188 | centerOn(imageRect.top(), 0); 189 | m_enableFitInView = false; 190 | 191 | applyTransformationModeByScaleFactor(); 192 | emit navigatorViewRequired(!isThingSmallerThanWindowWith(transform()), transform()); 193 | } 194 | 195 | void GraphicsView::displayScene() 196 | { 197 | if (shouldAvoidTransform()) { 198 | emit navigatorViewRequired(!isThingSmallerThanWindowWith(transform()), transform()); 199 | return; 200 | } 201 | 202 | if (isSceneBiggerThanView()) { 203 | fitInView(sceneRect(), Qt::KeepAspectRatio); 204 | } 205 | 206 | m_enableFitInView = true; 207 | m_firstUserMediaLoaded = true; 208 | } 209 | 210 | bool GraphicsView::isSceneBiggerThanView() const 211 | { 212 | if (!isThingSmallerThanWindowWith(transform())) { 213 | return true; 214 | } else { 215 | return false; 216 | } 217 | } 218 | 219 | // Automately do fit in view when viewport(window) smaller than image original size. 220 | void GraphicsView::setEnableAutoFitInView(bool enable) 221 | { 222 | m_enableFitInView = enable; 223 | } 224 | 225 | bool GraphicsView::avoidResetTransform() const 226 | { 227 | return m_avoidResetTransform; 228 | } 229 | 230 | void GraphicsView::setAvoidResetTransform(bool avoidReset) 231 | { 232 | m_avoidResetTransform = avoidReset; 233 | } 234 | 235 | inline double zeroOrOne(double number) 236 | { 237 | return qFuzzyIsNull(number) ? 0 : (number > 0 ? 1 : -1); 238 | } 239 | 240 | // Note: this only works if we only have 90 degree based rotation 241 | // and no shear/translate. 242 | QTransform GraphicsView::resetScale(const QTransform & orig) 243 | { 244 | return QTransform(zeroOrOne(orig.m11()), zeroOrOne(orig.m12()), 245 | zeroOrOne(orig.m21()), zeroOrOne(orig.m22()), 246 | orig.dx(), orig.dy()); 247 | } 248 | 249 | void GraphicsView::toggleCheckerboard(bool invertCheckerboardColor) 250 | { 251 | setCheckerboardEnabled(!m_checkerboardEnabled, invertCheckerboardColor); 252 | } 253 | 254 | void GraphicsView::mousePressEvent(QMouseEvent *event) 255 | { 256 | if (shouldIgnoreMousePressMoveEvent(event)) { 257 | event->ignore(); 258 | // blumia: return here, or the QMouseEvent event transparency won't 259 | // work if we set a QGraphicsView::ScrollHandDrag drag mode. 260 | return; 261 | } 262 | 263 | return QGraphicsView::mousePressEvent(event); 264 | } 265 | 266 | void GraphicsView::mouseMoveEvent(QMouseEvent *event) 267 | { 268 | if (shouldIgnoreMousePressMoveEvent(event)) { 269 | event->ignore(); 270 | } 271 | 272 | return QGraphicsView::mouseMoveEvent(event); 273 | } 274 | 275 | void GraphicsView::mouseReleaseEvent(QMouseEvent *event) 276 | { 277 | if (event->button() == Qt::ForwardButton || event->button() == Qt::BackButton) { 278 | event->ignore(); 279 | } else { 280 | QGraphicsItem *item = itemAt(event->pos()); 281 | if (!item) { 282 | event->ignore(); 283 | } 284 | } 285 | 286 | return QGraphicsView::mouseReleaseEvent(event); 287 | } 288 | 289 | void GraphicsView::wheelEvent(QWheelEvent *event) 290 | { 291 | event->ignore(); 292 | // blumia: no need for calling parent method. 293 | } 294 | 295 | void GraphicsView::resizeEvent(QResizeEvent *event) 296 | { 297 | if (m_enableFitInView) { 298 | bool originalSizeSmallerThanWindow = isThingSmallerThanWindowWith(resetScale(transform())); 299 | if (originalSizeSmallerThanWindow && scaleFactor() >= 1) { 300 | // no longer need to do fitInView() 301 | // but we leave the m_enableFitInView value unchanged in case 302 | // user resize down the window again. 303 | } else if (originalSizeSmallerThanWindow && scaleFactor() < 1) { 304 | resetScale(); 305 | } else { 306 | fitInView(sceneRect(), Qt::KeepAspectRatio); 307 | } 308 | } else { 309 | emit navigatorViewRequired(!isThingSmallerThanWindowWith(transform()), transform()); 310 | } 311 | return QGraphicsView::resizeEvent(event); 312 | } 313 | 314 | bool GraphicsView::isThingSmallerThanWindowWith(const QTransform &transform) const 315 | { 316 | return rect().size().expandedTo(transform.mapRect(sceneRect()).size().toSize()) 317 | == rect().size(); 318 | } 319 | 320 | bool GraphicsView::shouldIgnoreMousePressMoveEvent(const QMouseEvent *event) const 321 | { 322 | if (event->buttons() == Qt::NoButton) { 323 | return true; 324 | } 325 | 326 | QGraphicsItem *item = itemAt(event->pos()); 327 | if (!item) { 328 | return true; 329 | } 330 | 331 | if (isThingSmallerThanWindowWith(transform())) { 332 | return true; 333 | } 334 | 335 | return false; 336 | } 337 | 338 | void GraphicsView::setCheckerboardEnabled(bool enabled, bool invertColor) 339 | { 340 | m_checkerboardEnabled = enabled; 341 | bool isLightCheckerboard = Settings::instance()->useLightCheckerboard() ^ invertColor; 342 | if (m_checkerboardEnabled) { 343 | // Prepare background check-board pattern 344 | QPixmap tilePixmap(0x20, 0x20); 345 | tilePixmap.fill(isLightCheckerboard ? QColor(220, 220, 220, 170) : QColor(35, 35, 35, 170)); 346 | QPainter tilePainter(&tilePixmap); 347 | constexpr QColor color(45, 45, 45, 170); 348 | constexpr QColor invertedColor(210, 210, 210, 170); 349 | tilePainter.fillRect(0, 0, 0x10, 0x10, isLightCheckerboard ? invertedColor : color); 350 | tilePainter.fillRect(0x10, 0x10, 0x10, 0x10, isLightCheckerboard ? invertedColor : color); 351 | tilePainter.end(); 352 | 353 | setBackgroundBrush(tilePixmap); 354 | } else { 355 | setBackgroundBrush(Qt::transparent); 356 | } 357 | } 358 | 359 | void GraphicsView::applyTransformationModeByScaleFactor() 360 | { 361 | if (this->scaleFactor() < 1) { 362 | scene()->trySetTransformationMode(Qt::SmoothTransformation, this->scaleFactor()); 363 | } else { 364 | scene()->trySetTransformationMode(Qt::FastTransformation, this->scaleFactor()); 365 | } 366 | } 367 | 368 | bool GraphicsView::shouldAvoidTransform() const 369 | { 370 | return m_firstUserMediaLoaded && m_avoidResetTransform; 371 | } 372 | -------------------------------------------------------------------------------- /app/graphicsview.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Gary Wang 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | #ifndef GRAPHICSVIEW_H 6 | #define GRAPHICSVIEW_H 7 | 8 | #include 9 | #include 10 | 11 | class GraphicsScene; 12 | class GraphicsView : public QGraphicsView 13 | { 14 | Q_OBJECT 15 | public: 16 | GraphicsView(QWidget *parent = nullptr); 17 | 18 | void showFileFromPath(const QString &filePath); 19 | 20 | void showImage(const QPixmap &pixmap); 21 | void showImage(const QImage &image); 22 | void showText(const QString &text); 23 | void showSvg(const QString &filepath); 24 | void showAnimated(const QString &filepath); 25 | 26 | GraphicsScene * scene() const; 27 | void setScene(GraphicsScene *scene); 28 | 29 | qreal scaleFactor() const; 30 | 31 | void resetTransform(); 32 | void zoomView(qreal scaleFactor); 33 | void rotateView(bool clockwise = true); 34 | void flipView(bool horizontal = true); 35 | void resetScale(); 36 | void fitInView(const QRectF &rect, Qt::AspectRatioMode aspectRadioMode = Qt::IgnoreAspectRatio); 37 | void fitByOrientation(Qt::Orientation ori = Qt::Horizontal, bool scaleDownOnly = false); 38 | 39 | void displayScene(); 40 | bool isSceneBiggerThanView() const; 41 | void setEnableAutoFitInView(bool enable = true); 42 | 43 | bool avoidResetTransform() const; 44 | void setAvoidResetTransform(bool avoidReset); 45 | 46 | static QTransform resetScale(const QTransform & orig); 47 | 48 | signals: 49 | void navigatorViewRequired(bool required, QTransform transform); 50 | void viewportRectChanged(); 51 | 52 | public slots: 53 | void toggleCheckerboard(bool invertCheckerboardColor = false); 54 | 55 | private: 56 | void mousePressEvent(QMouseEvent * event) override; 57 | void mouseMoveEvent(QMouseEvent * event) override; 58 | void mouseReleaseEvent(QMouseEvent * event) override; 59 | void wheelEvent(QWheelEvent *event) override; 60 | void resizeEvent(QResizeEvent *event) override; 61 | 62 | bool isThingSmallerThanWindowWith(const QTransform &transform) const; 63 | bool shouldIgnoreMousePressMoveEvent(const QMouseEvent *event) const; 64 | void setCheckerboardEnabled(bool enabled, bool invertColor = false); 65 | void applyTransformationModeByScaleFactor(); 66 | 67 | inline bool shouldAvoidTransform() const; 68 | 69 | // Consider switch to 3 state for "no fit", "always fit" and "fit when view is smaller"? 70 | // ... or even more? e.g. "fit/snap width" things... 71 | // Currently it's "no fit" when it's false and "fit when view is smaller" when it's true. 72 | bool m_enableFitInView = false; 73 | bool m_avoidResetTransform = false; 74 | bool m_checkerboardEnabled = false; 75 | bool m_useLightCheckerboard = false; 76 | bool m_firstUserMediaLoaded = false; 77 | }; 78 | 79 | #endif // GRAPHICSVIEW_H 80 | -------------------------------------------------------------------------------- /app/main.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Gary Wang 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | #include "mainwindow.h" 6 | 7 | #include "playlistmanager.h" 8 | #include "settings.h" 9 | 10 | #ifdef Q_OS_MACOS 11 | #include "fileopeneventhandler.h" 12 | #endif // Q_OS_MACOS 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | using namespace Qt::Literals::StringLiterals; 21 | 22 | int main(int argc, char *argv[]) 23 | { 24 | QCoreApplication::setApplicationName(u"Pineapple Pictures"_s); 25 | QCoreApplication::setApplicationVersion(PPIC_VERSION_STRING); 26 | QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Settings::instance()->hiDpiScaleFactorBehavior()); 27 | 28 | QApplication a(argc, argv); 29 | 30 | QTranslator translator; 31 | #if defined(TRANSLATION_RESOURCE_EMBEDDING) 32 | const QString qmDir = u":/i18n/"_s; 33 | #elif defined(QM_FILE_INSTALL_ABSOLUTE_DIR) 34 | const QString qmDir = QT_STRINGIFY(QM_FILE_INSTALL_ABSOLUTE_DIR); 35 | #else 36 | const QString qmDir = QDir(QCoreApplication::applicationDirPath()).absoluteFilePath("translations"); 37 | #endif 38 | if (translator.load(QLocale(), u"PineapplePictures"_s, u"_"_s, qmDir)) { 39 | QCoreApplication::installTranslator(&translator); 40 | } 41 | 42 | QGuiApplication::setApplicationDisplayName(QCoreApplication::translate("main", "Pineapple Pictures")); 43 | 44 | // commandline options 45 | QCommandLineOption supportedImageFormats(u"supported-image-formats"_s, QCoreApplication::translate("main", "List supported image format suffixes, and quit program.")); 46 | // parse commandline arguments 47 | QCommandLineParser parser; 48 | parser.addOption(supportedImageFormats); 49 | parser.addPositionalArgument("File list", QCoreApplication::translate("main", "File list.")); 50 | parser.addHelpOption(); 51 | parser.process(a); 52 | 53 | if (parser.isSet(supportedImageFormats)) { 54 | #if QT_VERSION < QT_VERSION_CHECK(6, 9, 0) 55 | fputs(qPrintable(MainWindow::supportedImageFormats().join(QChar('\n'))), stdout); 56 | ::exit(EXIT_SUCCESS); 57 | #else 58 | QCommandLineParser::showMessageAndExit(QCommandLineParser::MessageType::Information, 59 | MainWindow::supportedImageFormats().join(QChar('\n'))); 60 | #endif 61 | } 62 | 63 | MainWindow w; 64 | w.show(); 65 | 66 | #ifdef Q_OS_MACOS 67 | FileOpenEventHandler * fileOpenEventHandler = new FileOpenEventHandler(&a); 68 | a.installEventFilter(fileOpenEventHandler); 69 | a.connect(fileOpenEventHandler, &FileOpenEventHandler::fileOpen, [&w](const QUrl & url){ 70 | if (w.isHidden()) { 71 | w.setWindowOpacity(1); 72 | w.showNormal(); 73 | } else { 74 | w.activateWindow(); 75 | } 76 | w.showUrls({url}); 77 | w.initWindowSize(); 78 | }); 79 | #endif // Q_OS_MACOS 80 | 81 | QStringList urlStrList = parser.positionalArguments(); 82 | QList && urlList = PlaylistManager::convertToUrlList(urlStrList); 83 | 84 | if (!urlList.isEmpty()) { 85 | w.showUrls(urlList); 86 | } 87 | 88 | w.initWindowSize(); 89 | 90 | return QApplication::exec(); 91 | } 92 | -------------------------------------------------------------------------------- /app/mainwindow.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2025 Gary Wang 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | #ifndef MAINWINDOW_H 6 | #define MAINWINDOW_H 7 | 8 | #include "framelesswindow.h" 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | QT_BEGIN_NAMESPACE 15 | class QGraphicsOpacityEffect; 16 | class QGraphicsView; 17 | class QFileSystemWatcher; 18 | QT_END_NAMESPACE 19 | 20 | class ActionManager; 21 | class PlaylistManager; 22 | class ToolButton; 23 | class GraphicsView; 24 | class NavigatorView; 25 | class BottomButtonGroup; 26 | class MainWindow : public FramelessWindow 27 | { 28 | Q_OBJECT 29 | 30 | public: 31 | explicit MainWindow(QWidget *parent = nullptr); 32 | ~MainWindow() override; 33 | 34 | void showUrls(const QList &urls); 35 | void initWindowSize(); 36 | void adjustWindowSizeBySceneRect(); 37 | QUrl currentImageFileUrl() const; 38 | 39 | void clearGallery(); 40 | void galleryPrev(); 41 | void galleryNext(); 42 | void galleryCurrent(bool showLoadImageHintWhenEmpty, bool reloadImage); 43 | 44 | static QStringList supportedImageFormats(); 45 | 46 | protected slots: 47 | void showEvent(QShowEvent *event) override; 48 | void enterEvent(QEnterEvent *event) override; 49 | void leaveEvent(QEvent *event) override; 50 | void mousePressEvent(QMouseEvent *event) override; 51 | void mouseMoveEvent(QMouseEvent *event) override; 52 | void mouseReleaseEvent(QMouseEvent *event) override; 53 | void mouseDoubleClickEvent(QMouseEvent *event) override; 54 | void wheelEvent(QWheelEvent *event) override; 55 | void resizeEvent(QResizeEvent *event) override; 56 | void contextMenuEvent(QContextMenuEvent *event) override; 57 | void dragEnterEvent(QDragEnterEvent *event) override; 58 | void dragMoveEvent(QDragMoveEvent *event) override; 59 | void dropEvent(QDropEvent *event) override; 60 | 61 | void centerWindow(); 62 | void closeWindow(); 63 | void updateWidgetsPosition(); 64 | void toggleProtectedMode(); 65 | void toggleStayOnTop(); 66 | void toggleAvoidResetTransform(); 67 | bool stayOnTop() const; 68 | bool canPaste() const; 69 | void quitAppAction(bool force = false); 70 | void toggleFullscreen(); 71 | void toggleMaximize(); 72 | 73 | protected: 74 | QSize sizeHint() const override; 75 | 76 | private slots: 77 | void on_actionOpen_triggered(); 78 | 79 | void on_actionActualSize_triggered(); 80 | void on_actionToggleMaximize_triggered(); 81 | void on_actionZoomIn_triggered(); 82 | void on_actionZoomOut_triggered(); 83 | void on_actionToggleCheckerboard_triggered(); 84 | void on_actionRotateClockwise_triggered(); 85 | void on_actionRotateCounterClockwise_triggered(); 86 | 87 | void on_actionPrevPicture_triggered(); 88 | void on_actionNextPicture_triggered(); 89 | 90 | void on_actionTogglePauseAnimation_triggered(); 91 | void on_actionAnimationNextFrame_triggered(); 92 | 93 | void on_actionHorizontalFlip_triggered(); 94 | void on_actionFitInView_triggered(); 95 | void on_actionFitByWidth_triggered(); 96 | void on_actionCopyPixmap_triggered(); 97 | void on_actionCopyFilePath_triggered(); 98 | void on_actionPaste_triggered(); 99 | void on_actionTrash_triggered(); 100 | void on_actionToggleStayOnTop_triggered(); 101 | void on_actionToggleProtectMode_triggered(); 102 | void on_actionToggleAvoidResetTransform_triggered(); 103 | void on_actionSettings_triggered(); 104 | void on_actionHelp_triggered(); 105 | void on_actionProperties_triggered(); 106 | void on_actionLocateInFileManager_triggered(); 107 | void on_actionQuitApp_triggered(); 108 | 109 | private: 110 | bool updateFileWatcher(const QString & basePath = QString()); 111 | 112 | private: 113 | ActionManager *m_am; 114 | PlaylistManager *m_pm; 115 | 116 | QPoint m_oldMousePos; 117 | QPropertyAnimation *m_fadeOutAnimation; 118 | QPropertyAnimation *m_floatUpAnimation; 119 | QParallelAnimationGroup *m_exitAnimationGroup; 120 | QFileSystemWatcher *m_fileSystemWatcher; 121 | ToolButton *m_closeButton; 122 | ToolButton *m_prevButton; 123 | ToolButton *m_nextButton; 124 | GraphicsView *m_graphicsView; 125 | NavigatorView *m_gv; 126 | BottomButtonGroup *m_bottomButtonGroup; 127 | bool m_protectedMode = false; 128 | bool m_clickedOnWindow = false; 129 | }; 130 | 131 | #endif // MAINWINDOW_H 132 | -------------------------------------------------------------------------------- /app/metadatadialog.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Gary Wang 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | #include "metadatadialog.h" 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include "metadatamodel.h" 15 | 16 | class PropertyTreeView : public QTreeView 17 | { 18 | public: 19 | explicit PropertyTreeView(QWidget* parent) : QTreeView(parent) {} 20 | ~PropertyTreeView() {} 21 | 22 | protected: 23 | void rowsInserted(const QModelIndex& parent, int start, int end) override 24 | { 25 | QTreeView::rowsInserted(parent, start, end); 26 | if (!parent.isValid()) { 27 | // we are inserting a section group 28 | for (int row = start; row <= end; ++row) { 29 | setupSection(row); 30 | } 31 | } else { 32 | // we are inserting a property 33 | setRowHidden(parent.row(), QModelIndex(), false); 34 | } 35 | } 36 | 37 | void reset() override 38 | { 39 | QTreeView::reset(); 40 | if (model()) { 41 | for (int row = 0; row < model()->rowCount(); ++row) { 42 | setupSection(row); 43 | } 44 | } 45 | } 46 | 47 | private: 48 | void setupSection(int row) 49 | { 50 | expand(model()->index(row, 0)); 51 | setFirstColumnSpanned(row, QModelIndex(), true); 52 | setRowHidden(row, QModelIndex(), !model()->hasChildren(model()->index(row, 0))); 53 | } 54 | }; 55 | 56 | class PropertyTreeItemDelegate : public QStyledItemDelegate 57 | { 58 | public: 59 | PropertyTreeItemDelegate(QObject* parent) 60 | : QStyledItemDelegate(parent) 61 | {} 62 | 63 | protected: 64 | void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override 65 | { 66 | QStyleOptionViewItem opt = option; 67 | if (!index.parent().isValid()) { 68 | opt.font.setBold(true); 69 | opt.features.setFlag(QStyleOptionViewItem::Alternate); 70 | } 71 | QStyledItemDelegate::paint(painter, opt, index); 72 | } 73 | }; 74 | 75 | MetadataDialog::MetadataDialog(QWidget *parent) 76 | : QDialog(parent) 77 | , m_treeView(new PropertyTreeView(this)) 78 | { 79 | m_treeView->setRootIsDecorated(false); 80 | m_treeView->setIndentation(0); 81 | m_treeView->setItemDelegate(new PropertyTreeItemDelegate(m_treeView)); 82 | m_treeView->header()->resizeSection(0, sizeHint().width() / 2); 83 | 84 | setWindowTitle(tr("Image Metadata")); 85 | 86 | QDialogButtonBox * buttonBox = new QDialogButtonBox(QDialogButtonBox::Close); 87 | 88 | setLayout(new QVBoxLayout); 89 | layout()->addWidget(m_treeView); 90 | layout()->addWidget(buttonBox); 91 | 92 | connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::close); 93 | 94 | setWindowFlag(Qt::WindowContextHelpButtonHint, false); 95 | } 96 | 97 | MetadataDialog::~MetadataDialog() 98 | { 99 | 100 | } 101 | 102 | void MetadataDialog::setMetadataModel(MetadataModel * model) 103 | { 104 | m_treeView->setModel(model); 105 | } 106 | 107 | QSize MetadataDialog::sizeHint() const 108 | { 109 | return QSize(520, 350); 110 | } 111 | -------------------------------------------------------------------------------- /app/metadatadialog.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Gary Wang 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | #ifndef METADATADIALOG_H 6 | #define METADATADIALOG_H 7 | 8 | #include 9 | 10 | QT_BEGIN_NAMESPACE 11 | class QTreeView; 12 | QT_END_NAMESPACE 13 | 14 | class MetadataModel; 15 | class MetadataDialog : public QDialog 16 | { 17 | Q_OBJECT 18 | public: 19 | explicit MetadataDialog(QWidget * parent); 20 | ~MetadataDialog() override; 21 | 22 | void setMetadataModel(MetadataModel * model); 23 | 24 | QSize sizeHint() const override; 25 | 26 | private: 27 | QTreeView * m_treeView = nullptr; 28 | }; 29 | 30 | #endif // METADATADIALOG_H 31 | -------------------------------------------------------------------------------- /app/metadatamodel.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Gary Wang 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | #ifndef METADATAMODEL_H 6 | #define METADATAMODEL_H 7 | 8 | #include 9 | 10 | class Exiv2Wrapper; 11 | class MetadataModel : public QAbstractItemModel 12 | { 13 | Q_OBJECT 14 | 15 | public: 16 | explicit MetadataModel(QObject *parent = nullptr); 17 | ~MetadataModel(); 18 | 19 | void setFile(const QString & imageFilePath); 20 | static QString imageSize(const QSize &size); 21 | static QString imageSizeRatio(const QSize &size); 22 | bool appendSection(const QString & sectionKey, QStringView sectionDisplayName); 23 | bool appendPropertyIfNotEmpty(const QString & sectionKey, const QString & propertyKey, 24 | const QString & propertyDisplayName, const QString & propertyValue = QString()); 25 | bool appendProperty(const QString & sectionKey, const QString & propertyKey, 26 | QStringView propertyDisplayName, QStringView propertyValue = QString()); 27 | bool appendExivPropertyIfExist(const Exiv2Wrapper & wrapper, const QString & sectionKey, 28 | const QString & exiv2propertyKey, const QString & propertyDisplayName = QString(), 29 | bool isXmpString = false); 30 | 31 | private: 32 | enum RowType : quintptr { 33 | SectionRow, 34 | PropertyRow, 35 | }; 36 | 37 | QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; 38 | QModelIndex parent(const QModelIndex &child) const override; 39 | int rowCount(const QModelIndex &parent = QModelIndex()) const override; 40 | int columnCount(const QModelIndex & = QModelIndex()) const override; 41 | QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; 42 | QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; 43 | 44 | // [SECTION_KEY] 45 | QList m_sections; 46 | // {SECTION_KEY: (SECTION_DISPLAY_NAME, [PROPERTY_KEY])} 47 | QMap > > m_sectionProperties; 48 | // {PROPERTY_KEY: (PROPERTY_DISPLAY_NAME, PROPERTY_VALUE)} 49 | QMap > m_properties; 50 | }; 51 | 52 | #endif // METADATAMODEL_H 53 | -------------------------------------------------------------------------------- /app/navigatorview.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Gary Wang 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | #include "navigatorview.h" 6 | 7 | #include "graphicsview.h" 8 | #include "opacityhelper.h" 9 | 10 | #include 11 | #include 12 | 13 | NavigatorView::NavigatorView(QWidget *parent) 14 | : QGraphicsView (parent) 15 | , m_viewportRegion(this->rect()) 16 | , m_opacityHelper(new OpacityHelper(this)) 17 | { 18 | setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); 19 | setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); 20 | setStyleSheet("background-color: rgba(0, 0, 0, 120);" 21 | "border-radius: 3px;"); 22 | } 23 | 24 | // doesn't take or manage its ownership 25 | void NavigatorView::setMainView(GraphicsView *mainView) 26 | { 27 | m_mainView = mainView; 28 | } 29 | 30 | void NavigatorView::setOpacity(qreal opacity, bool animated) 31 | { 32 | m_opacityHelper->setOpacity(opacity, animated); 33 | } 34 | 35 | void NavigatorView::updateMainViewportRegion() 36 | { 37 | if (m_mainView != nullptr) { 38 | m_viewportRegion = mapFromScene(m_mainView->mapToScene(m_mainView->rect())); 39 | update(); 40 | } 41 | } 42 | 43 | void NavigatorView::mousePressEvent(QMouseEvent *event) 44 | { 45 | m_mouseDown = true; 46 | 47 | if (m_mainView) { 48 | m_mainView->centerOn(mapToScene(event->pos())); 49 | update(); 50 | } 51 | 52 | event->accept(); 53 | } 54 | 55 | void NavigatorView::mouseMoveEvent(QMouseEvent *event) 56 | { 57 | if (m_mouseDown && m_mainView) { 58 | m_mainView->centerOn(mapToScene(event->pos())); 59 | update(); 60 | event->accept(); 61 | } else { 62 | event->ignore(); 63 | } 64 | } 65 | 66 | void NavigatorView::mouseReleaseEvent(QMouseEvent *event) 67 | { 68 | m_mouseDown = false; 69 | 70 | event->accept(); 71 | } 72 | 73 | void NavigatorView::wheelEvent(QWheelEvent *event) 74 | { 75 | event->ignore(); 76 | return QGraphicsView::wheelEvent(event); 77 | } 78 | 79 | void NavigatorView::paintEvent(QPaintEvent *event) 80 | { 81 | QGraphicsView::paintEvent(event); 82 | 83 | QPainter painter(viewport()); 84 | painter.setPen(QPen(Qt::gray, 2)); 85 | painter.drawRect(m_viewportRegion.boundingRect()); 86 | } 87 | -------------------------------------------------------------------------------- /app/navigatorview.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Gary Wang 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | #ifndef NAVIGATORVIEW_H 6 | #define NAVIGATORVIEW_H 7 | 8 | #include 9 | 10 | class OpacityHelper; 11 | class GraphicsView; 12 | class NavigatorView : public QGraphicsView 13 | { 14 | Q_OBJECT 15 | public: 16 | NavigatorView(QWidget *parent = nullptr); 17 | 18 | void setMainView(GraphicsView *mainView); 19 | void setOpacity(qreal opacity, bool animated = true); 20 | 21 | public slots: 22 | void updateMainViewportRegion(); 23 | 24 | private: 25 | void mousePressEvent(QMouseEvent * event) override; 26 | void mouseMoveEvent(QMouseEvent * event) override; 27 | void mouseReleaseEvent(QMouseEvent * event) override; 28 | 29 | void wheelEvent(QWheelEvent *event) override; 30 | void paintEvent(QPaintEvent *event) override; 31 | 32 | bool m_mouseDown = false; 33 | QPolygon m_viewportRegion; 34 | QGraphicsView *m_mainView = nullptr; 35 | OpacityHelper *m_opacityHelper = nullptr; 36 | }; 37 | 38 | #endif // NAVIGATORVIEW_H 39 | -------------------------------------------------------------------------------- /app/opacityhelper.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Gary Wang 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | #include "opacityhelper.h" 6 | 7 | #include 8 | #include 9 | 10 | OpacityHelper::OpacityHelper(QWidget *parent) 11 | : QObject(parent) 12 | , m_opacityFx(new QGraphicsOpacityEffect(parent)) 13 | , m_opacityAnimation(new QPropertyAnimation(m_opacityFx, "opacity")) 14 | { 15 | parent->setGraphicsEffect(m_opacityFx); 16 | 17 | m_opacityAnimation->setDuration(300); 18 | } 19 | 20 | void OpacityHelper::setOpacity(qreal opacity, bool animated) 21 | { 22 | if (!animated) { 23 | m_opacityFx->setOpacity(opacity); 24 | return; 25 | } 26 | 27 | m_opacityAnimation->stop(); 28 | m_opacityAnimation->setStartValue(m_opacityFx->opacity()); 29 | m_opacityAnimation->setEndValue(opacity); 30 | m_opacityAnimation->start(); 31 | } 32 | -------------------------------------------------------------------------------- /app/opacityhelper.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Gary Wang 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | #ifndef OPACITYHELPER_H 6 | #define OPACITYHELPER_H 7 | 8 | #include 9 | 10 | QT_BEGIN_NAMESPACE 11 | class QGraphicsOpacityEffect; 12 | class QPropertyAnimation; 13 | QT_END_NAMESPACE 14 | 15 | class OpacityHelper : QObject 16 | { 17 | public: 18 | OpacityHelper(QWidget * parent); 19 | 20 | void setOpacity(qreal opacity, bool animated = true); 21 | 22 | protected: 23 | QGraphicsOpacityEffect * m_opacityFx; 24 | QPropertyAnimation * m_opacityAnimation; 25 | }; 26 | 27 | #endif // OPACITYHELPER_H 28 | -------------------------------------------------------------------------------- /app/playlistmanager.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2025 Gary Wang 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | #include "playlistmanager.h" 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | PlaylistModel::PlaylistModel(QObject *parent) 13 | : QAbstractListModel(parent) 14 | { 15 | 16 | } 17 | 18 | PlaylistModel::~PlaylistModel() 19 | = default; 20 | 21 | void PlaylistModel::setPlaylist(const QList &urls) 22 | { 23 | beginResetModel(); 24 | m_playlist = urls; 25 | endResetModel(); 26 | } 27 | 28 | QModelIndex PlaylistModel::loadPlaylist(const QList & urls) 29 | { 30 | if (urls.isEmpty()) return {}; 31 | if (urls.count() == 1) { 32 | return loadPlaylist(urls.constFirst()); 33 | } else { 34 | setPlaylist(urls); 35 | return index(0); 36 | } 37 | } 38 | 39 | QModelIndex PlaylistModel::loadPlaylist(const QUrl &url) 40 | { 41 | QFileInfo info(url.toLocalFile()); 42 | QDir dir(info.path()); 43 | QString && currentFileName = info.fileName(); 44 | 45 | if (dir.path() == m_currentDir) { 46 | int idx = indexOf(url); 47 | return idx == -1 ? appendToPlaylist(url) : index(idx); 48 | } 49 | 50 | QStringList entryList = dir.entryList( 51 | m_autoLoadSuffixes, 52 | QDir::Files | QDir::NoSymLinks, QDir::NoSort); 53 | 54 | QCollator collator; 55 | collator.setNumericMode(true); 56 | 57 | std::sort(entryList.begin(), entryList.end(), collator); 58 | 59 | QList playlist; 60 | 61 | int idx = -1; 62 | for (int i = 0; i < entryList.count(); i++) { 63 | const QString & fileName = entryList.at(i); 64 | const QString & oneEntry = dir.absoluteFilePath(fileName); 65 | const QUrl & url = QUrl::fromLocalFile(oneEntry); 66 | playlist.append(url); 67 | if (fileName == currentFileName) { 68 | idx = i; 69 | } 70 | } 71 | if (idx == -1) { 72 | idx = playlist.count(); 73 | playlist.append(url); 74 | } 75 | m_currentDir = dir.path(); 76 | 77 | setPlaylist(playlist); 78 | 79 | return index(idx); 80 | } 81 | 82 | QModelIndex PlaylistModel::appendToPlaylist(const QUrl &url) 83 | { 84 | const int lastIndex = rowCount(); 85 | beginInsertRows(QModelIndex(), lastIndex, lastIndex); 86 | m_playlist.append(url); 87 | endInsertRows(); 88 | return index(lastIndex); 89 | } 90 | 91 | bool PlaylistModel::removeAt(int index) 92 | { 93 | if (index < 0 || index >= rowCount()) return false; 94 | beginRemoveRows(QModelIndex(), index, index); 95 | m_playlist.removeAt(index); 96 | endRemoveRows(); 97 | return true; 98 | } 99 | 100 | int PlaylistModel::indexOf(const QUrl &url) const 101 | { 102 | return m_playlist.indexOf(url); 103 | } 104 | 105 | QUrl PlaylistModel::urlByIndex(int index) const 106 | { 107 | return m_playlist.value(index); 108 | } 109 | 110 | QStringList PlaylistModel::autoLoadFilterSuffixes() const 111 | { 112 | return m_autoLoadSuffixes; 113 | } 114 | 115 | QHash PlaylistModel::roleNames() const 116 | { 117 | QHash result = QAbstractListModel::roleNames(); 118 | result.insert(UrlRole, "url"); 119 | return result; 120 | } 121 | 122 | int PlaylistModel::rowCount(const QModelIndex &parent) const 123 | { 124 | return m_playlist.count(); 125 | } 126 | 127 | QVariant PlaylistModel::data(const QModelIndex &index, int role) const 128 | { 129 | if (!index.isValid()) return {}; 130 | 131 | switch (role) { 132 | case Qt::DisplayRole: 133 | return m_playlist.at(index.row()).fileName(); 134 | case UrlRole: 135 | return m_playlist.at(index.row()); 136 | } 137 | 138 | return {}; 139 | } 140 | 141 | PlaylistManager::PlaylistManager(QObject *parent) 142 | : QObject(parent) 143 | { 144 | connect(&m_model, &PlaylistModel::rowsRemoved, this, 145 | [this](const QModelIndex &, int, int) { 146 | if (m_model.rowCount() <= m_currentIndex) { 147 | setProperty("currentIndex", m_currentIndex - 1); 148 | } 149 | }); 150 | 151 | auto onRowCountChanged = [this](){ 152 | emit totalCountChanged(m_model.rowCount()); 153 | }; 154 | 155 | connect(&m_model, &PlaylistModel::rowsInserted, this, onRowCountChanged); 156 | connect(&m_model, &PlaylistModel::rowsRemoved, this, onRowCountChanged); 157 | connect(&m_model, &PlaylistModel::modelReset, this, onRowCountChanged); 158 | } 159 | 160 | PlaylistManager::~PlaylistManager() 161 | { 162 | 163 | } 164 | 165 | PlaylistModel *PlaylistManager::model() 166 | { 167 | return &m_model; 168 | } 169 | 170 | void PlaylistManager::setPlaylist(const QList &urls) 171 | { 172 | m_model.setPlaylist(urls); 173 | } 174 | 175 | QModelIndex PlaylistManager::loadPlaylist(const QList &urls) 176 | { 177 | QModelIndex idx = m_model.loadPlaylist(urls); 178 | setProperty("currentIndex", idx.row()); 179 | return idx; 180 | } 181 | 182 | QModelIndex PlaylistManager::loadPlaylist(const QUrl &url) 183 | { 184 | QModelIndex idx = m_model.loadPlaylist(url); 185 | setProperty("currentIndex", idx.row()); 186 | return idx; 187 | } 188 | 189 | int PlaylistManager::totalCount() const 190 | { 191 | return m_model.rowCount(); 192 | } 193 | 194 | QModelIndex PlaylistManager::previousIndex() const 195 | { 196 | int count = totalCount(); 197 | if (count == 0) return {}; 198 | 199 | return m_model.index(m_currentIndex - 1 < 0 ? count - 1 : m_currentIndex - 1); 200 | } 201 | 202 | QModelIndex PlaylistManager::nextIndex() const 203 | { 204 | int count = totalCount(); 205 | if (count == 0) return {}; 206 | 207 | return m_model.index(m_currentIndex + 1 == count ? 0 : m_currentIndex + 1); 208 | } 209 | 210 | QModelIndex PlaylistManager::curIndex() const 211 | { 212 | return m_model.index(m_currentIndex); 213 | } 214 | 215 | void PlaylistManager::setCurrentIndex(const QModelIndex &index) 216 | { 217 | if (index.isValid() && index.row() >= 0 && index.row() < totalCount()) { 218 | setProperty("currentIndex", index.row()); 219 | } 220 | } 221 | 222 | QUrl PlaylistManager::urlByIndex(const QModelIndex &index) 223 | { 224 | return m_model.urlByIndex(index.row()); 225 | } 226 | 227 | QString PlaylistManager::localFileByIndex(const QModelIndex &index) 228 | { 229 | return urlByIndex(index).toLocalFile(); 230 | } 231 | 232 | bool PlaylistManager::removeAt(const QModelIndex &index) 233 | { 234 | return m_model.removeAt(index.row()); 235 | } 236 | 237 | void PlaylistManager::setAutoLoadFilterSuffixes(const QStringList &nameFilters) 238 | { 239 | m_model.setProperty("autoLoadFilterSuffixes", nameFilters); 240 | } 241 | 242 | QList PlaylistManager::convertToUrlList(const QStringList &files) 243 | { 244 | QList urlList; 245 | for (const QString & str : std::as_const(files)) { 246 | QUrl url = QUrl::fromLocalFile(str); 247 | if (url.isValid()) { 248 | urlList.append(url); 249 | } 250 | } 251 | 252 | return urlList; 253 | } 254 | -------------------------------------------------------------------------------- /app/playlistmanager.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2025 Gary Wang 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | #pragma once 6 | 7 | #include 8 | #include 9 | 10 | class PlaylistModel : public QAbstractListModel 11 | { 12 | Q_OBJECT 13 | public: 14 | enum PlaylistRole { 15 | UrlRole = Qt::UserRole 16 | }; 17 | Q_ENUM(PlaylistRole) 18 | Q_PROPERTY(QStringList autoLoadFilterSuffixes MEMBER m_autoLoadSuffixes NOTIFY autoLoadFilterSuffixesChanged) 19 | 20 | explicit PlaylistModel(QObject *parent = nullptr); 21 | ~PlaylistModel() override; 22 | 23 | void setPlaylist(const QList & urls); 24 | QModelIndex loadPlaylist(const QList & urls); 25 | QModelIndex loadPlaylist(const QUrl & url); 26 | QModelIndex appendToPlaylist(const QUrl & url); 27 | bool removeAt(int index); 28 | int indexOf(const QUrl & url) const; 29 | QUrl urlByIndex(int index) const; 30 | QStringList autoLoadFilterSuffixes() const; 31 | 32 | QHash roleNames() const override; 33 | int rowCount(const QModelIndex &parent = QModelIndex()) const override; 34 | QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; 35 | 36 | signals: 37 | void autoLoadFilterSuffixesChanged(QStringList suffixes); 38 | 39 | private: 40 | // model data 41 | QList m_playlist; 42 | // properties 43 | QStringList m_autoLoadSuffixes = {}; 44 | // internal 45 | QString m_currentDir; 46 | }; 47 | 48 | class PlaylistManager : public QObject 49 | { 50 | Q_OBJECT 51 | public: 52 | Q_PROPERTY(int currentIndex MEMBER m_currentIndex NOTIFY currentIndexChanged) 53 | Q_PROPERTY(QStringList autoLoadFilterSuffixes WRITE setAutoLoadFilterSuffixes) 54 | Q_PROPERTY(PlaylistModel * model READ model CONSTANT) 55 | 56 | explicit PlaylistManager(QObject *parent = nullptr); 57 | ~PlaylistManager(); 58 | 59 | PlaylistModel * model(); 60 | 61 | void setPlaylist(const QList & url); 62 | Q_INVOKABLE QModelIndex loadPlaylist(const QList & urls); 63 | Q_INVOKABLE QModelIndex loadPlaylist(const QUrl & url); 64 | 65 | int totalCount() const; 66 | QModelIndex previousIndex() const; 67 | QModelIndex nextIndex() const; 68 | QModelIndex curIndex() const; 69 | void setCurrentIndex(const QModelIndex & index); 70 | QUrl urlByIndex(const QModelIndex & index); 71 | QString localFileByIndex(const QModelIndex & index); 72 | bool removeAt(const QModelIndex & index); 73 | 74 | void setAutoLoadFilterSuffixes(const QStringList &nameFilters); 75 | 76 | static QList convertToUrlList(const QStringList & files); 77 | 78 | signals: 79 | void currentIndexChanged(int index); 80 | void totalCountChanged(int count); 81 | 82 | private: 83 | int m_currentIndex = -1; 84 | PlaylistModel m_model; 85 | }; 86 | -------------------------------------------------------------------------------- /app/settings.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Gary Wang 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | #include "settings.h" 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | namespace QEnumHelper 17 | { 18 | template 19 | E fromString(const QString &text, const E defaultValue) 20 | { 21 | bool ok; 22 | E result = static_cast(QMetaEnum::fromType().keyToValue(text.toUtf8(), &ok)); 23 | if (!ok) { 24 | return defaultValue; 25 | } 26 | return result; 27 | } 28 | 29 | template 30 | QString toString(E value) 31 | { 32 | const int intValue = static_cast(value); 33 | return QString::fromUtf8(QMetaEnum::fromType().valueToKey(intValue)); 34 | } 35 | } 36 | 37 | Settings *Settings::m_settings_instance = nullptr; 38 | 39 | Settings *Settings::instance() 40 | { 41 | if (!m_settings_instance) { 42 | m_settings_instance = new Settings; 43 | } 44 | 45 | return m_settings_instance; 46 | } 47 | 48 | bool Settings::stayOnTop() 49 | { 50 | return m_qsettings->value("stay_on_top", true).toBool(); 51 | } 52 | 53 | bool Settings::useLightCheckerboard() 54 | { 55 | return m_qsettings->value("use_light_checkerboard", false).toBool(); 56 | } 57 | 58 | Settings::DoubleClickBehavior Settings::doubleClickBehavior() const 59 | { 60 | QString result = m_qsettings->value("double_click_behavior", "Close").toString(); 61 | 62 | return QEnumHelper::fromString(result, DoubleClickBehavior::Close); 63 | } 64 | 65 | Settings::MouseWheelBehavior Settings::mouseWheelBehavior() const 66 | { 67 | QString result = m_qsettings->value("mouse_wheel_behavior", "Zoom").toString(); 68 | 69 | return QEnumHelper::fromString(result, MouseWheelBehavior::Zoom); 70 | } 71 | 72 | Settings::WindowSizeBehavior Settings::initWindowSizeBehavior() const 73 | { 74 | QString result = m_qsettings->value("init_window_size_behavior", "Auto").toString(); 75 | 76 | return QEnumHelper::fromString(result, WindowSizeBehavior::Auto); 77 | } 78 | 79 | Qt::HighDpiScaleFactorRoundingPolicy Settings::hiDpiScaleFactorBehavior() const 80 | { 81 | QString result = m_qsettings->value("hidpi_scale_factor_behavior", "PassThrough").toString(); 82 | 83 | return QEnumHelper::fromString(result, Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); 84 | } 85 | 86 | void Settings::setStayOnTop(bool on) 87 | { 88 | m_qsettings->setValue("stay_on_top", on); 89 | m_qsettings->sync(); 90 | } 91 | 92 | void Settings::setUseLightCheckerboard(bool light) 93 | { 94 | m_qsettings->setValue("use_light_checkerboard", light); 95 | m_qsettings->sync(); 96 | } 97 | 98 | void Settings::setDoubleClickBehavior(DoubleClickBehavior dcb) 99 | { 100 | m_qsettings->setValue("double_click_behavior", QEnumHelper::toString(dcb)); 101 | m_qsettings->sync(); 102 | } 103 | 104 | void Settings::setMouseWheelBehavior(MouseWheelBehavior mwb) 105 | { 106 | m_qsettings->setValue("mouse_wheel_behavior", QEnumHelper::toString(mwb)); 107 | m_qsettings->sync(); 108 | } 109 | 110 | void Settings::setInitWindowSizeBehavior(WindowSizeBehavior wsb) 111 | { 112 | m_qsettings->setValue("init_window_size_behavior", QEnumHelper::toString(wsb)); 113 | m_qsettings->sync(); 114 | } 115 | 116 | void Settings::setHiDpiScaleFactorBehavior(Qt::HighDpiScaleFactorRoundingPolicy hidpi) 117 | { 118 | m_qsettings->setValue("hidpi_scale_factor_behavior", QEnumHelper::toString(hidpi)); 119 | m_qsettings->sync(); 120 | } 121 | 122 | void Settings::applyUserShortcuts(QWidget *widget) 123 | { 124 | m_qsettings->beginGroup("shortcuts"); 125 | const QStringList shortcutNames = m_qsettings->allKeys(); 126 | for (const QString & name : shortcutNames) { 127 | QList shortcuts = m_qsettings->value(name).value>(); 128 | setShortcutsForAction(widget, name, shortcuts, false); 129 | } 130 | m_qsettings->endGroup(); 131 | } 132 | 133 | bool Settings::setShortcutsForAction(QWidget *widget, const QString &objectName, 134 | QList shortcuts, bool writeConfig) 135 | { 136 | QAction * targetAction = nullptr; 137 | for (QAction * action : widget->actions()) { 138 | if (action->objectName() == objectName) { 139 | targetAction = action; 140 | } else { 141 | for (const QKeySequence & shortcut : std::as_const(shortcuts)) { 142 | if (action->shortcuts().contains(shortcut)) { 143 | return false; 144 | } 145 | } 146 | } 147 | } 148 | 149 | if (targetAction) { 150 | targetAction->setShortcuts(shortcuts); 151 | } 152 | 153 | if (targetAction && writeConfig) { 154 | m_qsettings->beginGroup("shortcuts"); 155 | m_qsettings->setValue(objectName, QVariant::fromValue(shortcuts)); 156 | m_qsettings->endGroup(); 157 | m_qsettings->sync(); 158 | } 159 | 160 | return true; 161 | } 162 | 163 | #if defined(FLAG_PORTABLE_MODE_SUPPORT) && defined(Q_OS_WIN) 164 | #include 165 | // QCoreApplication::applicationDirPath() parses the "applicationDirPath" from arg0, which... 166 | // 1. rely on a QApplication object instance 167 | // but we need to call QGuiApplication::setHighDpiScaleFactorRoundingPolicy() before QApplication get created 168 | // 2. arg0 is NOT garanteed to be the path of execution 169 | // see also: https://stackoverflow.com/questions/383973/is-args0-guaranteed-to-be-the-path-of-execution 170 | // This function is here mainly for #1. 171 | QString getApplicationDirPath() 172 | { 173 | WCHAR buffer[MAX_PATH]; 174 | GetModuleFileNameW(NULL, buffer, MAX_PATH); 175 | QString appPath = QString::fromWCharArray(buffer); 176 | 177 | return appPath.left(appPath.lastIndexOf('\\')); 178 | } 179 | #endif // defined(FLAG_PORTABLE_MODE_SUPPORT) && defined(Q_OS_WIN) 180 | 181 | Settings::Settings() 182 | : QObject(qApp) 183 | { 184 | QString configPath; 185 | 186 | #if defined(FLAG_PORTABLE_MODE_SUPPORT) && defined(Q_OS_WIN) 187 | QString portableConfigDirPath = QDir(getApplicationDirPath()).absoluteFilePath("data"); 188 | QFileInfo portableConfigDirInfo(portableConfigDirPath); 189 | if (portableConfigDirInfo.exists() && portableConfigDirInfo.isDir() && portableConfigDirInfo.isWritable()) { 190 | // we can use it. 191 | configPath = portableConfigDirPath; 192 | } 193 | #endif // defined(FLAG_PORTABLE_MODE_SUPPORT) && defined(Q_OS_WIN) 194 | 195 | if (configPath.isEmpty()) { 196 | // Should be %LOCALAPPDATA%\ under Windows, ~/.config/ under Linux. 197 | configPath = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation); 198 | } 199 | 200 | m_qsettings = new QSettings(QDir(configPath).absoluteFilePath("config.ini"), QSettings::IniFormat, this); 201 | 202 | qRegisterMetaType>(); 203 | } 204 | 205 | -------------------------------------------------------------------------------- /app/settings.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Gary Wang 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | #pragma once 6 | 7 | #include 8 | #include 9 | 10 | class Settings : public QObject 11 | { 12 | Q_OBJECT 13 | public: 14 | enum DoubleClickBehavior { 15 | Ignore, 16 | Close, 17 | Maximize, 18 | FullScreen, 19 | }; 20 | Q_ENUM(DoubleClickBehavior) 21 | 22 | enum MouseWheelBehavior { 23 | Zoom, 24 | Switch, 25 | }; 26 | Q_ENUM(MouseWheelBehavior) 27 | 28 | enum WindowSizeBehavior { 29 | Auto, 30 | Maximized, 31 | Windowed, 32 | }; 33 | Q_ENUM(WindowSizeBehavior) 34 | 35 | static Settings *instance(); 36 | 37 | bool stayOnTop(); 38 | bool useLightCheckerboard(); 39 | DoubleClickBehavior doubleClickBehavior() const; 40 | MouseWheelBehavior mouseWheelBehavior() const; 41 | WindowSizeBehavior initWindowSizeBehavior() const; 42 | Qt::HighDpiScaleFactorRoundingPolicy hiDpiScaleFactorBehavior() const; 43 | 44 | void setStayOnTop(bool on); 45 | void setUseLightCheckerboard(bool light); 46 | void setDoubleClickBehavior(DoubleClickBehavior dcb); 47 | void setMouseWheelBehavior(MouseWheelBehavior mwb); 48 | void setInitWindowSizeBehavior(WindowSizeBehavior wsb); 49 | void setHiDpiScaleFactorBehavior(Qt::HighDpiScaleFactorRoundingPolicy hidpi); 50 | 51 | void applyUserShortcuts(QWidget * widget); 52 | bool setShortcutsForAction(QWidget * widget, const QString & objectName, 53 | QList shortcuts, bool writeConfig = true); 54 | 55 | private: 56 | Settings(); 57 | 58 | static Settings *m_settings_instance; 59 | 60 | QSettings *m_qsettings; 61 | 62 | signals: 63 | 64 | public slots: 65 | }; 66 | -------------------------------------------------------------------------------- /app/settingsdialog.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Gary Wang 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | #include "settingsdialog.h" 6 | 7 | #include "settings.h" 8 | #include "shortcutedit.h" 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | SettingsDialog::SettingsDialog(QWidget *parent) 21 | : QDialog(parent) 22 | , m_stayOnTop(new QCheckBox) 23 | , m_useLightCheckerboard(new QCheckBox) 24 | , m_doubleClickBehavior(new QComboBox) 25 | , m_mouseWheelBehavior(new QComboBox) 26 | , m_initWindowSizeBehavior(new QComboBox) 27 | , m_hiDpiRoundingPolicyBehavior(new QComboBox) 28 | { 29 | this->setWindowTitle(tr("Settings")); 30 | 31 | QHBoxLayout * mainLayout = new QHBoxLayout(this); 32 | QTabWidget * settingsTabs = new QTabWidget(this); 33 | mainLayout->addWidget(settingsTabs); 34 | 35 | QWidget * settingsFormHolder = new QWidget; 36 | QFormLayout * settingsForm = new QFormLayout(settingsFormHolder); 37 | settingsTabs->addTab(settingsFormHolder, tr("Options")); 38 | 39 | QSplitter * shortcutEditorSplitter = new QSplitter; 40 | shortcutEditorSplitter->setOrientation(Qt::Vertical); 41 | shortcutEditorSplitter->setChildrenCollapsible(false); 42 | QScrollArea * shortcutScrollArea = new QScrollArea; 43 | shortcutEditorSplitter->addWidget(shortcutScrollArea); 44 | shortcutScrollArea->setWidgetResizable(true); 45 | shortcutScrollArea->setMinimumHeight(200); 46 | QWidget * shortcutsFormHolder = new QWidget; 47 | QFormLayout * shortcutsForm = new QFormLayout(shortcutsFormHolder); 48 | shortcutScrollArea->setWidget(shortcutsFormHolder); 49 | settingsTabs->addTab(shortcutEditorSplitter, tr("Shortcuts")); 50 | 51 | for (const QAction * action : parent->actions()) { 52 | ShortcutEdit * shortcutEdit = new ShortcutEdit; 53 | shortcutEdit->setObjectName(QLatin1String("shortcut_") + action->objectName()); 54 | shortcutEdit->setShortcuts(action->shortcuts()); 55 | shortcutsForm->addRow(action->text(), shortcutEdit); 56 | connect(shortcutEdit, &ShortcutEdit::editButtonClicked, this, [=](){ 57 | if (shortcutEditorSplitter->count() == 1) shortcutEditorSplitter->addWidget(new QWidget); 58 | ShortcutEditor * shortcutEditor = new ShortcutEditor(shortcutEdit); 59 | shortcutEditor->setDescription(tr("Editing shortcuts for action \"%1\":").arg(action->text())); 60 | QWidget * oldEditor = shortcutEditorSplitter->replaceWidget(1, shortcutEditor); 61 | shortcutEditorSplitter->setSizes({shortcutEditorSplitter->height(), 1}); 62 | oldEditor->deleteLater(); 63 | }); 64 | connect(shortcutEdit, &ShortcutEdit::applyShortcutsRequested, this, [=](QList newShortcuts){ 65 | bool succ = Settings::instance()->setShortcutsForAction(parent, shortcutEdit->objectName().mid(9), 66 | newShortcuts); 67 | if (!succ) { 68 | QMessageBox::warning(this, tr("Failed to set shortcuts"), 69 | tr("Please check if shortcuts are duplicated with existing shortcuts.")); 70 | } 71 | shortcutEdit->setShortcuts(action->shortcuts()); 72 | }); 73 | } 74 | 75 | static QList< QPair > _dc_options { 76 | { Settings::DoubleClickBehavior::Ignore, tr("Do nothing") }, 77 | { Settings::DoubleClickBehavior::Close, tr("Close the window") }, 78 | { Settings::DoubleClickBehavior::Maximize, tr("Toggle maximize") }, 79 | { Settings::DoubleClickBehavior::FullScreen, tr("Toggle fullscreen") } 80 | }; 81 | 82 | static QList< QPair > _mw_options { 83 | { Settings::MouseWheelBehavior::Zoom, tr("Zoom in and out") }, 84 | { Settings::MouseWheelBehavior::Switch, tr("View next or previous item") } 85 | }; 86 | 87 | static QList< QPair > _iws_options { 88 | { Settings::WindowSizeBehavior::Auto, tr("Auto size") }, 89 | { Settings::WindowSizeBehavior::Maximized, tr("Maximized") }, 90 | { Settings::WindowSizeBehavior::Windowed, tr("Windowed") } 91 | }; 92 | 93 | static QList< QPair > _hidpi_options { 94 | { Qt::HighDpiScaleFactorRoundingPolicy::Round, tr("Round (Integer scaling)", "This option means round up for .5 and above") }, 95 | { Qt::HighDpiScaleFactorRoundingPolicy::Ceil, tr("Ceil (Integer scaling)", "This option means always round up") }, 96 | { Qt::HighDpiScaleFactorRoundingPolicy::Floor, tr("Floor (Integer scaling)", "This option means always round down") }, 97 | { Qt::HighDpiScaleFactorRoundingPolicy::PassThrough, tr("Follow system (Fractional scaling)", "This option means don't round") } 98 | }; 99 | 100 | QStringList dcbDropDown; 101 | for (const QPair & dcOption : _dc_options) { 102 | dcbDropDown.append(dcOption.second); 103 | } 104 | 105 | QStringList mwbDropDown; 106 | for (const QPair & mwOption : _mw_options) { 107 | mwbDropDown.append(mwOption.second); 108 | } 109 | 110 | QStringList iwsbDropDown; 111 | for (const QPair & iwsOption : _iws_options) { 112 | iwsbDropDown.append(iwsOption.second); 113 | } 114 | 115 | QStringList hidpiDropDown; 116 | for (const QPair & hidpiOption : _hidpi_options) { 117 | hidpiDropDown.append(hidpiOption.second); 118 | } 119 | 120 | settingsForm->addRow(tr("Stay on top when start-up"), m_stayOnTop); 121 | settingsForm->addRow(tr("Use light-color checkerboard"), m_useLightCheckerboard); 122 | settingsForm->addRow(tr("Double-click behavior"), m_doubleClickBehavior); 123 | settingsForm->addRow(tr("Mouse wheel behavior"), m_mouseWheelBehavior); 124 | settingsForm->addRow(tr("Default window size"), m_initWindowSizeBehavior); 125 | settingsForm->addRow(tr("HiDPI scale factor rounding policy"), m_hiDpiRoundingPolicyBehavior); 126 | 127 | m_stayOnTop->setChecked(Settings::instance()->stayOnTop()); 128 | m_useLightCheckerboard->setChecked(Settings::instance()->useLightCheckerboard()); 129 | m_doubleClickBehavior->setModel(new QStringListModel(dcbDropDown)); 130 | Settings::DoubleClickBehavior dcb = Settings::instance()->doubleClickBehavior(); 131 | m_doubleClickBehavior->setCurrentIndex(static_cast(dcb)); 132 | m_mouseWheelBehavior->setModel(new QStringListModel(mwbDropDown)); 133 | Settings::MouseWheelBehavior mwb = Settings::instance()->mouseWheelBehavior(); 134 | m_mouseWheelBehavior->setCurrentIndex(static_cast(mwb)); 135 | m_initWindowSizeBehavior->setModel(new QStringListModel(iwsbDropDown)); 136 | Settings::WindowSizeBehavior iwsb = Settings::instance()->initWindowSizeBehavior(); 137 | m_initWindowSizeBehavior->setCurrentIndex(static_cast(iwsb)); 138 | m_hiDpiRoundingPolicyBehavior->setModel(new QStringListModel(hidpiDropDown)); 139 | Qt::HighDpiScaleFactorRoundingPolicy hidpi = Settings::instance()->hiDpiScaleFactorBehavior(); 140 | for (int i = 0; i < _hidpi_options.count(); i++) { 141 | if (_hidpi_options.at(i).first == hidpi) { 142 | m_hiDpiRoundingPolicyBehavior->setCurrentIndex(i); 143 | break; 144 | } 145 | } 146 | 147 | connect(m_stayOnTop, &QCheckBox::stateChanged, this, [ = ](int state){ 148 | Settings::instance()->setStayOnTop(state == Qt::Checked); 149 | }); 150 | 151 | connect(m_useLightCheckerboard, &QCheckBox::stateChanged, this, [ = ](int state){ 152 | Settings::instance()->setUseLightCheckerboard(state == Qt::Checked); 153 | }); 154 | 155 | connect(m_doubleClickBehavior, QOverload::of(&QComboBox::currentIndexChanged), this, [ = ](int index){ 156 | Settings::instance()->setDoubleClickBehavior(_dc_options.at(index).first); 157 | }); 158 | 159 | connect(m_mouseWheelBehavior, QOverload::of(&QComboBox::currentIndexChanged), this, [ = ](int index){ 160 | Settings::instance()->setMouseWheelBehavior(_mw_options.at(index).first); 161 | }); 162 | 163 | connect(m_initWindowSizeBehavior, QOverload::of(&QComboBox::currentIndexChanged), this, [ = ](int index){ 164 | Settings::instance()->setInitWindowSizeBehavior(_iws_options.at(index).first); 165 | }); 166 | 167 | connect(m_hiDpiRoundingPolicyBehavior, QOverload::of(&QComboBox::currentIndexChanged), this, [ = ](int index){ 168 | Settings::instance()->setHiDpiScaleFactorBehavior(_hidpi_options.at(index).first); 169 | }); 170 | 171 | adjustSize(); 172 | setWindowFlag(Qt::WindowContextHelpButtonHint, false); 173 | } 174 | 175 | SettingsDialog::~SettingsDialog() 176 | { 177 | 178 | } 179 | -------------------------------------------------------------------------------- /app/settingsdialog.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Gary Wang 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | #ifndef SETTINGSDIALOG_H 6 | #define SETTINGSDIALOG_H 7 | 8 | #include 9 | #include 10 | 11 | class QCheckBox; 12 | class QComboBox; 13 | class SettingsDialog : public QDialog 14 | { 15 | Q_OBJECT 16 | public: 17 | explicit SettingsDialog(QWidget *parent = nullptr); 18 | ~SettingsDialog(); 19 | 20 | signals: 21 | 22 | public slots: 23 | 24 | private: 25 | QCheckBox * m_stayOnTop = nullptr; 26 | QCheckBox * m_useLightCheckerboard = nullptr; 27 | QComboBox * m_doubleClickBehavior = nullptr; 28 | QComboBox * m_mouseWheelBehavior = nullptr; 29 | QComboBox * m_initWindowSizeBehavior = nullptr; 30 | QComboBox * m_hiDpiRoundingPolicyBehavior = nullptr; 31 | }; 32 | 33 | #endif // SETTINGSDIALOG_H 34 | -------------------------------------------------------------------------------- /app/shortcutedit.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 Gary Wang 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | #include "shortcutedit.h" 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | ShortcutEditor::ShortcutEditor(ShortcutEdit * shortcutEdit, QWidget * parent) 15 | : QWidget(parent) 16 | , m_descriptionLabel(new QLabel) 17 | , m_shortcutEdit(shortcutEdit) 18 | , m_shortcutLayout(new QFormLayout) 19 | { 20 | Q_CHECK_PTR(m_shortcutEdit); 21 | 22 | QDialogButtonBox * buttons = new QDialogButtonBox(QDialogButtonBox::Apply | QDialogButtonBox::Discard); 23 | 24 | QVBoxLayout * layout = new QVBoxLayout(this); 25 | layout->addWidget(m_descriptionLabel); 26 | layout->addLayout(m_shortcutLayout); 27 | layout->addWidget(buttons); 28 | 29 | connect(buttons, &QDialogButtonBox::clicked, this, [=](QAbstractButton *button){ 30 | if ((QPushButton *)button == buttons->button(QDialogButtonBox::Apply)) { 31 | applyShortcuts(); 32 | } else { 33 | reloadShortcuts(); 34 | } 35 | }); 36 | connect(shortcutEdit, &ShortcutEdit::shortcutsChanged, this, &ShortcutEditor::reloadShortcuts); 37 | 38 | setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum); 39 | 40 | reloadShortcuts(); 41 | } 42 | 43 | ShortcutEditor::~ShortcutEditor() 44 | { 45 | 46 | } 47 | 48 | void ShortcutEditor::setDescription(const QString &desc) 49 | { 50 | m_descriptionLabel->setText(desc); 51 | } 52 | 53 | void ShortcutEditor::reloadShortcuts() 54 | { 55 | if (!m_keySequenceEdits.isEmpty()) { 56 | for (QKeySequenceEdit * keyseqEdit : m_keySequenceEdits) { 57 | m_shortcutLayout->removeRow(keyseqEdit); 58 | } 59 | m_keySequenceEdits.clear(); 60 | } 61 | 62 | QList shortcuts = m_shortcutEdit->shortcuts(); 63 | shortcuts.append(QKeySequence()); 64 | for (const QKeySequence & shortcut : shortcuts) { 65 | QKeySequenceEdit * keyseqEdit = new QKeySequenceEdit(this); 66 | keyseqEdit->setClearButtonEnabled(true); 67 | #if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) 68 | keyseqEdit->setMaximumSequenceLength(1); 69 | #endif // QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) 70 | keyseqEdit->setKeySequence(shortcut); 71 | m_keySequenceEdits.append(keyseqEdit); 72 | } 73 | 74 | for (int i = 0; i < m_keySequenceEdits.count(); i++) { 75 | m_shortcutLayout->addRow(tr("Shortcut #%1").arg(i + 1), m_keySequenceEdits.at(i)); 76 | } 77 | } 78 | 79 | void ShortcutEditor::applyShortcuts() 80 | { 81 | QList shortcuts; 82 | for (const QKeySequenceEdit * keyseqEdit : m_keySequenceEdits) { 83 | if (!keyseqEdit->keySequence().isEmpty() && !shortcuts.contains(keyseqEdit->keySequence())) { 84 | shortcuts.append(keyseqEdit->keySequence()); 85 | } 86 | } 87 | emit m_shortcutEdit->applyShortcutsRequested(shortcuts); 88 | } 89 | 90 | // ---------------------------------------- 91 | 92 | ShortcutEdit::ShortcutEdit(QWidget *parent) 93 | : QWidget(parent) 94 | , m_shortcutsLabel(new QLabel(this)) 95 | , m_setShortcutButton(new QToolButton(this)) 96 | { 97 | m_setShortcutButton->setText("..."); 98 | 99 | QHBoxLayout * layout = new QHBoxLayout(this); 100 | layout->setContentsMargins(0, 0, 0, 0); 101 | layout->addWidget(m_shortcutsLabel, 1); 102 | layout->addWidget(m_setShortcutButton); 103 | 104 | connect(this, &ShortcutEdit::shortcutsChanged, this, [=](){ 105 | QStringList shortcutTexts; 106 | for (const QKeySequence & shortcut : std::as_const(m_shortcuts)) { 107 | shortcutTexts.append(shortcut.toString(QKeySequence::NativeText)); 108 | } 109 | m_shortcutsLabel->setText(shortcutTexts.isEmpty() ? tr("No shortcuts") : shortcutTexts.join(", ")); 110 | m_shortcutsLabel->setDisabled(shortcutTexts.isEmpty()); 111 | }); 112 | 113 | connect(m_setShortcutButton, &QToolButton::clicked, this, &ShortcutEdit::editButtonClicked); 114 | 115 | adjustSize(); 116 | } 117 | 118 | ShortcutEdit::~ShortcutEdit() 119 | { 120 | } 121 | 122 | QList ShortcutEdit::shortcuts() const 123 | { 124 | return m_shortcuts; 125 | } 126 | 127 | void ShortcutEdit::setShortcuts(const QList &shortcuts) 128 | { 129 | m_shortcuts = shortcuts; 130 | emit shortcutsChanged(); 131 | } 132 | -------------------------------------------------------------------------------- /app/shortcutedit.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 Gary Wang 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | #pragma once 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | class QLabel; 12 | class QFormLayout; 13 | class QToolButton; 14 | class QKeySequenceEdit; 15 | class ShortcutEdit; 16 | class ShortcutEditor : public QWidget 17 | { 18 | Q_OBJECT 19 | public: 20 | explicit ShortcutEditor(ShortcutEdit * shortcutEdit, QWidget * parent = nullptr); 21 | ~ShortcutEditor(); 22 | 23 | void setDescription(const QString & desc); 24 | 25 | void reloadShortcuts(); 26 | void applyShortcuts(); 27 | 28 | private: 29 | QLabel * m_descriptionLabel; 30 | ShortcutEdit * m_shortcutEdit; 31 | QFormLayout * m_shortcutLayout; 32 | QList m_keySequenceEdits; 33 | }; 34 | 35 | class ShortcutEdit : public QWidget 36 | { 37 | Q_OBJECT 38 | Q_PROPERTY(QList shortcuts MEMBER m_shortcuts WRITE setShortcuts NOTIFY shortcutsChanged) 39 | public: 40 | explicit ShortcutEdit(QWidget * parent = nullptr); 41 | ~ShortcutEdit(); 42 | 43 | QList shortcuts() const; 44 | void setShortcuts(const QList &shortcuts); 45 | 46 | signals: 47 | void shortcutsChanged(); 48 | void editButtonClicked(); 49 | void applyShortcutsRequested(QList newShortcuts); 50 | 51 | private: 52 | QList m_shortcuts; 53 | QLabel * m_shortcutsLabel; 54 | QToolButton * m_setShortcutButton; 55 | }; 56 | -------------------------------------------------------------------------------- /app/toolbutton.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Gary Wang 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | #include "toolbutton.h" 6 | 7 | #include "actionmanager.h" 8 | #include "opacityhelper.h" 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | ToolButton::ToolButton(bool hoverColor, QWidget *parent) 15 | : QPushButton(parent) 16 | , m_opacityHelper(new OpacityHelper(this)) 17 | { 18 | setFlat(true); 19 | QString qss = "QPushButton {" 20 | "background: transparent;" 21 | "}"; 22 | if (hoverColor) { 23 | qss += "QPushButton:hover {" 24 | "background: red;" 25 | "}"; 26 | } 27 | setStyleSheet(qss); 28 | } 29 | 30 | void ToolButton::setIconResourcePath(const QString &iconp) 31 | { 32 | this->setIcon(ActionManager::loadHidpiIcon(iconp, this->iconSize())); 33 | } 34 | 35 | void ToolButton::setOpacity(qreal opacity, bool animated) 36 | { 37 | m_opacityHelper->setOpacity(opacity, animated); 38 | } 39 | -------------------------------------------------------------------------------- /app/toolbutton.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Gary Wang 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | #ifndef TOOLBUTTON_H 6 | #define TOOLBUTTON_H 7 | 8 | #include 9 | 10 | class OpacityHelper; 11 | class ToolButton : public QPushButton 12 | { 13 | Q_OBJECT 14 | public: 15 | ToolButton(bool hoverColor = false, QWidget * parent = nullptr); 16 | void setIconResourcePath(const QString &iconp); 17 | 18 | public slots: 19 | void setOpacity(qreal opacity, bool animated = true); 20 | 21 | private: 22 | OpacityHelper * m_opacityHelper; 23 | }; 24 | 25 | #endif // TOOLBUTTON_H 26 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | image: 2 | - Visual Studio 2022 3 | environment: 4 | CMAKE_INSTALL_PREFIX: C:\projects\cmake 5 | LIBZ: C:\projects\zlib 6 | LIBEXPAT: C:\projects\libexpat 7 | LIBAVIF: C:\projects\libavif 8 | LIBEXIV2: C:\projects\exiv2 9 | PPKG: C:\projects\ppkg 10 | matrix: 11 | - job_name: mingw_64_qt6_8 12 | QTDIR: C:\Qt\6.8\mingw_64 13 | MINGW64: C:\Qt\Tools\mingw1310_64 14 | KF_BRANCH: master 15 | EXIV2_VERSION: "0.28.5" 16 | EXIV2_CMAKE_OPTIONS: "-DEXIV2_ENABLE_BROTLI=OFF -DEXIV2_ENABLE_INIH=OFF -DEXIV2_BUILD_EXIV2_COMMAND=OFF" 17 | PPIC_CMAKE_OPTIONS: "-DPREFER_QT_5=OFF" 18 | WINDEPLOYQT_ARGS: "--verbose=2 --no-quick-import --no-translations --no-opengl-sw --no-system-d3d-compiler --skip-plugin-types tls,networkinformation" 19 | 20 | install: 21 | - mkdir %CMAKE_INSTALL_PREFIX% 22 | - mkdir %LIBZ% 23 | - mkdir %LIBEXPAT% 24 | - mkdir %LIBAVIF% 25 | - mkdir %LIBEXIV2% 26 | - mkdir %PPKG% 27 | - cd %APPVEYOR_BUILD_FOLDER% 28 | - git submodule update --init --recursive 29 | - set PATH=%PATH%;%CMAKE_INSTALL_PREFIX%;%QTDIR%\bin;%MINGW64%\bin;%PPKG% 30 | - set CC=%MINGW64%\bin\gcc.exe 31 | - set CXX=%MINGW64%\bin\g++.exe 32 | 33 | build_script: 34 | # prepare 35 | - mkdir 3rdparty 36 | - choco install ninja 37 | - cd %PPKG% 38 | - curl -fsSL -o ppkg.exe https://github.com/BLumia/pineapple-package-manager/releases/latest/download/ppkg.exe 39 | - cd %APPVEYOR_BUILD_FOLDER% 40 | # download and install zlib for KArchive 41 | - cd %LIBZ% 42 | - curl -fsSL -o zlib131.zip https://zlib.net/zlib131.zip 43 | - 7z x zlib131.zip -y 44 | - cd zlib-1.3.1 45 | - mkdir build 46 | - cd build 47 | - cmake .. -G "Ninja" -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=%CMAKE_INSTALL_PREFIX% 48 | - cmake --build . --config Release 49 | - cmake --build . --config Release --target install/strip 50 | - cd %APPVEYOR_BUILD_FOLDER% 51 | # install ECM so we can build KImageFormats 52 | - cd 3rdparty 53 | - git clone -b %KF_BRANCH% -q https://invent.kde.org/frameworks/extra-cmake-modules.git 54 | - git rev-parse HEAD 55 | - cd extra-cmake-modules 56 | - cmake -G "Ninja" . -DCMAKE_INSTALL_PREFIX=%CMAKE_INSTALL_PREFIX% -DBUILD_TESTING=OFF 57 | - cmake --build . 58 | - cmake --build . --target install 59 | - cd %APPVEYOR_BUILD_FOLDER% 60 | # install AOM for libavif AV1 decoding support... 61 | - cd 3rdparty 62 | #- git clone -b v3.9.1 --depth 1 https://aomedia.googlesource.com/aom 63 | #- cd aom 64 | #- mkdir build.aom 65 | #- cd build.aom 66 | #- cmake .. -G "Ninja" -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=%CMAKE_INSTALL_PREFIX% -DENABLE_DOCS=OFF -DBUILD_SHARED_LIBS=ON -DAOM_TARGET_CPU=generic -DENABLE_TESTS=OFF -DENABLE_TESTDATA=OFF -DENABLE_TOOLS=OFF -DENABLE_EXAMPLES=0 67 | #- cmake --build . --config Release 68 | #- cmake --build . --config Release --target install/strip 69 | - mkdir aom 70 | - cd aom 71 | - curl -fsSL -o ppkg-aom.zip https://sourceforge.net/projects/pineapple-package-manager/files/packages/mingw-w64-x86_64-windows/aom-3.9.1-2.zip 72 | - ppkg ppkg-aom.zip 73 | - 7z x ppkg-aom.zip LICENSE -y 74 | - cd %APPVEYOR_BUILD_FOLDER% 75 | # install libavif for avif format support of KImageFormats 76 | - cd %LIBAVIF% 77 | - curl -fsSL -o libavif-v1_1_1.zip https://github.com/AOMediaCodec/libavif/archive/v1.1.1.zip 78 | - 7z x libavif-v1_1_1.zip -y 79 | - cd libavif-1.1.1 80 | - mkdir build 81 | - cd build 82 | - cmake .. -G "Ninja" -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=%CMAKE_INSTALL_PREFIX% -DAVIF_CODEC_AOM=ON -DAVIF_LOCAL_LIBYUV=ON 83 | - cmake --build . --config Release 84 | - cmake --build . --config Release --target install/strip 85 | - cd %APPVEYOR_BUILD_FOLDER% 86 | # install KArchive for kra format support of KImageFormats 87 | - cd 3rdparty 88 | - git clone -b %KF_BRANCH% -q https://invent.kde.org/frameworks/karchive.git 89 | - git rev-parse HEAD 90 | - cd karchive 91 | - mkdir build 92 | - cd build 93 | - cmake .. -G "Ninja" -DWITH_LIBZSTD=OFF -DWITH_BZIP2=OFF -DWITH_LIBLZMA=OFF -DWITH_OPENSSL=OFF -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=%CMAKE_INSTALL_PREFIX% 94 | - cmake --build . --config Release 95 | - cmake --build . --config Release --target install/strip 96 | - cd %APPVEYOR_BUILD_FOLDER% 97 | # build libexpat for libexiv2 98 | - cd %LIBEXPAT% 99 | - curl -fsSL -o R_2_6_2.zip https://github.com/libexpat/libexpat/archive/R_2_6_2.zip 100 | - 7z x R_2_6_2.zip -y 101 | - cd libexpat-R_2_6_2/expat/ 102 | - cmake -G "Ninja" . -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=%CMAKE_INSTALL_PREFIX% -DEXPAT_BUILD_EXAMPLES=OFF -DEXPAT_BUILD_TESTS=OFF -DEXPAT_BUILD_TOOLS=OFF 103 | - cmake --build . --target install/strip 104 | - cd %APPVEYOR_BUILD_FOLDER% 105 | # build libexiv2 106 | - cd %LIBEXIV2% 107 | - curl -fsSL -o v%EXIV2_VERSION%.zip https://github.com/Exiv2/exiv2/archive/v%EXIV2_VERSION%.zip 108 | - 7z x v%EXIV2_VERSION%.zip -y 109 | - cd exiv2-%EXIV2_VERSION% 110 | - cmake -G "Ninja" . -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=%CMAKE_INSTALL_PREFIX% %EXIV2_CMAKE_OPTIONS% 111 | - cmake --build . --target install/strip 112 | - cd %APPVEYOR_BUILD_FOLDER% 113 | # install KImageFormats 114 | - cd 3rdparty 115 | - git clone -b %KF_BRANCH% -q https://invent.kde.org/frameworks/kimageformats.git 116 | - git rev-parse HEAD 117 | - cd kimageformats 118 | - mkdir build 119 | - cd build 120 | - cmake .. -G "Ninja" -DBUILD_TESTING=OFF -DCMAKE_BUILD_TYPE=Release -DKDE_INSTALL_QTPLUGINDIR=%QTDIR%\plugins 121 | - cmake --build . --config Release 122 | - cmake --build . --config Release --target install/strip 123 | - cd %APPVEYOR_BUILD_FOLDER% 124 | # finally... 125 | - mkdir build 126 | - cd build 127 | - cmake .. -G "Ninja" %PPIC_CMAKE_OPTIONS% -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH=%CMAKE_INSTALL_PREFIX% -DCMAKE_INSTALL_PREFIX='%cd%' 128 | - cmake --build . --config Release 129 | - cmake --build . --config Release --target install/strip 130 | # fixme: I don't know how to NOT make the binary installed to the ./bin/ folder... 131 | - cd bin 132 | - copy %APPVEYOR_BUILD_FOLDER%\LICENSE 133 | - copy %CMAKE_INSTALL_PREFIX%\bin\libaom.dll 134 | - copy %CMAKE_INSTALL_PREFIX%\bin\libexpat-1.dll 135 | - copy %CMAKE_INSTALL_PREFIX%\bin\libexiv2.dll 136 | - copy %CMAKE_INSTALL_PREFIX%\bin\libavif.dll 137 | - copy %CMAKE_INSTALL_PREFIX%\bin\libzlib.dll 138 | - copy %CMAKE_INSTALL_PREFIX%\bin\libKF?Archive.dll 139 | - windeployqt %WINDEPLOYQT_ARGS% .\ppic.exe 140 | # copy 3rdparty licenses for the libs we vendored for windows... 141 | - mkdir licenses 142 | - cd licenses 143 | - copy %APPVEYOR_BUILD_FOLDER%\3rdparty\aom\LICENSE License.aom.txt 144 | - copy %APPVEYOR_BUILD_FOLDER%\3rdparty\karchive\LICENSES\LGPL-2.0-or-later.txt License.KArchive.txt 145 | - copy %APPVEYOR_BUILD_FOLDER%\3rdparty\kimageformats\LICENSES\LGPL-2.1-or-later.txt License.kimageformats.txt 146 | - copy %LIBEXPAT%\libexpat-R_2_6_2\expat\COPYING License.expat.txt 147 | - copy %LIBAVIF%\libavif-1.1.1\LICENSE License.libavif.txt 148 | - copy %LIBEXIV2%\exiv2-%EXIV2_VERSION%\COPYING License.exiv2.txt 149 | # TODO: Qt, zlib 150 | - cd .. 151 | # for debug.. 152 | - tree /f 153 | - cd %APPVEYOR_BUILD_FOLDER% 154 | - xcopy %CMAKE_INSTALL_PREFIX% .\cmake-prefix-copy /E /H /C /I 155 | 156 | artifacts: 157 | - path: build\bin 158 | - path: cmake-prefix-copy 159 | -------------------------------------------------------------------------------- /assets/icons/app-icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BLumia/pineapple-pictures/3cfb25db9a8a3f8084c637710cea87129cf713eb/assets/icons/app-icon.icns -------------------------------------------------------------------------------- /assets/icons/app-icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BLumia/pineapple-pictures/3cfb25db9a8a3f8084c637710cea87129cf713eb/assets/icons/app-icon.ico -------------------------------------------------------------------------------- /assets/icons/go-next.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /assets/icons/go-previous.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /assets/icons/object-rotate-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /assets/icons/view-background-checkerboard.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /assets/icons/view-fullscreen.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /assets/icons/window-close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 41 | 43 | 44 | 46 | image/svg+xml 47 | 49 | 50 | 51 | 52 | 53 | 58 | 64 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /assets/icons/zoom-in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /assets/icons/zoom-original.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /assets/icons/zoom-out.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /assets/pineapple-pictures.rc: -------------------------------------------------------------------------------- 1 | IDI_ICON1 ICON DISCARDABLE "icons/app-icon.ico" 2 | 1 VERSIONINFO 3 | BEGIN 4 | BLOCK "StringFileInfo" 5 | BEGIN 6 | BLOCK "040904E4" 7 | BEGIN 8 | VALUE "FileDescription", "Pineapple Pictures - Image Viewer" 9 | VALUE "LegalCopyright", "MIT/Expat License - Copyright (C) 2024 Gary Wang" 10 | VALUE "ProductName", "Pineapple Pictures" 11 | END 12 | END 13 | BLOCK "VarFileInfo" 14 | BEGIN 15 | VALUE "Translation", 0x409, 1200 16 | END 17 | END 18 | -------------------------------------------------------------------------------- /assets/plain/translators.html: -------------------------------------------------------------------------------- 1 |
    2 |
  • Catalan: Toni Estévez
  • 3 |
  • Chinese (Simplified): Percy Hong
  • 4 |
  • Dutch: Heimen Stoffels
  • 5 |
  • French: J. Lavoie, K. Herbert, Maxime Leroy
  • 6 |
  • German: K. Herbert, J. Lavoie, sal0max
  • 7 |
  • Indonesian: liimee, Reza Almanda
  • 8 |
  • Italian: albanobattistella
  • 9 |
  • Japanese: Black Cat, mmahhi, Percy Hong
  • 10 |
  • Korean: VenusGirl
  • 11 |
  • Norwegian Bokmål: Allan Nordhøy, ovl-1
  • 12 |
  • Punjabi (Pakistan): bgo-eiu
  • 13 |
  • Russian: Sergey Shornikov, Artem, Andrey
  • 14 |
  • Sinhala: HelaBasa
  • 15 |
  • Spanish: Toni Estévez, Génesis Toxical, William(ѕ)ⁿ, gallegonovato
  • 16 |
  • Tamil: தமிழ்நேரம் (TamilNeram)
  • 17 |
  • Turkish: E-Akcaer, Oğuz Ersen, Sabri Ünal
  • 18 |
  • Ukrainian: Dan, volkov, Сергій
  • 19 |
20 | -------------------------------------------------------------------------------- /assets/resources.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | icons/zoom-in.svg 4 | icons/zoom-out.svg 5 | icons/view-fullscreen.svg 6 | icons/zoom-original.svg 7 | icons/object-rotate-right.svg 8 | icons/view-background-checkerboard.svg 9 | icons/app-icon.svg 10 | icons/window-close.svg 11 | icons/go-next.svg 12 | icons/go-previous.svg 13 | plain/translators.html 14 | 15 | 16 | -------------------------------------------------------------------------------- /dist/MacOSXBundleInfo.plist.in: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | English 7 | CFBundleExecutable 8 | ${MACOSX_BUNDLE_EXECUTABLE_NAME} 9 | CFBundleGetInfoString 10 | ${MACOSX_BUNDLE_INFO_STRING} 11 | CFBundleIconFile 12 | ${MACOSX_BUNDLE_ICON_FILE} 13 | CFBundleIdentifier 14 | ${MACOSX_BUNDLE_GUI_IDENTIFIER} 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleLongVersionString 18 | ${MACOSX_BUNDLE_LONG_VERSION_STRING} 19 | CFBundleName 20 | ${MACOSX_BUNDLE_BUNDLE_NAME} 21 | CFBundlePackageType 22 | APPL 23 | CFBundleShortVersionString 24 | ${MACOSX_BUNDLE_SHORT_VERSION_STRING} 25 | CFBundleSignature 26 | ???? 27 | CFBundleVersion 28 | ${MACOSX_BUNDLE_BUNDLE_VERSION} 29 | CSResourcesFileMapped 30 | 31 | NSHumanReadableCopyright 32 | ${MACOSX_BUNDLE_COPYRIGHT} 33 | 34 | CFBundleLocalizations 35 | 36 | en 37 | ca 38 | de 39 | es 40 | fr 41 | id 42 | it 43 | ja 44 | ko 45 | nb_NO 46 | nl 47 | pa_PK 48 | ru 49 | si 50 | ta 51 | tr 52 | uk 53 | zh_CN 54 | 55 | CFBundleDocumentTypes 56 | 57 | 58 | 59 | CFBundleTypeName 60 | JPEG Image 61 | CFBundleTypeExtensions 62 | 63 | jpg 64 | jpeg 65 | jfif 66 | 67 | CFBundleTypeMIMETypes 68 | 69 | image/jpeg 70 | 71 | CFBundleTypeRole 72 | Viewer 73 | 74 | 75 | 76 | CFBundleTypeName 77 | PNG Image 78 | CFBundleTypeExtensions 79 | 80 | png 81 | 82 | CFBundleTypeMIMETypes 83 | 84 | image/png 85 | 86 | CFBundleTypeRole 87 | Viewer 88 | 89 | 90 | 91 | CFBundleTypeName 92 | WebP Image 93 | CFBundleTypeExtensions 94 | 95 | webp 96 | 97 | CFBundleTypeMIMETypes 98 | 99 | image/webp 100 | 101 | CFBundleTypeRole 102 | Viewer 103 | 104 | 105 | 106 | CFBundleTypeName 107 | GIF Image 108 | CFBundleTypeExtensions 109 | 110 | gif 111 | 112 | CFBundleTypeMIMETypes 113 | 114 | image/gif 115 | 116 | CFBundleTypeRole 117 | Viewer 118 | 119 | 120 | 121 | CFBundleTypeName 122 | SVG Image 123 | CFBundleTypeExtensions 124 | 125 | svg 126 | 127 | CFBundleTypeMIMETypes 128 | 129 | image/svg+xml 130 | 131 | CFBundleTypeRole 132 | Viewer 133 | 134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /dist/appstream/net.blumia.pineapple-pictures.metainfo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | net.blumia.pineapple-pictures 4 | Pineapple Pictures 5 | Pineapple Pictures 6 | Pineapple Pictures 7 | Pineapple Afbeeldingen 8 | Pineapple Pictures 9 | அன்னாசி படங்கள் 10 | Pineapple Pictures 11 | 菠萝看图 12 | Image Viewer 13 | Visor de imágenes 14 | 画像ビューアー 15 | Afbeeldingsweergave 16 | Просмотр изображений 17 | பட பார்வையாளர் 18 | Переглядач зображень 19 | 图像查看器 20 | CC0-1.0 21 | MIT 22 | 23 |

Pineapple Pictures is a lightweight and easy-to-use image viewer that comes with a handy navigation thumbnail when zoom-in, and doesn't contain any image management support.

24 |

Pineapple Pictures es un visor de imágenes ligero y fácil de usar que viene con una práctica miniatura de navegación al hacer zoom, y no contiene ningún soporte de gestión de imágenes.

25 |

Pineapple Picturesは、ズームイン時に便利なナビゲーションサムネイルを備えた軽量で使いやすい画像ビューアです。画像管理のサポートは含まれていません。

26 |

Pineapple Afbeeldingen is een licht en eenvoudig te gebruiken afbeeldingsweergaveprogramma met miniatuurnavigatie na inzoomen. Het programma heeft echter geen fotobeheermogelijkheid.

27 |

Pineapple Pictures - это легкий и простой в использовании просмотрщик изображений, оснащенный удобной навигацией по миниатюрам при увеличении масштаба и не содержащий никакой поддержки управления изображениями.

28 |

அன்னாசி படங்கள் ஒரு இலகுரக மற்றும் பயன்படுத்த எளிதான பட பார்வையாளராகும், இது பெரிதாக்கும்போது ஒரு எளிமையான வழிசெலுத்தல் சிறுபடத்துடன் வருகிறது, மேலும் எந்த பட மேலாண்மை ஆதரவையும் கொண்டிருக்கவில்லை.

29 |

Pineapple Pictures – це легкий і простий у використанні переглядач зображень, який постачається зі зручною навігаційною мініатюрою при збільшенні масштабу і не містить жодної підтримки керування зображеннями.

30 |

菠萝看图是一个轻量级易用的图像查看器,在图片放大时提供了方便的鸟瞰导航功能,且不包含任何图片管理功能。

31 |
32 | 33 | Gary (BLumia) Wang et al. 34 | Gary (BLumia) Wang など 35 | Gary (BLumia) Wang e.a. 36 | Gary (BLumia) Wang et al. 37 | Gary (BLumia) Wang. 38 | Gary (BLumia) Wang 等人 39 | 40 | net.blumia.pineapple-pictures.desktop 41 | https://github.com/BLumia/pineapple-pictures 42 | https://github.com/BLumia/pineapple-pictures/issues 43 | https://hosted.weblate.org/projects/pineapple-pictures/ 44 | 45 | ppic 46 | 47 | 48 | 49 | Main window when an image file is loaded 50 | Ventana principal cuando se carga un archivo de imagen 51 | 画像ファイル読み込み時のメインウィンドウ 52 | Hoofdvenster na het laden van een afbeelding 53 | Основное окно после загрузки файла изображения 54 | ஒரு படக் கோப்பு ஏற்றப்படும் போது முதன்மையான சாளரம் 55 | Головне вікно після завантаження файлу зображення 56 | 加载图片后的主窗口 57 | https://pineapple-pictures.sourceforge.io/ppic-gui-static.png 58 | 59 | 60 | Zooming in a raster image 61 | Ampliar una imagen rasterizada 62 | ラスター画像の拡大 63 | Inzoomen op een roosterafbeelding 64 | Масштабирование растрового изображения 65 | ராச்டர் படத்தில் பெரிதாக்குதல் 66 | Масштабування растрового зображення 67 | 放大浏览位图 68 | https://pineapple-pictures.sourceforge.io/ppic-zoom-raster.png 69 | 70 | 71 | Zooming in a vector image 72 | Ampliar una imagen vectorial 73 | ベクター画像の拡大 74 | Inzoomen op een vectorafbeelding 75 | Масштабирование векторного изображения 76 | ஒரு திசையன் படத்தில் பெரிதாக்குதல் 77 | Масштабування векторного зображення 78 | 放大浏览矢量图 79 | https://pineapple-pictures.sourceforge.io/ppic-zoom-svg.png 80 | 81 | 82 | 83 | 84 | 85 |

This release adds the following features:

86 |
    87 |
  • Support enforces windowed mode on start-up
  • 88 |
  • Reload image automatically when current image gets updated
  • 89 |
90 |

This release fixes the following bugs:

91 |
    92 |
  • Display correct text language on macOS
  • 93 |
94 |

This release includes the following changes:

95 |
    96 |
  • Use native text for shortcut editor's label
  • 97 |
  • Display native commandline message when possible
  • 98 |
  • Merge Qt translations into app applications as well
  • 99 |
100 |

With contributions from:

101 |

Heimen Stoffels, albanobattistella, mmahhi

102 |
103 |
104 | 105 | 106 |

This release fixes the following bug:

107 |
    108 |
  • Refer to the right exiv2 CMake module so it can be found on Linux
  • 109 |
110 |

This release includes the following changes:

111 |
    112 |
  • Convert DEP5 to REUSE.toml for better REUSE compliance
  • 113 |
  • Update translations
  • 114 |
115 |

With contributions from:

116 |

Pino Toscano, TamilNeram

117 |
118 |
119 | 120 | 121 |

This release adds the following features:

122 |
    123 |
  • Option to double-click to fullscreen
  • 124 |
  • Build-time option to embed translation resources
  • 125 |
126 |

This release fixes the following bugs:

127 |
    128 |
  • Fix window size not adjusted when open file on macOS
  • 129 |
  • Should center window according to available screen geometry
  • 130 |
131 |

This release includes the following changes:

132 |
    133 |
  • Change close window bahavior on macOS
  • 134 |
  • Update translations
  • 135 |
136 |

With contributions from:

137 |

albanobattistella, Sabri Ünal

138 |
139 |
140 | 141 | 142 |

This release adds the following features:

143 |
    144 |
  • Support custom shortcuts for existing actions
  • 145 |
  • Actions for frame-by-frame animated image playback support
  • 146 |
147 |

This release includes the following changes:

148 |
    149 |
  • Initial macOS bundle support
  • 150 |
  • bump minimum required CMake version to 3.16
  • 151 |
  • Update translations
  • 152 |
153 |

With contributions from:

154 |

albanobattistella, VenusGirl, gallegonovato, Sabri Ünal

155 |
156 |
157 | 158 | 159 |

This release fixes the following bug:

160 |
    161 |
  • Cannot load translations caused by a change in 0.8.2
  • 162 |
163 |
164 |
165 | 166 | 167 |

This release adds the following feature:

168 |
    169 |
  • New option to allow use light-color checkerboard by default
  • 170 |
171 |

With contributions from:

172 |

albanobattistella, mmahhi, gallegonovato

173 |
174 |
175 | 176 | 177 |

This release adds the following feature:

178 |
    179 |
  • New command line option to list all supported formats
  • 180 |
181 |

With contributions from:

182 |

albanobattistella, mmahhi, ovl-1, gallegonovato, Oğuz Ersen

183 |
184 |
185 | 186 | 187 |

This release adds the following feature:

188 |
    189 |
  • Support move image file to trash
  • 190 |
191 |

With contributions from:

192 |

albanobattistella, mmahhi, gallegonovato, Oğuz Ersen

193 |
194 |
195 | 196 | 197 |

This release adds the following feature:

198 |
    199 |
  • Add some icons for corresponding menu actions
  • 200 |
201 |

With contributions from:

202 |

Reza Almanda, mmahhi, Oğuz Ersen, volkov, Сергій

203 |
204 |
205 | 206 | 207 |

This release adds the following feature:

208 |
    209 |
  • Add "Keep transformation" to menu
  • 210 |
211 |

With contributions from:

212 |

mmahhi, VenusGirl, albanobattistella, gallegonovato, Heimen Stoffels

213 |
214 |
215 | 216 | 217 |

This release adds the following feature:

218 |
    219 |
  • Add an option in setting dialog to tweak the High-DPI scaling rounding policy (might only works in Qt 6 build)
  • 220 |
221 |

This release fixes the following bugs:

222 |
    223 |
  • Remove image size limit for Qt 6 build
  • 224 |
  • Fix application icon install location under Linux
  • 225 |
226 |

With contributions from:

227 |

Heimen Stoffels, Andrey, Dan, gallegonovato, albanobattistella, Sabri Ünal

228 |
229 |
230 | 231 | 232 |

This release adds the following features:

233 |
    234 |
  • TIF and TIFF format files in the same folder will now be automatically added to the gallery
  • 235 |
  • Built-in window resizing now also supports Linux desktop. (macOS might also works as well)
  • 236 |
237 |

This release fixes the following bugs:

238 |
    239 |
  • Settings dialog will automatedly use a suitable size instead of a hard-coded one
  • 240 |
  • Fix default configuration file location under Linux. (was `~/.config/config.ini`, now it's `~/.config/Pineapple Pictures/config.ini`)
  • 241 |
242 |

With contributions from:

243 |

yyc12345

244 |
245 |
246 |
247 | 248 |
249 | -------------------------------------------------------------------------------- /dist/appstream/po/net.blumia.pineapple-pictures.metainfo.es.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: PACKAGE VERSION\n" 4 | "POT-Creation-Date: 2023-08-22 18:49中国标准时间\n" 5 | "PO-Revision-Date: 2024-04-19 17:07+0000\n" 6 | "Last-Translator: gallegonovato \n" 7 | "Language-Team: Spanish \n" 9 | "Language: es\n" 10 | "MIME-Version: 1.0\n" 11 | "Content-Type: text/plain; charset=UTF-8\n" 12 | "Content-Transfer-Encoding: 8bit\n" 13 | "Plural-Forms: nplurals=2; plural=n != 1;\n" 14 | "X-Generator: Weblate 5.5-dev\n" 15 | 16 | #. (itstool) path: component/name 17 | #: net.blumia.pineapple-pictures.metainfo.xml:7 18 | msgid "Pineapple Pictures" 19 | msgstr "Pineapple Pictures" 20 | 21 | #. (itstool) path: component/summary 22 | #: net.blumia.pineapple-pictures.metainfo.xml:9 23 | msgid "Image Viewer" 24 | msgstr "Visor de imágenes" 25 | 26 | #. (itstool) path: description/p 27 | #: net.blumia.pineapple-pictures.metainfo.xml:12 28 | msgid "" 29 | "Pineapple Pictures is a lightweight and easy-to-use image viewer that comes " 30 | "with a handy navigation thumbnail when zoom-in, and doesn't contain any " 31 | "image management support." 32 | msgstr "" 33 | "Pineapple Pictures es un visor de imágenes ligero y fácil de usar que viene " 34 | "con una práctica miniatura de navegación al hacer zoom, y no contiene ningún " 35 | "soporte de gestión de imágenes." 36 | 37 | #. (itstool) path: screenshot/caption 38 | #: net.blumia.pineapple-pictures.metainfo.xml:17 39 | msgid "Main window when an image file is loaded" 40 | msgstr "Ventana principal cuando se carga un archivo de imagen" 41 | 42 | #. (itstool) path: screenshot/caption 43 | #: net.blumia.pineapple-pictures.metainfo.xml:22 44 | msgid "Zooming in a raster image" 45 | msgstr "Ampliar una imagen rasterizada" 46 | 47 | #. (itstool) path: screenshot/caption 48 | #: net.blumia.pineapple-pictures.metainfo.xml:27 49 | msgid "Zooming in a vector image" 50 | msgstr "Ampliar una imagen vectorial" 51 | 52 | #. (itstool) path: component/developer_name 53 | #: net.blumia.pineapple-pictures.metainfo.xml:34 54 | msgid "Gary (BLumia) Wang et al." 55 | msgstr "Gary (BLumia) Wang et al." 56 | -------------------------------------------------------------------------------- /dist/appstream/po/net.blumia.pineapple-pictures.metainfo.ja.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: PACKAGE VERSION\n" 4 | "POT-Creation-Date: 2023-08-22 18:49中国标准时间\n" 5 | "PO-Revision-Date: 2023-11-14 17:05+0000\n" 6 | "Last-Translator: mmahhi \n" 7 | "Language-Team: Japanese \n" 9 | "Language: ja\n" 10 | "MIME-Version: 1.0\n" 11 | "Content-Type: text/plain; charset=UTF-8\n" 12 | "Content-Transfer-Encoding: 8bit\n" 13 | "Plural-Forms: nplurals=1; plural=0;\n" 14 | "X-Generator: Weblate 5.2-dev\n" 15 | 16 | #. (itstool) path: component/name 17 | #: net.blumia.pineapple-pictures.metainfo.xml:7 18 | msgid "Pineapple Pictures" 19 | msgstr "Pineapple Pictures" 20 | 21 | #. (itstool) path: component/summary 22 | #: net.blumia.pineapple-pictures.metainfo.xml:9 23 | msgid "Image Viewer" 24 | msgstr "画像ビューアー" 25 | 26 | #. (itstool) path: description/p 27 | #: net.blumia.pineapple-pictures.metainfo.xml:12 28 | msgid "" 29 | "Pineapple Pictures is a lightweight and easy-to-use image viewer that comes " 30 | "with a handy navigation thumbnail when zoom-in, and doesn't contain any " 31 | "image management support." 32 | msgstr "" 33 | "Pineapple Picturesは、ズームイン時に便利なナビゲーションサムネイルを備えた軽" 34 | "量で使いやすい画像ビューアです。画像管理のサポートは含まれていません。" 35 | 36 | #. (itstool) path: screenshot/caption 37 | #: net.blumia.pineapple-pictures.metainfo.xml:17 38 | msgid "Main window when an image file is loaded" 39 | msgstr "画像ファイル読み込み時のメインウィンドウ" 40 | 41 | #. (itstool) path: screenshot/caption 42 | #: net.blumia.pineapple-pictures.metainfo.xml:22 43 | msgid "Zooming in a raster image" 44 | msgstr "ラスター画像の拡大" 45 | 46 | #. (itstool) path: screenshot/caption 47 | #: net.blumia.pineapple-pictures.metainfo.xml:27 48 | msgid "Zooming in a vector image" 49 | msgstr "ベクター画像の拡大" 50 | 51 | #. (itstool) path: component/developer_name 52 | #: net.blumia.pineapple-pictures.metainfo.xml:34 53 | msgid "Gary (BLumia) Wang et al." 54 | msgstr "Gary (BLumia) Wang など" 55 | -------------------------------------------------------------------------------- /dist/appstream/po/net.blumia.pineapple-pictures.metainfo.nl.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: PACKAGE VERSION\n" 4 | "POT-Creation-Date: 2023-08-22 18:49中国标准时间\n" 5 | "PO-Revision-Date: 2023-08-23 06:41+0000\n" 6 | "Last-Translator: Heimen Stoffels \n" 7 | "Language-Team: Dutch \n" 9 | "Language: nl\n" 10 | "MIME-Version: 1.0\n" 11 | "Content-Type: text/plain; charset=UTF-8\n" 12 | "Content-Transfer-Encoding: 8bit\n" 13 | "Plural-Forms: nplurals=2; plural=n != 1;\n" 14 | "X-Generator: Weblate 5.0-dev\n" 15 | 16 | #. (itstool) path: component/name 17 | #: net.blumia.pineapple-pictures.metainfo.xml:7 18 | msgid "Pineapple Pictures" 19 | msgstr "Pineapple Afbeeldingen" 20 | 21 | #. (itstool) path: component/summary 22 | #: net.blumia.pineapple-pictures.metainfo.xml:9 23 | msgid "Image Viewer" 24 | msgstr "Afbeeldingsweergave" 25 | 26 | #. (itstool) path: description/p 27 | #: net.blumia.pineapple-pictures.metainfo.xml:12 28 | msgid "" 29 | "Pineapple Pictures is a lightweight and easy-to-use image viewer that comes " 30 | "with a handy navigation thumbnail when zoom-in, and doesn't contain any " 31 | "image management support." 32 | msgstr "" 33 | "Pineapple Afbeeldingen is een licht en eenvoudig te gebruiken " 34 | "afbeeldingsweergaveprogramma met miniatuurnavigatie na inzoomen. Het " 35 | "programma heeft echter geen fotobeheermogelijkheid." 36 | 37 | #. (itstool) path: screenshot/caption 38 | #: net.blumia.pineapple-pictures.metainfo.xml:17 39 | msgid "Main window when an image file is loaded" 40 | msgstr "Hoofdvenster na het laden van een afbeelding" 41 | 42 | #. (itstool) path: screenshot/caption 43 | #: net.blumia.pineapple-pictures.metainfo.xml:22 44 | msgid "Zooming in a raster image" 45 | msgstr "Inzoomen op een roosterafbeelding" 46 | 47 | #. (itstool) path: screenshot/caption 48 | #: net.blumia.pineapple-pictures.metainfo.xml:27 49 | msgid "Zooming in a vector image" 50 | msgstr "Inzoomen op een vectorafbeelding" 51 | 52 | #. (itstool) path: component/developer_name 53 | #: net.blumia.pineapple-pictures.metainfo.xml:34 54 | msgid "Gary (BLumia) Wang et al." 55 | msgstr "Gary (BLumia) Wang e.a." 56 | -------------------------------------------------------------------------------- /dist/appstream/po/net.blumia.pineapple-pictures.metainfo.pot: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: PACKAGE VERSION\n" 4 | "POT-Creation-Date: 2023-08-22 18:49中国标准时间\n" 5 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 6 | "Last-Translator: FULL NAME \n" 7 | "Language-Team: LANGUAGE \n" 8 | "MIME-Version: 1.0\n" 9 | "Content-Type: text/plain; charset=UTF-8\n" 10 | "Content-Transfer-Encoding: 8bit\n" 11 | 12 | #. (itstool) path: component/name 13 | #: net.blumia.pineapple-pictures.metainfo.xml:7 14 | msgid "Pineapple Pictures" 15 | msgstr "" 16 | 17 | #. (itstool) path: component/summary 18 | #: net.blumia.pineapple-pictures.metainfo.xml:9 19 | msgid "Image Viewer" 20 | msgstr "" 21 | 22 | #. (itstool) path: description/p 23 | #: net.blumia.pineapple-pictures.metainfo.xml:12 24 | msgid "Pineapple Pictures is a lightweight and easy-to-use image viewer that comes with a handy navigation thumbnail when zoom-in, and doesn't contain any image management support." 25 | msgstr "" 26 | 27 | #. (itstool) path: screenshot/caption 28 | #: net.blumia.pineapple-pictures.metainfo.xml:17 29 | msgid "Main window when an image file is loaded" 30 | msgstr "" 31 | 32 | #. (itstool) path: screenshot/caption 33 | #: net.blumia.pineapple-pictures.metainfo.xml:22 34 | msgid "Zooming in a raster image" 35 | msgstr "" 36 | 37 | #. (itstool) path: screenshot/caption 38 | #: net.blumia.pineapple-pictures.metainfo.xml:27 39 | msgid "Zooming in a vector image" 40 | msgstr "" 41 | 42 | #. (itstool) path: component/developer_name 43 | #: net.blumia.pineapple-pictures.metainfo.xml:34 44 | msgid "Gary (BLumia) Wang et al." 45 | msgstr "" 46 | 47 | -------------------------------------------------------------------------------- /dist/appstream/po/net.blumia.pineapple-pictures.metainfo.ru.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: PACKAGE VERSION\n" 4 | "POT-Creation-Date: 2023-08-22 18:49中国标准时间\n" 5 | "PO-Revision-Date: 2023-08-23 06:41+0000\n" 6 | "Last-Translator: Andrey \n" 7 | "Language-Team: Russian \n" 9 | "Language: ru\n" 10 | "MIME-Version: 1.0\n" 11 | "Content-Type: text/plain; charset=UTF-8\n" 12 | "Content-Transfer-Encoding: 8bit\n" 13 | "Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" 14 | "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" 15 | "X-Generator: Weblate 5.0-dev\n" 16 | 17 | #. (itstool) path: component/name 18 | #: net.blumia.pineapple-pictures.metainfo.xml:7 19 | msgid "Pineapple Pictures" 20 | msgstr "Pineapple Pictures" 21 | 22 | #. (itstool) path: component/summary 23 | #: net.blumia.pineapple-pictures.metainfo.xml:9 24 | msgid "Image Viewer" 25 | msgstr "Просмотр изображений" 26 | 27 | #. (itstool) path: description/p 28 | #: net.blumia.pineapple-pictures.metainfo.xml:12 29 | msgid "" 30 | "Pineapple Pictures is a lightweight and easy-to-use image viewer that comes " 31 | "with a handy navigation thumbnail when zoom-in, and doesn't contain any " 32 | "image management support." 33 | msgstr "" 34 | "Pineapple Pictures - это легкий и простой в использовании просмотрщик " 35 | "изображений, оснащенный удобной навигацией по миниатюрам при увеличении " 36 | "масштаба и не содержащий никакой поддержки управления изображениями." 37 | 38 | #. (itstool) path: screenshot/caption 39 | #: net.blumia.pineapple-pictures.metainfo.xml:17 40 | msgid "Main window when an image file is loaded" 41 | msgstr "Основное окно после загрузки файла изображения" 42 | 43 | #. (itstool) path: screenshot/caption 44 | #: net.blumia.pineapple-pictures.metainfo.xml:22 45 | msgid "Zooming in a raster image" 46 | msgstr "Масштабирование растрового изображения" 47 | 48 | #. (itstool) path: screenshot/caption 49 | #: net.blumia.pineapple-pictures.metainfo.xml:27 50 | msgid "Zooming in a vector image" 51 | msgstr "Масштабирование векторного изображения" 52 | 53 | #. (itstool) path: component/developer_name 54 | #: net.blumia.pineapple-pictures.metainfo.xml:34 55 | msgid "Gary (BLumia) Wang et al." 56 | msgstr "Gary (BLumia) Wang et al." 57 | -------------------------------------------------------------------------------- /dist/appstream/po/net.blumia.pineapple-pictures.metainfo.ta.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: PACKAGE VERSION\n" 4 | "POT-Creation-Date: 2023-08-22 18:49中国标准时间\n" 5 | "PO-Revision-Date: 2025-01-28 09:01+0000\n" 6 | "Last-Translator: தமிழ்நேரம் \n" 7 | "Language-Team: Tamil \n" 9 | "Language: ta\n" 10 | "MIME-Version: 1.0\n" 11 | "Content-Type: text/plain; charset=UTF-8\n" 12 | "Content-Transfer-Encoding: 8bit\n" 13 | "Plural-Forms: nplurals=2; plural=n != 1;\n" 14 | "X-Generator: Weblate 5.10-dev\n" 15 | 16 | #. (itstool) path: component/name 17 | #: net.blumia.pineapple-pictures.metainfo.xml:7 18 | msgid "Pineapple Pictures" 19 | msgstr "அன்னாசி படங்கள்" 20 | 21 | #. (itstool) path: component/summary 22 | #: net.blumia.pineapple-pictures.metainfo.xml:9 23 | msgid "Image Viewer" 24 | msgstr "பட பார்வையாளர்" 25 | 26 | #. (itstool) path: description/p 27 | #: net.blumia.pineapple-pictures.metainfo.xml:12 28 | msgid "Pineapple Pictures is a lightweight and easy-to-use image viewer that comes with a handy navigation thumbnail when zoom-in, and doesn't contain any image management support." 29 | msgstr "" 30 | "அன்னாசி படங்கள் ஒரு இலகுரக மற்றும் பயன்படுத்த எளிதான பட பார்வையாளராகும், இது " 31 | "பெரிதாக்கும்போது ஒரு எளிமையான வழிசெலுத்தல் சிறுபடத்துடன் வருகிறது, மேலும் எந்த பட " 32 | "மேலாண்மை ஆதரவையும் கொண்டிருக்கவில்லை." 33 | 34 | #. (itstool) path: screenshot/caption 35 | #: net.blumia.pineapple-pictures.metainfo.xml:17 36 | msgid "Main window when an image file is loaded" 37 | msgstr "ஒரு படக் கோப்பு ஏற்றப்படும் போது முதன்மையான சாளரம்" 38 | 39 | #. (itstool) path: screenshot/caption 40 | #: net.blumia.pineapple-pictures.metainfo.xml:22 41 | msgid "Zooming in a raster image" 42 | msgstr "ராச்டர் படத்தில் பெரிதாக்குதல்" 43 | 44 | #. (itstool) path: screenshot/caption 45 | #: net.blumia.pineapple-pictures.metainfo.xml:27 46 | msgid "Zooming in a vector image" 47 | msgstr "ஒரு திசையன் படத்தில் பெரிதாக்குதல்" 48 | 49 | #. (itstool) path: component/developer_name 50 | #: net.blumia.pineapple-pictures.metainfo.xml:34 51 | msgid "Gary (BLumia) Wang et al." 52 | msgstr "கேரி (ப்ளூமியா) வாங் மற்றும் பலர்." 53 | -------------------------------------------------------------------------------- /dist/appstream/po/net.blumia.pineapple-pictures.metainfo.uk.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: PACKAGE VERSION\n" 4 | "POT-Creation-Date: 2023-08-22 18:49中国标准时间\n" 5 | "PO-Revision-Date: 2024-01-01 16:10+0000\n" 6 | "Last-Translator: Сергій \n" 7 | "Language-Team: Ukrainian \n" 9 | "Language: uk\n" 10 | "MIME-Version: 1.0\n" 11 | "Content-Type: text/plain; charset=UTF-8\n" 12 | "Content-Transfer-Encoding: 8bit\n" 13 | "Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && " 14 | "n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" 15 | "X-Generator: Weblate 5.4-dev\n" 16 | 17 | #. (itstool) path: component/name 18 | #: net.blumia.pineapple-pictures.metainfo.xml:7 19 | msgid "Pineapple Pictures" 20 | msgstr "Pineapple Pictures" 21 | 22 | #. (itstool) path: component/summary 23 | #: net.blumia.pineapple-pictures.metainfo.xml:9 24 | msgid "Image Viewer" 25 | msgstr "Переглядач зображень" 26 | 27 | #. (itstool) path: description/p 28 | #: net.blumia.pineapple-pictures.metainfo.xml:12 29 | msgid "" 30 | "Pineapple Pictures is a lightweight and easy-to-use image viewer that comes " 31 | "with a handy navigation thumbnail when zoom-in, and doesn't contain any " 32 | "image management support." 33 | msgstr "" 34 | "Pineapple Pictures – це легкий і простий у використанні переглядач " 35 | "зображень, який постачається зі зручною навігаційною мініатюрою при " 36 | "збільшенні масштабу і не містить жодної підтримки керування зображеннями." 37 | 38 | #. (itstool) path: screenshot/caption 39 | #: net.blumia.pineapple-pictures.metainfo.xml:17 40 | msgid "Main window when an image file is loaded" 41 | msgstr "Головне вікно після завантаження файлу зображення" 42 | 43 | #. (itstool) path: screenshot/caption 44 | #: net.blumia.pineapple-pictures.metainfo.xml:22 45 | msgid "Zooming in a raster image" 46 | msgstr "Масштабування растрового зображення" 47 | 48 | #. (itstool) path: screenshot/caption 49 | #: net.blumia.pineapple-pictures.metainfo.xml:27 50 | msgid "Zooming in a vector image" 51 | msgstr "Масштабування векторного зображення" 52 | 53 | #. (itstool) path: component/developer_name 54 | #: net.blumia.pineapple-pictures.metainfo.xml:34 55 | msgid "Gary (BLumia) Wang et al." 56 | msgstr "Gary (BLumia) Wang." 57 | -------------------------------------------------------------------------------- /dist/appstream/po/net.blumia.pineapple-pictures.metainfo.zh_CN.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: \n" 4 | "POT-Creation-Date: 2023-08-22 18:49中国标准时间\n" 5 | "PO-Revision-Date: 2023-08-22 18:22+0800\n" 6 | "Last-Translator: \n" 7 | "Language-Team: \n" 8 | "Language: zh_CN\n" 9 | "MIME-Version: 1.0\n" 10 | "Content-Type: text/plain; charset=UTF-8\n" 11 | "Content-Transfer-Encoding: 8bit\n" 12 | "X-Generator: Poedit 3.3.2\n" 13 | 14 | #. (itstool) path: component/name 15 | #: net.blumia.pineapple-pictures.metainfo.xml:7 16 | msgid "Pineapple Pictures" 17 | msgstr "菠萝看图" 18 | 19 | #. (itstool) path: component/summary 20 | #: net.blumia.pineapple-pictures.metainfo.xml:9 21 | msgid "Image Viewer" 22 | msgstr "图像查看器" 23 | 24 | #. (itstool) path: description/p 25 | #: net.blumia.pineapple-pictures.metainfo.xml:12 26 | msgid "" 27 | "Pineapple Pictures is a lightweight and easy-to-use image viewer that comes " 28 | "with a handy navigation thumbnail when zoom-in, and doesn't contain any " 29 | "image management support." 30 | msgstr "" 31 | "菠萝看图是一个轻量级易用的图像查看器,在图片放大时提供了方便的鸟瞰导航功能," 32 | "且不包含任何图片管理功能。" 33 | 34 | #. (itstool) path: screenshot/caption 35 | #: net.blumia.pineapple-pictures.metainfo.xml:17 36 | msgid "Main window when an image file is loaded" 37 | msgstr "加载图片后的主窗口" 38 | 39 | #. (itstool) path: screenshot/caption 40 | #: net.blumia.pineapple-pictures.metainfo.xml:22 41 | msgid "Zooming in a raster image" 42 | msgstr "放大浏览位图" 43 | 44 | #. (itstool) path: screenshot/caption 45 | #: net.blumia.pineapple-pictures.metainfo.xml:27 46 | msgid "Zooming in a vector image" 47 | msgstr "放大浏览矢量图" 48 | 49 | #. (itstool) path: component/developer_name 50 | #: net.blumia.pineapple-pictures.metainfo.xml:34 51 | msgid "Gary (BLumia) Wang et al." 52 | msgstr "Gary (BLumia) Wang 等人" 53 | -------------------------------------------------------------------------------- /dist/net.blumia.pineapple-pictures.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Categories=Graphics; 3 | Comment=Pineapple Pictures is a lightweight image viewer 4 | Comment[zh_CN]=菠萝看图是一个轻量级的图像查看器 5 | Exec=ppic %F 6 | GenericName=Image Viewer 7 | GenericName[zh_CN]=图像查看器 8 | Icon=net.blumia.pineapple-pictures 9 | Keywords=Picture;Image;Viewer;Jpg;Jpeg;Png; 10 | MimeType=image/bmp;image/bmp24;image/jpg;image/jpe;image/jpeg;image/jpeg24;image/jng;image/pcd;image/pcx;image/png;image/tif;image/tiff;image/tiff24;image/dds;image/gif;image/sgi;image/j2k;image/jp2;image/pct;image/wdp;image/arw;image/icb;image/dng;image/vda;image/vst;image/svg;image/ptif;image/mef;image/xbm;image/svg+xml; 11 | Name=Pineapple Pictures 12 | Name[zh_CN]=菠萝看图 13 | StartupNotify=false 14 | Type=Application 15 | Terminal=false 16 | -------------------------------------------------------------------------------- /pineapple-pictures.pro: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 Gary Wang 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | QT += core widgets gui svg svgwidgets 6 | 7 | TARGET = ppic 8 | TEMPLATE = app 9 | DEFINES += PPIC_VERSION_STRING=\\\"x.y.z\\\" 10 | 11 | win32 { 12 | DEFINES += FLAG_PORTABLE_MODE_SUPPORT=1 13 | } 14 | 15 | # The following define makes your compiler emit warnings if you use 16 | # any feature of Qt which has been marked as deprecated (the exact warnings 17 | # depend on your compiler). Please consult the documentation of the 18 | # deprecated API in order to know how to port your code away from it. 19 | DEFINES += QT_DEPRECATED_WARNINGS 20 | 21 | # You can also make your code fail to compile if you use deprecated APIs. 22 | # In order to do so, uncomment the following line. 23 | # You can also select to disable deprecated APIs only up to a certain version of Qt. 24 | #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 25 | 26 | CONFIG += c++17 lrelease embed_translations 27 | 28 | SOURCES += \ 29 | app/aboutdialog.cpp \ 30 | app/main.cpp \ 31 | app/framelesswindow.cpp \ 32 | app/mainwindow.cpp \ 33 | app/graphicsview.cpp \ 34 | app/bottombuttongroup.cpp \ 35 | app/graphicsscene.cpp \ 36 | app/navigatorview.cpp \ 37 | app/opacityhelper.cpp \ 38 | app/toolbutton.cpp \ 39 | app/settings.cpp \ 40 | app/settingsdialog.cpp \ 41 | app/metadatamodel.cpp \ 42 | app/metadatadialog.cpp \ 43 | app/exiv2wrapper.cpp \ 44 | app/actionmanager.cpp \ 45 | app/playlistmanager.cpp \ 46 | app/shortcutedit.cpp \ 47 | app/fileopeneventhandler.cpp 48 | 49 | HEADERS += \ 50 | app/aboutdialog.h \ 51 | app/framelesswindow.h \ 52 | app/mainwindow.h \ 53 | app/graphicsview.h \ 54 | app/bottombuttongroup.h \ 55 | app/graphicsscene.h \ 56 | app/navigatorview.h \ 57 | app/opacityhelper.h \ 58 | app/toolbutton.h \ 59 | app/settings.h \ 60 | app/settingsdialog.h \ 61 | app/metadatamodel.h \ 62 | app/metadatadialog.h \ 63 | app/exiv2wrapper.h \ 64 | app/actionmanager.h \ 65 | app/playlistmanager.h \ 66 | app/shortcutedit.h \ 67 | app/fileopeneventhandler.h 68 | 69 | TRANSLATIONS = \ 70 | app/translations/PineapplePictures_en.ts \ 71 | app/translations/PineapplePictures_zh_CN.ts \ 72 | app/translations/PineapplePictures_de.ts \ 73 | app/translations/PineapplePictures_es.ts \ 74 | app/translations/PineapplePictures_fr.ts \ 75 | app/translations/PineapplePictures_nb_NO.ts \ 76 | app/translations/PineapplePictures_nl.ts \ 77 | app/translations/PineapplePictures_ru.ts \ 78 | app/translations/PineapplePictures_si.ts \ 79 | app/translations/PineapplePictures_id.ts 80 | 81 | # Default rules for deployment. 82 | qnx: target.path = /tmp/$${TARGET}/bin 83 | else: unix:!android: target.path = /opt/$${TARGET}/bin 84 | !isEmpty(target.path): INSTALLS += target 85 | 86 | RESOURCES += \ 87 | assets/resources.qrc 88 | 89 | # Generate from svg: 90 | # magick convert -density 512x512 -background none app-icon.svg -define icon:auto-resize app-icon.ico 91 | RC_ICONS = assets/icons/app-icon.ico 92 | 93 | # Windows only, for rc file (we're not going to use the .rc file in this repo) 94 | QMAKE_TARGET_PRODUCT = Pineapple Pictures 95 | QMAKE_TARGET_DESCRIPTION = Pineapple Pictures - Image Viewer 96 | QMAKE_TARGET_COPYRIGHT = MIT/Expat License - Copyright (C) 2024 Gary Wang 97 | 98 | # MSVC only, since QMake doesn't have a CMAKE_CXX_STANDARD_LIBRARIES or cpp_winlibs similar thing 99 | win32-msvc* { 100 | LIBS += -luser32 101 | } 102 | --------------------------------------------------------------------------------