Compare commits
51
Commits
1.4.1
..
f55f8c731d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f55f8c731d | ||
|
|
4ed491c78a | ||
|
|
b54cde2cbf | ||
|
|
b1d12cc406 | ||
|
|
aa13355966 | ||
|
|
bd35a8c3e0 | ||
|
|
81413b51da | ||
|
|
a01d640c27 | ||
|
|
16800df138 | ||
|
|
7e8fd4ea21 | ||
|
|
04fa491cd8 | ||
|
|
dbf8c09368 | ||
|
|
369a17ed7d | ||
|
|
b19a4f8723 | ||
|
|
a8e5464e9f | ||
|
|
46f46480cb | ||
|
|
c06f900ac5 | ||
|
|
31303a20c7 | ||
|
|
319fd64fc6 | ||
|
|
cdd0b764a7 | ||
|
|
2a69482adb | ||
|
|
fc3ef1cd7f | ||
|
|
1de1517a81 | ||
|
|
04a4ab9bff | ||
|
|
011c1e858f | ||
|
|
4fe09757b3 | ||
|
|
82f1f10cca | ||
|
|
60eebea741 | ||
|
|
9b151e2fea | ||
|
|
9db9c3e9f7 | ||
|
|
9a18a41b8f | ||
|
|
c93ed94210 | ||
|
|
f4d272e014 | ||
|
|
d536e8c611 | ||
|
|
c8dfccb460 | ||
|
|
6fde9474ae | ||
|
|
c19fcaa560 | ||
|
|
602e2cccf8 | ||
|
|
3c1a3c54a7 | ||
|
|
032752faf9 | ||
|
|
457647c59f | ||
|
|
821e0f9fd6 | ||
|
|
26f51e418d | ||
|
|
cd2eac44f1 | ||
|
|
f0bb945c7d | ||
|
|
870e472f53 | ||
|
|
76677381de | ||
|
|
d2bd521ad0 | ||
|
|
265e894b3f | ||
|
|
6339a93c6c | ||
|
|
6da53ced81 |
@@ -1,35 +0,0 @@
|
||||
name: deepin 25 CI
|
||||
|
||||
on: [push, pull_request, workflow_dispatch]
|
||||
|
||||
jobs:
|
||||
build-deepin-25-deb:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: linuxdeepin/deepin:crimson
|
||||
options: --user root
|
||||
|
||||
steps:
|
||||
- name: Update package lists and install basic tools
|
||||
run: |
|
||||
apt update
|
||||
apt install -y git ca-certificates curl file
|
||||
- uses: actions/checkout@v6
|
||||
- name: Get build dept.
|
||||
run: |
|
||||
apt install -y cmake qt6-base-dev qt6-svg-dev qt6-tools-dev libexiv2-dev
|
||||
- name: Build it
|
||||
run: |
|
||||
git config --global --add safe.directory '*' # to avoid "fatal: detected dubious ownership in repository at xxx"
|
||||
cmake -Bbuild -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr .
|
||||
cmake --build build -j
|
||||
cd build
|
||||
cpack -G DEB
|
||||
- name: Try install it
|
||||
run: |
|
||||
cd build
|
||||
apt install -y ./*.deb
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: deepin-25-deb-package
|
||||
path: build/*.deb
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
ren .\exiv2-0.28.7-2022msvc-AMD64 dependencies_bin
|
||||
echo ::endgroup::
|
||||
echo ::group::===== zlib =====
|
||||
curl -fsSL -o zlib_src.zip https://zlib.net/zlib132.zip
|
||||
curl -fsSL -o zlib_src.zip https://github.com/madler/zlib/releases/download/v1.3.2/zlib132.zip
|
||||
7z x zlib_src.zip -y -o"dependencies_src"
|
||||
ren .\dependencies_src\zlib-1.3.2 zlib || goto :error
|
||||
cmake ./dependencies_src/zlib -Bbuild_dependencies/zlib -DCMAKE_INSTALL_PREFIX="dependencies_bin" || goto :error
|
||||
@@ -125,10 +125,6 @@ jobs:
|
||||
cmake --build build --config Release
|
||||
cmake --install build --config Release --prefix "%PWD%\build"
|
||||
echo ::endgroup::
|
||||
echo ::group::===== passoc stuff =====
|
||||
curl -fsSL -o passoc-standalone.zip https://github.com/BLumia/pineapple-assoc-manager/releases/latest/download/pineapple-assoc-manager-qt${{ matrix.qt_ver }}-standalone.zip
|
||||
7z x passoc-standalone.zip passoc.exe -y -o"build/bin"
|
||||
echo ::endgroup::
|
||||
:: ------ pkg ------
|
||||
windeployqt --verbose=2 --no-compiler-runtime --no-quick-import --no-translations --no-network --no-opengl-sw --no-system-d3d-compiler --no-system-dxc-compiler --skip-plugin-types tls,networkinformation build\bin\ppic.exe
|
||||
robocopy ./dependencies_bin/bin build/bin *.dll
|
||||
|
||||
+22
-16
@@ -4,7 +4,7 @@
|
||||
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
project(pineapple-pictures VERSION 1.4.1) # don't forget to update NEWS file and AppStream metadata.
|
||||
project(pineapple-pictures VERSION 1.5.0) # don't forget to update NEWS file and AppStream metadata.
|
||||
|
||||
include(GNUInstallDirs)
|
||||
include(FeatureSummary)
|
||||
@@ -16,10 +16,11 @@ set (CMAKE_CXX_STANDARD 17)
|
||||
set (CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set (CMAKE_AUTOMOC ON)
|
||||
set (CMAKE_AUTORCC ON)
|
||||
set (CMAKE_AUTOUIC ON)
|
||||
|
||||
find_package(QT NAMES Qt6 REQUIRED COMPONENTS Core)
|
||||
|
||||
set (QT_MINIMUM_VERSION "6.4")
|
||||
set (QT_MINIMUM_VERSION "6.5")
|
||||
|
||||
find_package(Qt${QT_VERSION_MAJOR} ${QT_MINIMUM_VERSION} REQUIRED
|
||||
COMPONENTS Widgets Svg SvgWidgets LinguistTools
|
||||
@@ -38,15 +39,12 @@ endif ()
|
||||
|
||||
set (PPIC_CPP_FILES
|
||||
app/main.cpp
|
||||
app/framelesswindow.cpp
|
||||
app/mainwindow.cpp
|
||||
app/actionmanager.cpp
|
||||
app/graphicsview.cpp
|
||||
app/graphicsscene.cpp
|
||||
app/bottombuttongroup.cpp
|
||||
app/navigatorview.cpp
|
||||
app/opacityhelper.cpp
|
||||
app/toolbutton.cpp
|
||||
app/settings.cpp
|
||||
app/settingsdialog.cpp
|
||||
app/aboutdialog.cpp
|
||||
@@ -54,20 +52,19 @@ set (PPIC_CPP_FILES
|
||||
app/metadatadialog.cpp
|
||||
app/exiv2wrapper.cpp
|
||||
app/playlistmanager.cpp
|
||||
app/shortcutedit.cpp
|
||||
app/fileopeneventhandler.cpp
|
||||
app/playlistscout.cpp
|
||||
app/playlistscouts/qtscout.cpp
|
||||
app/playlistscouts/explorerscout.cpp
|
||||
)
|
||||
|
||||
set (PPIC_HEADER_FILES
|
||||
app/framelesswindow.h
|
||||
app/mainwindow.h
|
||||
app/actionmanager.h
|
||||
app/graphicsview.h
|
||||
app/graphicsscene.h
|
||||
app/bottombuttongroup.h
|
||||
app/navigatorview.h
|
||||
app/opacityhelper.h
|
||||
app/toolbutton.h
|
||||
app/settings.h
|
||||
app/settingsdialog.h
|
||||
app/aboutdialog.h
|
||||
@@ -75,8 +72,17 @@ set (PPIC_HEADER_FILES
|
||||
app/metadatadialog.h
|
||||
app/exiv2wrapper.h
|
||||
app/playlistmanager.h
|
||||
app/shortcutedit.h
|
||||
app/fileopeneventhandler.h
|
||||
app/playlistscout.h
|
||||
app/playlistscouts/qtscout.h
|
||||
app/playlistscouts/explorerscout.h
|
||||
)
|
||||
|
||||
set (PPIC_UI_FILES
|
||||
app/settingsdialog.ui
|
||||
app/aboutdialog.ui
|
||||
app/metadatadialog.ui
|
||||
app/mainwindow.ui
|
||||
)
|
||||
|
||||
set (PPIC_QRC_FILES
|
||||
@@ -101,6 +107,7 @@ endif ()
|
||||
add_executable (${EXE_NAME}
|
||||
${PPIC_HEADER_FILES}
|
||||
${PPIC_CPP_FILES}
|
||||
${PPIC_UI_FILES}
|
||||
${PPIC_QRC_FILES}
|
||||
${PPIC_RC_FILES}
|
||||
)
|
||||
@@ -117,6 +124,11 @@ else()
|
||||
qt_add_translations(${EXE_NAME} ${ADD_TRANSLATIONS_ADDITIONAL_ARGS} TS_FILES ${PPIC_TS_FILES} QM_FILES_OUTPUT_VARIABLE PPIC_QM_FILES)
|
||||
endif()
|
||||
|
||||
# YYC MARK:
|
||||
# This is essential, otherwise Qt UIC generated UI header file
|
||||
# can not find our header files if there are custom widgets inside UI file.
|
||||
target_include_directories(${EXE_NAME} PRIVATE app/)
|
||||
|
||||
target_sources(${EXE_NAME} PRIVATE ${PPIC_QM_FILES})
|
||||
|
||||
target_link_libraries (${EXE_NAME} Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Svg Qt${QT_VERSION_MAJOR}::SvgWidgets)
|
||||
@@ -271,12 +283,6 @@ if (DEFINED QM_FILE_INSTALL_DIR)
|
||||
endif()
|
||||
|
||||
if (WIN32)
|
||||
install(
|
||||
FILES dist/passoc/ppic.pacfg
|
||||
DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
RENAME default-assoc.pacfg
|
||||
)
|
||||
|
||||
install(
|
||||
FILES
|
||||
dist/passoc/generic.ico
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
Version 1.5.0
|
||||
~~~~~~~~~~~~~
|
||||
Released: 2026-07-04
|
||||
|
||||
Features:
|
||||
* Introduced an optional titlebar that display file name and allows drag and double-click to move and toggle maximize state
|
||||
* Windows: Now the application will use the system's built-in animation for open window animation
|
||||
|
||||
Bugfixes:
|
||||
* Show empty url list should also reset window title to blank
|
||||
|
||||
Miscellaneous:
|
||||
* Update translations
|
||||
* Add vcs-browser link to AppStream metadata
|
||||
|
||||
Contributors:
|
||||
Heimen Stoffels (vistaus), VenusGirl (VenusGirl)
|
||||
|
||||
Version 1.4.1
|
||||
~~~~~~~~~~~~~
|
||||
Released: 2026-04-05
|
||||
|
||||
@@ -1,4 +1,17 @@
|
||||
Yet another image viewer.
|
||||
# Sarasas Chip Workshop Picture
|
||||
|
||||
Yet another legacy Windows Photo Viewer with cross-platform support.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> SarasaCW Picture are still in refactoring. Please do not use it until this message removes.
|
||||
|
||||
## Summary
|
||||
|
||||
Sarasas Chip Workshop Picture (abbr. SarasaCW Picture) is a lightweight image viewer that has cross-platform support and similar user interface about legacy Windows Photo Viewer. It allows you view JPEG, PNG, GIF, SVG, PSD, KRA, XCF, TGA, HDR, AVIF and some other frequently used image formats files quickly and easily without changing your long-term Windows usage experience.
|
||||
|
||||
SarasaCW Picture is the **hard fork** of [Pineapple Pictures](https://github.com/BLumia/pineapple-pictures) since Pineapple Pictures 1.4.1 version.
|
||||
|
||||
<!--
|
||||
|
||||
|CI|Build Status|
|
||||
|---|---|
|
||||
@@ -8,10 +21,6 @@ Yet another image viewer.
|
||||
|
||||

|
||||
|
||||
## Summary
|
||||
|
||||
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.
|
||||
|
||||
## Get it!
|
||||
|
||||
### Maintained by the original author
|
||||
@@ -71,3 +80,5 @@ It's possible to build it under Windows, Linux, macOS, and maybe other desktop p
|
||||
## License
|
||||
|
||||
Pineapple Pictures as a whole is licensed under MIT license. Individual files may have a different, but compatible license.
|
||||
|
||||
-->
|
||||
|
||||
+17
-4
@@ -1,3 +1,18 @@
|
||||
# Sarasas Chip Workshop 照片查看器
|
||||
|
||||
简单轻量的、具有跨平台支持的、经典样式的 Windows 照片查看器。
|
||||
|
||||
> [!IMPORTANT]
|
||||
> SarasaCW 照片查看器还在重构中。在这条信息被移除之前请不要使用它。
|
||||
|
||||
## 简介
|
||||
|
||||
Sarasas Chip Workshop 照片查看器(简称 SarasaCW 照片查看器)是一个轻量级的图片查看器。支持跨平台。界面风格类似于经典的 Windows 照片查看器。它可以让你快速轻松地浏览 JPEG、PNG、GIF、SVG、PSD、KRA、XCF、TGA、HDR、AVIF 以及其他一些常用的图片格式,且无需改变你长期的 Windows 使用习惯。
|
||||
|
||||
SarasaCW 照片查看器是[菠萝看图](https://github.com/BLumia/pineapple-pictures)的**硬分叉**,该分叉自菠萝看图 1.4.1 版本开始。
|
||||
|
||||
<!--
|
||||
|
||||
简单轻量的跨平台看图工具。
|
||||
|
||||
|CI|构建状态|
|
||||
@@ -8,10 +23,6 @@
|
||||
|
||||

|
||||
|
||||
## 简介
|
||||
|
||||
菠萝看图是一个轻量图像查看器,允许你简单快捷的查看 JPEG, PNG, GIF, SVG, PSD, KRA, XCF, TGA, HDR, AVIF 等常用格式的图像文件,并提供了置顶窗口的选项以便你在使用其它软件时也可以将参考图片固定在顶端。
|
||||
|
||||
## 立即获取!
|
||||
|
||||
### 由原作者维护
|
||||
@@ -72,3 +83,5 @@ $ cmake --build . # 如果你使用 Makefile 作为 CMake 生成器,也可以
|
||||
## 许可协议
|
||||
|
||||
菠萝看图整体使用 MIT 协议进行发布。项目所随的部分源文件可能具备不同但与之兼容的许可协议。
|
||||
|
||||
-->
|
||||
|
||||
+62
-144
@@ -3,176 +3,94 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "aboutdialog.h"
|
||||
#include "ui_aboutdialog.h"
|
||||
|
||||
#include <QAbstractButton>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QTextBrowser>
|
||||
#include <QTabWidget>
|
||||
#include <QVBoxLayout>
|
||||
#include <QApplication>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QFile>
|
||||
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
static QString loadEmbeddedHtml(QString&& path) {
|
||||
QFile htmlFile(path);
|
||||
bool canOpenFile = htmlFile.open(QIODevice::ReadOnly);
|
||||
QByteArray htmlRawContent = canOpenFile ? htmlFile.readAll() : QByteArrayLiteral("");
|
||||
return QString::fromUtf8(htmlRawContent);
|
||||
}
|
||||
|
||||
AboutDialog::AboutDialog(QWidget *parent)
|
||||
: QDialog(parent)
|
||||
, m_tabWidget(new QTabWidget)
|
||||
, m_buttonBox(new QDialogButtonBox)
|
||||
, m_helpTextEdit(new QTextBrowser)
|
||||
, m_aboutTextEdit(new QTextBrowser)
|
||||
, m_specialThanksTextEdit(new QTextBrowser)
|
||||
, m_licenseTextEdit(new QTextBrowser)
|
||||
, m_3rdPartyLibsTextEdit(new QTextBrowser)
|
||||
: QDialog(parent), ui(new QT_PREPEND_NAMESPACE(Ui::AboutDialog))
|
||||
{
|
||||
this->setWindowTitle(tr("About"));
|
||||
ui->setupUi(this);
|
||||
setWindowFlag(Qt::WindowContextHelpButtonHint, false);
|
||||
|
||||
const QStringList helpStr {
|
||||
u"<p>%1</p>"_s.arg(tr("Launch application with image file path as argument to load the file.")),
|
||||
u"<p>%1</p>"_s.arg(tr("Drag and drop image file onto the window is also supported.")),
|
||||
u"<p>%1</p>"_s.arg(tr("None of the operations in this application will alter the pictures on disk.")),
|
||||
u"<p>%1</p>"_s.arg(tr("Context menu option explanation:")),
|
||||
u"<ul>"_s,
|
||||
// blumia: Chain two arg() here since it seems lupdate will remove one of them if we use
|
||||
// the old `arg(QCoreApp::translate(), tr())` way, but it's worth to mention
|
||||
// `arg(QCoreApp::translate(), this->tr())` works, but lupdate will complain about the usage.
|
||||
u"<li><b>%1</b>:<br/>%2</li>"_s
|
||||
.arg(QCoreApplication::translate("MainWindow", "Stay on top"))
|
||||
.arg(tr("Make window stay on top of all other windows.")),
|
||||
u"<li><b>%1</b>:<br/>%2</li>"_s
|
||||
.arg(QCoreApplication::translate("MainWindow", "Protected mode"))
|
||||
.arg(tr("Avoid close window accidentally. (eg. by double clicking the window)")),
|
||||
u"<li><b>%1</b>:<br/>%2</li>"_s
|
||||
.arg(QCoreApplication::translate("MainWindow", "Keep transformation", "The 'transformation' means the flip/rotation status that currently applied to the image view"))
|
||||
.arg(tr("Avoid resetting the zoom/rotation/flip state that was applied to the image view when switching between images.")),
|
||||
u"</ul>"_s
|
||||
};
|
||||
// blumia: Chain two arg() here since it seems lupdate will remove one of them if we use
|
||||
// the old `arg(QCoreApp::translate(), tr())` way, but it's worth to mention
|
||||
// `arg(QCoreApp::translate(), this->tr())` works, but lupdate will complain about the usage.
|
||||
const QString HELP_HTML = loadEmbeddedHtml(u":/plain/help.html"_s)
|
||||
.arg(tr("General Help"))
|
||||
.arg(tr("Launch application with image file path as argument to load the file."))
|
||||
.arg(tr("Passing in a single image file will load all the images in that directory and start viewing from that image."))
|
||||
.arg(tr("Passing in multiple image files lets you view them one by one."))
|
||||
.arg(tr("Drag and drop image file onto the window is also supported."))
|
||||
.arg(tr("None of the operations in this application will alter the pictures on disk."))
|
||||
.arg(tr("Context Menu Help"))
|
||||
.arg(tr("Keep transformation", "The same meaning of MainWindow's Keep transformation."))
|
||||
.arg(tr("Avoid resetting the zoom/rotation/flip state that was applied to the image view when switching between images."))
|
||||
.arg(tr("Checkerboard", "The same meaning of MainWindow's Checkerboard."))
|
||||
.arg(tr("Checkerboard light and dark theme can be adjusted in the configuration window, or easily switch it by holding Shift and clicking the button."));
|
||||
|
||||
const QStringList aboutStr {
|
||||
u"<center><img width='128' height='128' src=':/icons/app-icon.svg'/><br/>"_s,
|
||||
qApp->applicationDisplayName(),
|
||||
(u"<br/>"_s + tr("Version: %1").arg(
|
||||
const QString APP_NAME_ARG = qApp->applicationDisplayName();
|
||||
const QString APP_VERSION_ARG =
|
||||
#ifdef GIT_DESCRIBE_VERSION_STRING
|
||||
GIT_DESCRIBE_VERSION_STRING
|
||||
GIT_DESCRIBE_VERSION_STRING;
|
||||
#else
|
||||
qApp->applicationVersion()
|
||||
qApp->applicationVersion();
|
||||
#endif // GIT_DESCRIBE_VERSION_STRING
|
||||
)),
|
||||
u"<hr/>"_s,
|
||||
tr("Copyright (c) %1 %2", "%1 is year, %2 is the name of copyright holder(s)")
|
||||
.arg(u"2026"_s, u"<a href='https://github.com/BLumia'>@BLumia</a>"_s),
|
||||
u"<br/>"_s,
|
||||
tr("Logo designed by %1").arg(u"<a href='https://github.com/Lovelyblack'>@Lovelyblack</a>"_s),
|
||||
u"<hr/>"_s,
|
||||
tr("Built with Qt %1 (%2)").arg(QT_VERSION_STR, QSysInfo::buildCpuArchitecture()),
|
||||
QStringLiteral("<br/><a href='%1'>%2</a>").arg("https://github.com/BLumia/pineapple-pictures", tr("Source code")),
|
||||
u"</center>"_s
|
||||
};
|
||||
const QString QT_VERSION_ARG = QT_VERSION_STR;
|
||||
const QString QT_ARCH_ARG = QSysInfo::buildCpuArchitecture();
|
||||
const QString ABOUT_HTML = loadEmbeddedHtml(u":/plain/about.html"_s)
|
||||
.arg(APP_NAME_ARG)
|
||||
.arg(tr("Version: %1").arg(APP_VERSION_ARG))
|
||||
.arg(tr("Upstream author"))
|
||||
.arg(tr("Hard fork author"))
|
||||
.arg(tr("Logo designer"))
|
||||
.arg(tr("Built with Qt %1 (%2)").arg(QT_VERSION_ARG).arg(QT_ARCH_ARG))
|
||||
.arg(tr("Source code"));
|
||||
|
||||
QFile translaterHtml(u":/plain/translators.html"_s);
|
||||
bool canOpenFile = translaterHtml.open(QIODevice::ReadOnly);
|
||||
const QByteArray & translatorList = canOpenFile ? translaterHtml.readAll() : QByteArrayLiteral("");
|
||||
const QString SPECIAL_THANKS_HTML = loadEmbeddedHtml(u":/plain/special-thanks.html"_s)
|
||||
.arg(tr("Contributors"))
|
||||
.arg(tr("List of contributors on GitHub"))
|
||||
.arg(tr("Thanks to all people who contributed to this project."))
|
||||
.arg(tr("Translators"))
|
||||
.arg(tr("I would like to thank the following people who volunteered to translate this application."));
|
||||
|
||||
const QStringList specialThanksStr {
|
||||
u"<h1 align='center'>%1</h1><a href='%2'>%3</a><p>%4</p>"_s.arg(
|
||||
tr("Contributors"),
|
||||
u"https://github.com/BLumia/pineapple-pictures/graphs/contributors"_s,
|
||||
tr("List of contributors on GitHub"),
|
||||
tr("Thanks to all people who contributed to this project.")
|
||||
),
|
||||
const QString LICENSE_HTML = loadEmbeddedHtml(u":/plain/license.html"_s)
|
||||
.arg(tr("License"))
|
||||
.arg(tr("This application is released under the MIT License."))
|
||||
.arg(tr("License Verbatim Copy"));
|
||||
|
||||
u"<h1 align='center'>%1</h1><p>%2</p>%3"_s.arg(
|
||||
tr("Translators"),
|
||||
tr("I would like to thank the following people who volunteered to translate this application."),
|
||||
translatorList
|
||||
)
|
||||
};
|
||||
const QString THIRD_PARTY_LIBS_HTML = loadEmbeddedHtml(u":/plain/third-party-libs.html"_s)
|
||||
.arg(tr("Third-party Libraries"))
|
||||
.arg(tr("This application is built on the following free software libraries.", "Free as in freedom"))
|
||||
.arg(tr("Some of them are optional and might not be included with this distribution depending on the build environment."));
|
||||
|
||||
const QStringList licenseStr {
|
||||
u"<h1 align='center'><b>%1</b></h1>"_s.arg(tr("Your Rights")),
|
||||
u"<p>%1</p><p>%2</p><ul><li>%3</li><li>%4</li><li>%5</li><li>%6</li></ul>"_s.arg(
|
||||
tr("%1 is released under the MIT License."), // %1
|
||||
tr("This license grants people a number of freedoms:"), // %2
|
||||
tr("You are free to use %1, for any purpose"), // %3
|
||||
tr("You are free to distribute %1"), // %4
|
||||
tr("You can study how %1 works and change it"), // %5
|
||||
tr("You can distribute changed versions of %1") // %6
|
||||
).arg(u"<i>%1</i>"_s),
|
||||
u"<p>%1</p>"_s.arg(tr("The MIT license guarantees you this freedom. Nobody is ever permitted to take it away.")),
|
||||
u"<hr/><pre>%2</pre>"_s
|
||||
};
|
||||
ui->m_helpTextEdit->setHtml(HELP_HTML);
|
||||
ui->m_aboutTextEdit->setHtml(ABOUT_HTML);
|
||||
ui->m_specialThanksTextEdit->setHtml(SPECIAL_THANKS_HTML);
|
||||
ui->m_licenseTextEdit->setHtml(LICENSE_HTML);
|
||||
ui->m_3rdPartyLibsTextEdit->setHtml(THIRD_PARTY_LIBS_HTML);
|
||||
|
||||
const QString mitLicense(QStringLiteral(R"(Expat/MIT License
|
||||
|
||||
Copyright © 2026 BLumia
|
||||
|
||||
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:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
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.
|
||||
)"));
|
||||
|
||||
const QStringList thirdPartyLibsStr {
|
||||
u"<h1 align='center'><b>%1</b></h1>"_s.arg(tr("Third-party Libraries used by %1")),
|
||||
tr("%1 is built on the following free software libraries:", "Free as in freedom"),
|
||||
u"<ul>"_s,
|
||||
#ifdef HAVE_EXIV2_VERSION
|
||||
u"<li><a href='%1'>%2</a>: %3</li>"_s.arg("https://www.exiv2.org/", "Exiv2", "GPLv2"),
|
||||
#endif // EXIV2_VERSION
|
||||
u"<li><a href='%1'>%2</a>: %3</li>"_s.arg("https://www.qt.io/", "Qt", "GPLv2 + GPLv3 + LGPLv2.1 + LGPLv3"),
|
||||
u"</ul>"_s
|
||||
};
|
||||
|
||||
m_helpTextEdit->setText(helpStr.join('\n'));
|
||||
|
||||
m_aboutTextEdit->setText(aboutStr.join('\n'));
|
||||
m_aboutTextEdit->setOpenExternalLinks(true);
|
||||
|
||||
m_specialThanksTextEdit->setText(specialThanksStr.join('\n'));
|
||||
m_specialThanksTextEdit->setOpenExternalLinks(true);
|
||||
|
||||
m_licenseTextEdit->setText(licenseStr.join('\n').arg(qApp->applicationDisplayName(), mitLicense));
|
||||
|
||||
m_3rdPartyLibsTextEdit->setText(thirdPartyLibsStr.join('\n').arg(u"<i>%1</i>"_s).arg(qApp->applicationDisplayName()));
|
||||
m_3rdPartyLibsTextEdit->setOpenExternalLinks(true);
|
||||
|
||||
m_tabWidget->addTab(m_helpTextEdit, tr("&Help"));
|
||||
m_tabWidget->addTab(m_aboutTextEdit, tr("&About"));
|
||||
m_tabWidget->addTab(m_specialThanksTextEdit, tr("&Special Thanks"));
|
||||
m_tabWidget->addTab(m_licenseTextEdit, tr("&License"));
|
||||
m_tabWidget->addTab(m_3rdPartyLibsTextEdit, tr("&Third-party Libraries"));
|
||||
|
||||
m_buttonBox->setStandardButtons(QDialogButtonBox::Close);
|
||||
connect(m_buttonBox, QOverload<QAbstractButton *>::of(&QDialogButtonBox::clicked), this, [this](){
|
||||
connect(ui->m_buttonBox, QOverload<QAbstractButton *>::of(&QDialogButtonBox::clicked), this, [this](QAbstractButton * btn){
|
||||
Q_UNUSED(btn)
|
||||
this->close();
|
||||
});
|
||||
|
||||
setLayout(new QVBoxLayout);
|
||||
|
||||
layout()->addWidget(m_tabWidget);
|
||||
layout()->addWidget(m_buttonBox);
|
||||
|
||||
setMinimumSize(361, 161); // not sure why it complain "Unable to set geometry"
|
||||
setWindowFlag(Qt::WindowContextHelpButtonHint, false);
|
||||
}
|
||||
|
||||
AboutDialog::~AboutDialog()
|
||||
{
|
||||
|
||||
delete ui;
|
||||
}
|
||||
|
||||
QSize AboutDialog::sizeHint() const
|
||||
|
||||
+4
-11
@@ -8,9 +8,9 @@
|
||||
#include <QDialog>
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
class QTextBrowser;
|
||||
class QTabWidget;
|
||||
class QDialogButtonBox;
|
||||
namespace Ui {
|
||||
class AboutDialog;
|
||||
}
|
||||
QT_END_NAMESPACE
|
||||
|
||||
class AboutDialog : public QDialog
|
||||
@@ -23,14 +23,7 @@ public:
|
||||
QSize sizeHint() const override;
|
||||
|
||||
private:
|
||||
QTabWidget * m_tabWidget = nullptr;
|
||||
QDialogButtonBox * m_buttonBox = nullptr;
|
||||
|
||||
QTextBrowser * m_helpTextEdit = nullptr;
|
||||
QTextBrowser * m_aboutTextEdit = nullptr;
|
||||
QTextBrowser * m_specialThanksTextEdit = nullptr;
|
||||
QTextBrowser * m_licenseTextEdit = nullptr;
|
||||
QTextBrowser * m_3rdPartyLibsTextEdit = nullptr;
|
||||
QT_PREPEND_NAMESPACE(Ui::AboutDialog) *ui;
|
||||
};
|
||||
|
||||
#endif // ABOUTDIALOG_H
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>AboutDialog</class>
|
||||
<widget class="QDialog" name="AboutDialog">
|
||||
<property name="windowModality">
|
||||
<enum>Qt::NonModal</enum>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>About</string>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>361</width>
|
||||
<height>161</height>
|
||||
</size>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QTabWidget" name="m_tabWidget">
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="helpTab">
|
||||
<attribute name="title">
|
||||
<string>&Help</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="helpTabLayout">
|
||||
<item>
|
||||
<widget class="QTextBrowser" name="m_helpTextEdit">
|
||||
<property name="openExternalLinks">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="aboutTab">
|
||||
<attribute name="title">
|
||||
<string>&About</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="aboutTabLayout">
|
||||
<item>
|
||||
<widget class="QTextBrowser" name="m_aboutTextEdit">
|
||||
<property name="openExternalLinks">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="specialThanksTab">
|
||||
<attribute name="title">
|
||||
<string>&Special Thanks</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="specialThanksTabLayout">
|
||||
<item>
|
||||
<widget class="QTextBrowser" name="m_specialThanksTextEdit">
|
||||
<property name="openExternalLinks">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="licenseTab">
|
||||
<attribute name="title">
|
||||
<string>&License</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="licenseTabLayout">
|
||||
<item>
|
||||
<widget class="QTextBrowser" name="m_licenseTextEdit">
|
||||
<property name="openExternalLinks">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="thirdPartyLibsTab">
|
||||
<attribute name="title">
|
||||
<string>&Third-party Libraries</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="thirdPartyLibsTabLayout">
|
||||
<item>
|
||||
<widget class="QTextBrowser" name="m_3rdPartyLibsTextEdit">
|
||||
<property name="openExternalLinks">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="m_buttonBox">
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Close</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
+139
-83
@@ -4,19 +4,17 @@
|
||||
|
||||
#include "actionmanager.h"
|
||||
|
||||
#include "settings.h"
|
||||
#include "mainwindow.h"
|
||||
#include "playlistmanager.h"
|
||||
#include "graphicsview.h"
|
||||
#include "graphicsscene.h"
|
||||
|
||||
#include <QGuiApplication>
|
||||
#include <QSvgRenderer>
|
||||
#include <QPainter>
|
||||
|
||||
#define ICON_NAME(name)\
|
||||
QStringLiteral(":/icons/" #name ".svg")
|
||||
|
||||
#define ACTION_NAME(s) QStringLiteral(STRIFY(s))
|
||||
#define STRIFY(s) #s
|
||||
|
||||
QIcon ActionManager::loadHidpiIcon(const QString &resp, QSize sz)
|
||||
static QIcon loadHidpiIcon(const QString &resp, QSize sz = QSize(32, 32))
|
||||
{
|
||||
QSvgRenderer r(resp);
|
||||
QPixmap pm = QPixmap(sz * qApp->devicePixelRatio());
|
||||
@@ -29,130 +27,188 @@ QIcon ActionManager::loadHidpiIcon(const QString &resp, QSize sz)
|
||||
|
||||
void ActionManager::setupAction(MainWindow *mainWindow)
|
||||
{
|
||||
// region: Action and Its Icon Creation
|
||||
|
||||
// Declare some convenient macros and lambdas.
|
||||
|
||||
#define ICON_NAME(name)\
|
||||
QStringLiteral(":/icons/" #name ".svg")
|
||||
|
||||
#define ACTION_NAME(s) QStringLiteral(STRIFY(s))
|
||||
#define STRIFY(s) #s
|
||||
|
||||
auto create_action = [] (QWidget *w, QAction **a, QString i, QString an, bool iconFromTheme = false) {
|
||||
*a = new QAction(w);
|
||||
if (!i.isNull())
|
||||
(*a)->setIcon(iconFromTheme ? QIcon::fromTheme(i) : ActionManager::loadHidpiIcon(i));
|
||||
(*a)->setIcon(iconFromTheme ? QIcon::fromTheme(i) : loadHidpiIcon(i));
|
||||
(*a)->setObjectName(an);
|
||||
w->addAction(*a);
|
||||
};
|
||||
#define CREATE_NEW_ICON_ACTION(w, a, i) create_action(w, &a, ICON_NAME(i), ACTION_NAME(a))
|
||||
|
||||
/// This macro for register normal action.
|
||||
#define CREATE_NEW_ACTION(w, a) create_action(w, &a, QString(), ACTION_NAME(a))
|
||||
/// This macro for register Qt theme action.
|
||||
#define CREATE_NEW_THEMEICON_ACTION(w, a, i) create_action(w, &a, QLatin1String(STRIFY(i)), ACTION_NAME(a), true)
|
||||
/// This macro for register action with our custom icon.
|
||||
#define CREATE_NEW_ICON_ACTION(w, a, i) create_action(w, &a, ICON_NAME(i), ACTION_NAME(a), false)
|
||||
|
||||
// TODO: We may replace all Qt theme icons to our custom icon for visual consistency.
|
||||
// TODO: Some icons are invalid.
|
||||
|
||||
CREATE_NEW_ACTION(mainWindow, actionCopyPixmap);
|
||||
CREATE_NEW_ACTION(mainWindow, actionCopyFilePath);
|
||||
|
||||
CREATE_NEW_ICON_ACTION(mainWindow, actionPrevPicture, go-previous);
|
||||
CREATE_NEW_ICON_ACTION(mainWindow, actionNextPicture, go-next);
|
||||
|
||||
CREATE_NEW_ICON_ACTION(mainWindow, actionActualSize, zoom-original);
|
||||
CREATE_NEW_ICON_ACTION(mainWindow, actionToggleMaximize, view-fullscreen);
|
||||
CREATE_NEW_ICON_ACTION(mainWindow, actionFitToScreen, app-icon);
|
||||
CREATE_NEW_ICON_ACTION(mainWindow, actionFitByWidth, app-icon);
|
||||
CREATE_NEW_ICON_ACTION(mainWindow, actionFitByHeight, app-icon);
|
||||
CREATE_NEW_ICON_ACTION(mainWindow, actionZoomIn, zoom-in);
|
||||
CREATE_NEW_ICON_ACTION(mainWindow, actionZoomOut, zoom-out);
|
||||
CREATE_NEW_ICON_ACTION(mainWindow, actionToggleCheckerboard, view-background-checkerboard);
|
||||
|
||||
CREATE_NEW_ICON_ACTION(mainWindow, actionFlipHorizontal, app-icon);
|
||||
CREATE_NEW_ICON_ACTION(mainWindow, actionFlipVertical, app-icon);
|
||||
CREATE_NEW_ICON_ACTION(mainWindow, actionRotateClockwise, object-rotate-right);
|
||||
#undef CREATE_NEW_ICON_ACTION
|
||||
|
||||
#define CREATE_NEW_ACTION(w, a) create_action(w, &a, QString(), ACTION_NAME(a))
|
||||
#define CREATE_NEW_THEMEICON_ACTION(w, a, i) create_action(w, &a, QLatin1String(STRIFY(i)), ACTION_NAME(a), true)
|
||||
CREATE_NEW_ACTION(mainWindow, actionRotateCounterClockwise);
|
||||
CREATE_NEW_ACTION(mainWindow, actionPrevPicture);
|
||||
CREATE_NEW_ACTION(mainWindow, actionNextPicture);
|
||||
CREATE_NEW_ICON_ACTION(mainWindow, actionRotateCounterClockwise, app-icon);
|
||||
|
||||
CREATE_NEW_ACTION(mainWindow, actionToggleAvoidResetTransform);
|
||||
CREATE_NEW_ICON_ACTION(mainWindow, actionToggleCheckerboard, view-background-checkerboard);
|
||||
CREATE_NEW_ACTION(mainWindow, actionTogglePauseAnimation);
|
||||
CREATE_NEW_ACTION(mainWindow, actionAnimationNextFrame);
|
||||
|
||||
CREATE_NEW_THEMEICON_ACTION(mainWindow, actionOpen, document-open);
|
||||
CREATE_NEW_THEMEICON_ACTION(mainWindow, actionSaveAs, document-save-as);
|
||||
CREATE_NEW_ACTION(mainWindow, actionHorizontalFlip);
|
||||
CREATE_NEW_ACTION(mainWindow, actionFitInView);
|
||||
CREATE_NEW_ACTION(mainWindow, actionFitByWidth);
|
||||
CREATE_NEW_ACTION(mainWindow, actionFitLongImage);
|
||||
CREATE_NEW_THEMEICON_ACTION(mainWindow, actionCopyPixmap, edit-copy);
|
||||
CREATE_NEW_ACTION(mainWindow, actionCopyFilePath);
|
||||
CREATE_NEW_THEMEICON_ACTION(mainWindow, actionPaste, edit-paste);
|
||||
CREATE_NEW_THEMEICON_ACTION(mainWindow, actionTrash, edit-delete);
|
||||
CREATE_NEW_ACTION(mainWindow, actionToggleStayOnTop);
|
||||
CREATE_NEW_ACTION(mainWindow, actionToggleProtectMode);
|
||||
CREATE_NEW_ACTION(mainWindow, actionToggleAvoidResetTransform);
|
||||
CREATE_NEW_ACTION(mainWindow, actionSettings);
|
||||
CREATE_NEW_THEMEICON_ACTION(mainWindow, actionHelp, system-help);
|
||||
CREATE_NEW_THEMEICON_ACTION(mainWindow, actionLocateInFileManager, system-file-manager);
|
||||
CREATE_NEW_THEMEICON_ACTION(mainWindow, actionProperties, document-properties);
|
||||
CREATE_NEW_ACTION(mainWindow, actionQuitApp);
|
||||
#undef CREATE_NEW_ACTION
|
||||
|
||||
CREATE_NEW_ACTION(mainWindow, actionSettings);
|
||||
CREATE_NEW_THEMEICON_ACTION(mainWindow, actionHelp, system-help);
|
||||
|
||||
// Remove convenient macros
|
||||
|
||||
#undef CREATE_NEW_ICON_ACTION
|
||||
#undef CREATE_NEW_THEMEICON_ACTION
|
||||
#undef CREATE_NEW_ACTION
|
||||
|
||||
retranslateUi(mainWindow);
|
||||
#undef STRIFY
|
||||
#undef ACTION_NAME
|
||||
#undef ICON_NAME
|
||||
|
||||
QMetaObject::connectSlotsByName(mainWindow);
|
||||
}
|
||||
// endregion
|
||||
|
||||
void ActionManager::retranslateUi(MainWindow *mainWindow)
|
||||
{
|
||||
Q_UNUSED(mainWindow);
|
||||
// region: Setup Checkable Action
|
||||
|
||||
actionOpen->setText(QCoreApplication::translate("MainWindow", "&Open...", nullptr));
|
||||
actionSaveAs->setText(QCoreApplication::translate("MainWindow", "Save &As...", nullptr));
|
||||
actionToggleAvoidResetTransform->setCheckable(true);
|
||||
actionToggleCheckerboard->setCheckable(true);
|
||||
actionTogglePauseAnimation->setCheckable(true);
|
||||
|
||||
actionActualSize->setText(QCoreApplication::translate("MainWindow", "Actual size", nullptr));
|
||||
actionToggleMaximize->setText(QCoreApplication::translate("MainWindow", "Toggle maximize", nullptr));
|
||||
actionZoomIn->setText(QCoreApplication::translate("MainWindow", "Zoom in", nullptr));
|
||||
actionZoomOut->setText(QCoreApplication::translate("MainWindow", "Zoom out", nullptr));
|
||||
actionToggleCheckerboard->setText(QCoreApplication::translate("MainWindow", "Toggle Checkerboard", nullptr));
|
||||
actionRotateClockwise->setText(QCoreApplication::translate("MainWindow", "Rotate right", nullptr));
|
||||
actionRotateCounterClockwise->setText(QCoreApplication::translate("MainWindow", "Rotate left", nullptr));
|
||||
// endregion
|
||||
|
||||
// region: Action Translatable Text Setup
|
||||
|
||||
actionCopyPixmap->setText(QCoreApplication::translate("MainWindow", "Copy P&ixmap", nullptr));
|
||||
actionCopyFilePath->setText(QCoreApplication::translate("MainWindow", "Copy &File Path", nullptr));
|
||||
|
||||
actionPrevPicture->setText(QCoreApplication::translate("MainWindow", "Previous image", nullptr));
|
||||
actionNextPicture->setText(QCoreApplication::translate("MainWindow", "Next image", nullptr));
|
||||
|
||||
actionActualSize->setText(QCoreApplication::translate("MainWindow", "Actual size", nullptr));
|
||||
actionFitToScreen->setText(QCoreApplication::translate("MainWindow", "Fit to screen", nullptr));
|
||||
actionFitByWidth->setText(QCoreApplication::translate("MainWindow", "Fit by width", nullptr));
|
||||
actionFitByHeight->setText(QCoreApplication::translate("MainWindow", "Fit by height", nullptr));
|
||||
actionZoomIn->setText(QCoreApplication::translate("MainWindow", "Zoom in", nullptr));
|
||||
actionZoomOut->setText(QCoreApplication::translate("MainWindow", "Zoom out", nullptr));
|
||||
|
||||
actionFlipHorizontal->setText(QCoreApplication::translate("MainWindow", "Flip &Horizontally", nullptr));
|
||||
actionFlipVertical->setText(QCoreApplication::translate("MainWindow", "Flip &Vertically", nullptr));
|
||||
actionRotateClockwise->setText(QCoreApplication::translate("MainWindow", "Rotate right", nullptr));
|
||||
actionRotateCounterClockwise->setText(QCoreApplication::translate("MainWindow", "Rotate left", nullptr));
|
||||
|
||||
actionToggleAvoidResetTransform->setText(QCoreApplication::translate("MainWindow", "Keep transformation", "The 'transformation' means the flip/rotation status that currently applied to the image view"));
|
||||
actionToggleCheckerboard->setText(QCoreApplication::translate("MainWindow", "Toggle Checkerboard", nullptr));
|
||||
actionTogglePauseAnimation->setText(QCoreApplication::translate("MainWindow", "Pause/Resume Animation", nullptr));
|
||||
actionAnimationNextFrame->setText(QCoreApplication::translate("MainWindow", "Animation Go to Next Frame", nullptr));
|
||||
|
||||
actionHorizontalFlip->setText(QCoreApplication::translate("MainWindow", "Flip &Horizontally", nullptr));
|
||||
actionFitInView->setText(QCoreApplication::translate("MainWindow", "Fit to view", nullptr));
|
||||
actionFitByWidth->setText(QCoreApplication::translate("MainWindow", "Fit to width", nullptr));
|
||||
actionFitLongImage->setText(QCoreApplication::translate("MainWindow", "Fit long image", nullptr));
|
||||
actionCopyPixmap->setText(QCoreApplication::translate("MainWindow", "Copy P&ixmap", nullptr));
|
||||
actionCopyFilePath->setText(QCoreApplication::translate("MainWindow", "Copy &File Path", nullptr));
|
||||
actionPaste->setText(QCoreApplication::translate("MainWindow", "&Paste", nullptr));
|
||||
actionTrash->setText(QCoreApplication::translate("MainWindow", "Move to Trash", nullptr));
|
||||
actionToggleStayOnTop->setText(QCoreApplication::translate("MainWindow", "Stay on top", nullptr));
|
||||
actionToggleProtectMode->setText(QCoreApplication::translate("MainWindow", "Protected mode", nullptr));
|
||||
actionToggleAvoidResetTransform->setText(QCoreApplication::translate("MainWindow", "Keep transformation", "The 'transformation' means the flip/rotation status that currently applied to the image view"));
|
||||
actionSettings->setText(QCoreApplication::translate("MainWindow", "Configure...", nullptr));
|
||||
actionHelp->setText(QCoreApplication::translate("MainWindow", "Help", nullptr));
|
||||
#ifdef Q_OS_WIN
|
||||
actionLocateInFileManager->setText(
|
||||
QCoreApplication::translate(
|
||||
"MainWindow", "Show in File Explorer",
|
||||
"File Explorer is the name of explorer.exe under Windows"
|
||||
)
|
||||
);
|
||||
)
|
||||
);
|
||||
#else
|
||||
actionLocateInFileManager->setText(QCoreApplication::translate("MainWindow", "Show in directory", nullptr));
|
||||
#endif // Q_OS_WIN
|
||||
actionProperties->setText(QCoreApplication::translate("MainWindow", "Properties", nullptr));
|
||||
actionQuitApp->setText(QCoreApplication::translate("MainWindow", "Quit", nullptr));
|
||||
|
||||
actionSettings->setText(QCoreApplication::translate("MainWindow", "Configure...", nullptr));
|
||||
actionHelp->setText(QCoreApplication::translate("MainWindow", "Help", nullptr));
|
||||
// endregion
|
||||
|
||||
QMetaObject::connectSlotsByName(mainWindow);
|
||||
}
|
||||
|
||||
void ActionManager::setupShortcuts()
|
||||
{
|
||||
actionOpen->setShortcut(QKeySequence::Open);
|
||||
actionSaveAs->setShortcut(QKeySequence::SaveAs);
|
||||
actionCopyPixmap->setShortcut(QKeySequence::Copy);
|
||||
|
||||
actionPrevPicture->setShortcuts({
|
||||
QKeySequence(Qt::Key_Left),
|
||||
QKeySequence(Qt::Key_PageUp),
|
||||
});
|
||||
actionNextPicture->setShortcuts({
|
||||
QKeySequence(Qt::Key_Right),
|
||||
QKeySequence(Qt::Key_PageDown),
|
||||
});
|
||||
|
||||
actionActualSize->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_0));
|
||||
actionZoomIn->setShortcut(QKeySequence::ZoomIn);
|
||||
actionZoomOut->setShortcut(QKeySequence::ZoomOut);
|
||||
actionPrevPicture->setShortcuts({
|
||||
QKeySequence(Qt::Key_PageUp),
|
||||
QKeySequence(Qt::Key_Left),
|
||||
});
|
||||
actionNextPicture->setShortcuts({
|
||||
QKeySequence(Qt::Key_PageDown),
|
||||
QKeySequence(Qt::Key_Right),
|
||||
});
|
||||
actionHorizontalFlip->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_R));
|
||||
actionCopyPixmap->setShortcut(QKeySequence::Copy);
|
||||
actionPaste->setShortcut(QKeySequence::Paste);
|
||||
|
||||
actionTrash->setShortcut(QKeySequence::Delete);
|
||||
actionHelp->setShortcut(QKeySequence::HelpContents);
|
||||
actionSettings->setShortcut(QKeySequence::Preferences);
|
||||
actionProperties->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_I));
|
||||
actionQuitApp->setShortcuts({
|
||||
QKeySequence(Qt::Key_Space),
|
||||
QKeySequence(Qt::Key_Escape)
|
||||
});
|
||||
|
||||
actionSettings->setShortcut(QKeySequence::Preferences);
|
||||
actionHelp->setShortcut(QKeySequence::HelpContents);
|
||||
}
|
||||
|
||||
void ActionManager::updateActionState(PlaylistManager* pm, GraphicsView* gv)
|
||||
{
|
||||
const auto* gs = gv->scene();
|
||||
const auto* settings = Settings::instance();
|
||||
|
||||
// Update copy actions
|
||||
bool canCopy = pm->currentIndex().has_value();
|
||||
actionCopyPixmap->setEnabled(canCopy);
|
||||
actionCopyFilePath->setEnabled(canCopy);
|
||||
|
||||
// Update Prev/Next Picture State
|
||||
const int galleryFileCount = pm->totalCount();
|
||||
const bool loopGallery = settings->loopGallery();
|
||||
actionPrevPicture->setEnabled(galleryFileCount > 1 && (loopGallery || !pm->isFirstIndex()));
|
||||
actionNextPicture->setEnabled(galleryFileCount > 1 && (loopGallery || !pm->isLastIndex()));
|
||||
|
||||
// Update Avoid Reset Transform
|
||||
actionToggleAvoidResetTransform->setChecked(gv->isAvoidResetTransform());
|
||||
// Update Checkerboard
|
||||
actionToggleCheckerboard->setChecked(gv->isCheckerboard());
|
||||
// Update Animation Graphics Action
|
||||
bool canPauseAnimation = gs->canPauseAnimation();
|
||||
actionTogglePauseAnimation->setEnabled(canPauseAnimation);
|
||||
if (canPauseAnimation) {
|
||||
actionTogglePauseAnimation->setChecked(gs->isPauseAnimation());
|
||||
} else {
|
||||
actionTogglePauseAnimation->setChecked(false);
|
||||
}
|
||||
bool canSkipAnimation = gs->canSkipAnimationFrame();
|
||||
actionAnimationNextFrame->setEnabled(canSkipAnimation);
|
||||
|
||||
// Update trash, located in explorer and properties.
|
||||
bool hasLoadedFile = pm->currentIndex().has_value();
|
||||
actionTrash->setEnabled(hasLoadedFile);
|
||||
actionLocateInFileManager->setEnabled(hasLoadedFile);
|
||||
actionProperties->setEnabled(hasLoadedFile);
|
||||
}
|
||||
|
||||
|
||||
+25
-25
@@ -8,7 +8,11 @@
|
||||
#include <QAction>
|
||||
|
||||
class MainWindow;
|
||||
class PlaylistManager;
|
||||
class GraphicsView;
|
||||
|
||||
/// Action Manager is separated from Main Window
|
||||
/// to manage all actions specifically.
|
||||
class ActionManager
|
||||
{
|
||||
public:
|
||||
@@ -16,45 +20,41 @@ public:
|
||||
~ActionManager() = default;
|
||||
|
||||
void setupAction(MainWindow * mainWindow);
|
||||
void retranslateUi(MainWindow *MainWindow);
|
||||
void setupShortcuts();
|
||||
|
||||
static QIcon loadHidpiIcon(const QString &resp, QSize sz = QSize(32, 32));
|
||||
public slots:
|
||||
void updateActionState(PlaylistManager* pm, GraphicsView* gv);
|
||||
|
||||
public:
|
||||
QAction *actionOpen;
|
||||
QAction *actionSaveAs;
|
||||
|
||||
QAction *actionActualSize;
|
||||
QAction *actionToggleMaximize;
|
||||
QAction *actionZoomIn;
|
||||
QAction *actionZoomOut;
|
||||
QAction *actionToggleCheckerboard;
|
||||
QAction *actionRotateClockwise;
|
||||
QAction *actionRotateCounterClockwise;
|
||||
QAction *actionCopyPixmap;
|
||||
QAction *actionCopyFilePath;
|
||||
|
||||
QAction *actionPrevPicture;
|
||||
QAction *actionNextPicture;
|
||||
|
||||
QAction *actionActualSize;
|
||||
QAction *actionFitToScreen;
|
||||
QAction *actionFitByWidth;
|
||||
QAction *actionFitByHeight;
|
||||
QAction *actionZoomIn;
|
||||
QAction *actionZoomOut;
|
||||
|
||||
QAction *actionFlipHorizontal;
|
||||
QAction *actionFlipVertical;
|
||||
QAction *actionRotateClockwise;
|
||||
QAction *actionRotateCounterClockwise;
|
||||
|
||||
QAction *actionToggleAvoidResetTransform;
|
||||
QAction *actionToggleCheckerboard;
|
||||
QAction *actionTogglePauseAnimation;
|
||||
QAction *actionAnimationNextFrame;
|
||||
|
||||
QAction *actionHorizontalFlip;
|
||||
QAction *actionFitInView;
|
||||
QAction *actionFitByWidth;
|
||||
QAction *actionFitLongImage;
|
||||
QAction *actionCopyPixmap;
|
||||
QAction *actionCopyFilePath;
|
||||
QAction *actionPaste;
|
||||
QAction *actionTrash;
|
||||
QAction *actionToggleStayOnTop;
|
||||
QAction *actionToggleProtectMode;
|
||||
QAction *actionToggleAvoidResetTransform;
|
||||
QAction *actionSettings;
|
||||
QAction *actionHelp;
|
||||
QAction *actionLocateInFileManager;
|
||||
QAction *actionProperties;
|
||||
QAction *actionQuitApp;
|
||||
|
||||
QAction *actionSettings;
|
||||
QAction *actionHelp;
|
||||
};
|
||||
|
||||
#endif // ACTIONMANAGER_H
|
||||
|
||||
+21
-33
@@ -4,54 +4,42 @@
|
||||
|
||||
#include "bottombuttongroup.h"
|
||||
|
||||
#include "opacityhelper.h"
|
||||
|
||||
#include <QToolButton>
|
||||
#include <QVBoxLayout>
|
||||
#include <QDebug>
|
||||
|
||||
BottomButtonGroup::BottomButtonGroup(const std::vector<QAction *> &actionList, QWidget *parent)
|
||||
BottomButtonGroup::BottomButtonGroup(QWidget *parent)
|
||||
: QGroupBox (parent)
|
||||
, m_opacityHelper(new OpacityHelper(this))
|
||||
{
|
||||
QHBoxLayout * mainLayout = new QHBoxLayout(this);
|
||||
mainLayout->setSizeConstraint(QLayout::SetFixedSize);
|
||||
this->setLayout(mainLayout);
|
||||
this->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
|
||||
this->setStyleSheet("BottomButtonGroup {"
|
||||
"border: 1px solid gray;"
|
||||
"border-top-left-radius: 10px;"
|
||||
"border-top-right-radius: 10px;"
|
||||
"border-style: none;"
|
||||
"background-color:rgba(0,0,0,120)"
|
||||
"}"
|
||||
"QToolButton {"
|
||||
"background:transparent;"
|
||||
"}"
|
||||
"QToolButton:!focus {"
|
||||
"border-style: none;"
|
||||
"}");
|
||||
this->setStyleSheet(R"#(
|
||||
BottomButtonGroup {
|
||||
border-style: none;
|
||||
background: transparent;
|
||||
}
|
||||
QToolButton {
|
||||
background: transparent;
|
||||
border-style: none;
|
||||
}
|
||||
)#");
|
||||
}
|
||||
|
||||
auto newActionBtn = [this](QAction * action) -> QToolButton * {
|
||||
void BottomButtonGroup::addActions(const std::vector<QAction *> & actionList) {
|
||||
for (QAction * action : actionList) {
|
||||
// Create button
|
||||
QToolButton * btn = new QToolButton(this);
|
||||
btn->setDefaultAction(action);
|
||||
btn->setIconSize(QSize(32, 32));
|
||||
btn->setFixedSize(40, 40);
|
||||
return btn;
|
||||
};
|
||||
|
||||
for (QAction * action : actionList) {
|
||||
addButton(newActionBtn(action));
|
||||
// Add button
|
||||
layout()->addWidget(btn);
|
||||
}
|
||||
}
|
||||
|
||||
void BottomButtonGroup::setOpacity(qreal opacity, bool animated)
|
||||
{
|
||||
m_opacityHelper->setOpacity(opacity, animated);
|
||||
}
|
||||
|
||||
void BottomButtonGroup::addButton(QAbstractButton *button)
|
||||
{
|
||||
layout()->addWidget(button);
|
||||
updateGeometry();
|
||||
}
|
||||
|
||||
void BottomButtonGroup::addAction(QAction* action) {
|
||||
addActions({action});
|
||||
}
|
||||
|
||||
@@ -10,18 +10,14 @@
|
||||
#include <QAbstractButton>
|
||||
#include <QGroupBox>
|
||||
|
||||
class OpacityHelper;
|
||||
class BottomButtonGroup : public QGroupBox
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit BottomButtonGroup(const std::vector<QAction *> & actionList, QWidget *parent = nullptr);
|
||||
explicit BottomButtonGroup(QWidget *parent = nullptr);
|
||||
|
||||
void setOpacity(qreal opacity, bool animated = true);
|
||||
void addButton(QAbstractButton *button);
|
||||
|
||||
private:
|
||||
OpacityHelper * m_opacityHelper;
|
||||
void addActions(const std::vector<QAction *> & actionList);
|
||||
void addAction(QAction* action);
|
||||
};
|
||||
|
||||
#endif // BOTTOMBUTTONGROUP_H
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2022 Gary Wang <wzc782970009@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2023 Tad Young <yyc12321@outlook.com>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "framelesswindow.h"
|
||||
|
||||
#include <QMouseEvent>
|
||||
#include <QHoverEvent>
|
||||
#include <QApplication>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWindow>
|
||||
|
||||
FramelessWindow::FramelessWindow(QWidget *parent)
|
||||
: QWidget(parent)
|
||||
, m_centralLayout(new QVBoxLayout(this))
|
||||
, m_oldCursorShape(Qt::ArrowCursor)
|
||||
, m_oldEdges()
|
||||
{
|
||||
this->setWindowFlags(Qt::Window | Qt::FramelessWindowHint | Qt::WindowMinMaxButtonsHint);
|
||||
this->setMouseTracking(true);
|
||||
this->setAttribute(Qt::WA_Hover, true);
|
||||
this->installEventFilter(this);
|
||||
|
||||
m_centralLayout->setContentsMargins(QMargins());
|
||||
}
|
||||
|
||||
void FramelessWindow::setCentralWidget(QWidget *widget)
|
||||
{
|
||||
if (m_centralWidget) {
|
||||
m_centralLayout->removeWidget(m_centralWidget);
|
||||
m_centralWidget->deleteLater();
|
||||
}
|
||||
|
||||
m_centralLayout->addWidget(widget);
|
||||
m_centralWidget = widget;
|
||||
}
|
||||
|
||||
void FramelessWindow::installResizeCapture(QObject* widget)
|
||||
{
|
||||
widget->installEventFilter(this);
|
||||
}
|
||||
|
||||
bool FramelessWindow::eventFilter(QObject* o, QEvent* e)
|
||||
{
|
||||
switch (e->type()) {
|
||||
case QEvent::HoverMove:
|
||||
{
|
||||
QWidget* wg = qobject_cast<QWidget*>(o);
|
||||
if (wg != nullptr)
|
||||
return mouseHover(static_cast<QHoverEvent*>(e), wg);
|
||||
|
||||
break;
|
||||
}
|
||||
case QEvent::MouseButtonPress:
|
||||
return mousePress(static_cast<QMouseEvent*>(e));
|
||||
}
|
||||
|
||||
return QWidget::eventFilter(o, e);
|
||||
}
|
||||
|
||||
bool FramelessWindow::mouseHover(QHoverEvent* event, QWidget* wg)
|
||||
{
|
||||
if (!isMaximized() && !isFullScreen()) {
|
||||
QWindow* win = window()->windowHandle();
|
||||
Qt::Edges edges = this->getEdgesByPos(wg->mapToGlobal(event->oldPos()), win->frameGeometry());
|
||||
|
||||
// backup & restore cursor shape
|
||||
if (edges && !m_oldEdges)
|
||||
// entering the edge. backup cursor shape
|
||||
m_oldCursorShape = win->cursor().shape();
|
||||
if (!edges && m_oldEdges)
|
||||
// leaving the edge. restore cursor shape
|
||||
win->setCursor(m_oldCursorShape);
|
||||
|
||||
// save the latest edges status
|
||||
m_oldEdges = edges;
|
||||
|
||||
// show resize cursor shape if cursor is within border
|
||||
if (edges) {
|
||||
win->setCursor(this->getCursorByEdge(edges, Qt::ArrowCursor));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool FramelessWindow::mousePress(QMouseEvent* event)
|
||||
{
|
||||
if (event->buttons() & Qt::LeftButton && !isMaximized() && !isFullScreen()) {
|
||||
QWindow* win = window()->windowHandle();
|
||||
Qt::Edges edges = this->getEdgesByPos(event->globalPosition().toPoint(), win->frameGeometry());
|
||||
if (edges) {
|
||||
win->startSystemResize(edges);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Qt::CursorShape FramelessWindow::getCursorByEdge(const Qt::Edges& edges, Qt::CursorShape default_cursor)
|
||||
{
|
||||
if ((edges == (Qt::TopEdge | Qt::LeftEdge)) || (edges == (Qt::RightEdge | Qt::BottomEdge)))
|
||||
return Qt::SizeFDiagCursor;
|
||||
else if ((edges == (Qt::TopEdge | Qt::RightEdge)) || (edges == (Qt::LeftEdge | Qt::BottomEdge)))
|
||||
return Qt::SizeBDiagCursor;
|
||||
else if (edges & (Qt::TopEdge | Qt::BottomEdge))
|
||||
return Qt::SizeVerCursor;
|
||||
else if (edges & (Qt::LeftEdge | Qt::RightEdge))
|
||||
return Qt::SizeHorCursor;
|
||||
else
|
||||
return default_cursor;
|
||||
}
|
||||
|
||||
Qt::Edges FramelessWindow::getEdgesByPos(const QPoint gpos, const QRect& winrect)
|
||||
{
|
||||
const int borderWidth = 8;
|
||||
Qt::Edges edges;
|
||||
|
||||
int x = gpos.x() - winrect.x();
|
||||
int y = gpos.y() - winrect.y();
|
||||
|
||||
if (x < borderWidth)
|
||||
edges |= Qt::LeftEdge;
|
||||
if (x > (winrect.width() - borderWidth))
|
||||
edges |= Qt::RightEdge;
|
||||
if (y < borderWidth)
|
||||
edges |= Qt::TopEdge;
|
||||
if (y > (winrect.height() - borderWidth))
|
||||
edges |= Qt::BottomEdge;
|
||||
|
||||
return edges;
|
||||
}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2022 Gary Wang <wzc782970009@gmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef FRAMELESSWINDOW_H
|
||||
#define FRAMELESSWINDOW_H
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
class QVBoxLayout;
|
||||
QT_END_NAMESPACE
|
||||
|
||||
class FramelessWindow : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit FramelessWindow(QWidget *parent = nullptr);
|
||||
|
||||
void setCentralWidget(QWidget * widget);
|
||||
void installResizeCapture(QObject* widget);
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject *o, QEvent *e) override;
|
||||
bool mouseHover(QHoverEvent* event, QWidget* wg);
|
||||
bool mousePress(QMouseEvent* event);
|
||||
|
||||
private:
|
||||
Qt::Edges m_oldEdges;
|
||||
Qt::CursorShape m_oldCursorShape;
|
||||
|
||||
Qt::CursorShape getCursorByEdge(const Qt::Edges& edges, Qt::CursorShape default_cursor);
|
||||
Qt::Edges getEdgesByPos(const QPoint pos, const QRect& winrect);
|
||||
|
||||
QVBoxLayout * m_centralLayout = nullptr;
|
||||
QWidget * m_centralWidget = nullptr; // just a pointer, doesn't take the ownership.
|
||||
};
|
||||
|
||||
#endif // FRAMELESSWINDOW_H
|
||||
+85
-13
@@ -6,6 +6,9 @@
|
||||
|
||||
#include "settings.h"
|
||||
|
||||
#include <QStyleHints>
|
||||
#include <QOperatingSystemVersion>
|
||||
#include <QGuiApplication>
|
||||
#include <QGraphicsSceneMouseEvent>
|
||||
#include <QMimeData>
|
||||
#include <QDebug>
|
||||
@@ -16,6 +19,33 @@
|
||||
#include <QMovie>
|
||||
#include <QPainter>
|
||||
|
||||
static const QColor& ColorSchemaToTextForeground(Qt::ColorScheme colorScheme) {
|
||||
// YYC MARK:
|
||||
// If we are using Windows 10 and lower system or Qt which do not support "Windows 11"
|
||||
// theme (lower than Qt 6.7), we should only support light mode.
|
||||
// Because Qt's dark-mode-aware "Windows 11" theme is only well-supported on Windows 11.
|
||||
#if defined(Q_OS_WIN)
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 7, 0)
|
||||
colorScheme = Qt::ColorScheme::Light;
|
||||
#else
|
||||
if (QOperatingSystemVersion::current() < QOperatingSystemVersion::Windows11) {
|
||||
colorScheme = Qt::ColorScheme::Light;
|
||||
}
|
||||
#endif // QT_VERSION < QT_VERSION_CHECK(6, 7, 0)
|
||||
#endif // defined(Q_OS_WIN)
|
||||
|
||||
switch (colorScheme) {
|
||||
case Qt::ColorScheme::Dark:
|
||||
static QColor WHITE = QColor("White");
|
||||
return WHITE;
|
||||
case Qt::ColorScheme::Light:
|
||||
case Qt::ColorScheme::Unknown:
|
||||
default:
|
||||
static QColor BLACK = QColor("Black");
|
||||
return BLACK;
|
||||
}
|
||||
}
|
||||
|
||||
class PGraphicsPixmapItem : public QGraphicsPixmapItem
|
||||
{
|
||||
public:
|
||||
@@ -99,7 +129,8 @@ private:
|
||||
GraphicsScene::GraphicsScene(QObject *parent)
|
||||
: QGraphicsScene(parent)
|
||||
{
|
||||
showText(tr("Drag image here"));
|
||||
showText(tr("Drag image here", "The same meaning of MainWindow's hint."));
|
||||
connect(qGuiApp->styleHints(), &QStyleHints::colorSchemeChanged, this, &GraphicsScene::updateStyle);
|
||||
}
|
||||
|
||||
GraphicsScene::~GraphicsScene()
|
||||
@@ -121,7 +152,7 @@ void GraphicsScene::showText(const QString &text)
|
||||
{
|
||||
this->clear();
|
||||
QGraphicsTextItem * textItem = this->addText(text);
|
||||
textItem->setDefaultTextColor(QColor("White"));
|
||||
textItem->setDefaultTextColor(ColorSchemaToTextForeground(qGuiApp->styleHints()->colorScheme()));
|
||||
m_theThing = textItem;
|
||||
this->setSceneRect(m_theThing->boundingRect());
|
||||
}
|
||||
@@ -169,27 +200,57 @@ bool GraphicsScene::trySetTransformationMode(Qt::TransformationMode mode, float
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Fetch graphics item as our custom animated item.
|
||||
/// Return nullptr means this item is not animated item.
|
||||
static PGraphicsMovieItem* fetchAnimatedItem(QGraphicsItem *item) {
|
||||
return qgraphicsitem_cast<PGraphicsMovieItem *>(item);
|
||||
}
|
||||
|
||||
bool GraphicsScene::canPauseAnimation() const {
|
||||
return fetchAnimatedItem(m_theThing) != nullptr;
|
||||
}
|
||||
|
||||
bool GraphicsScene::isPauseAnimation() const {
|
||||
auto* animatedItem = fetchAnimatedItem(m_theThing);
|
||||
if (animatedItem != nullptr) {
|
||||
return animatedItem->movie()->state() == QMovie::Paused;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool GraphicsScene::togglePauseAnimation()
|
||||
{
|
||||
PGraphicsMovieItem * animatedItem = qgraphicsitem_cast<PGraphicsMovieItem *>(m_theThing);
|
||||
if (animatedItem) {
|
||||
auto* animatedItem = fetchAnimatedItem(m_theThing);
|
||||
if (animatedItem != nullptr) {
|
||||
animatedItem->movie()->setPaused(animatedItem->movie()->state() != QMovie::Paused);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool GraphicsScene::canSkipAnimationFrame() const {
|
||||
return isPauseAnimation();
|
||||
}
|
||||
|
||||
bool GraphicsScene::skipAnimationFrame(int delta)
|
||||
{
|
||||
PGraphicsMovieItem * animatedItem = qgraphicsitem_cast<PGraphicsMovieItem *>(m_theThing);
|
||||
if (animatedItem) {
|
||||
const int frameCount = animatedItem->movie()->frameCount();
|
||||
const int currentFrame = animatedItem->movie()->currentFrameNumber();
|
||||
const int targetFrame = (currentFrame + delta) % frameCount;
|
||||
animatedItem->movie()->setPaused(true);
|
||||
return animatedItem->movie()->jumpToFrame(targetFrame);
|
||||
auto* animatedItem = fetchAnimatedItem(m_theThing);
|
||||
|
||||
// Only paused animated item can skip frame
|
||||
if (animatedItem == nullptr) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
if (animatedItem->movie()->state() != QMovie::Paused) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Perform skip
|
||||
const int frameCount = animatedItem->movie()->frameCount();
|
||||
const int currentFrame = animatedItem->movie()->currentFrameNumber();
|
||||
const int targetFrame = (currentFrame + delta) % frameCount;
|
||||
return animatedItem->movie()->jumpToFrame(targetFrame);
|
||||
}
|
||||
|
||||
QPixmap GraphicsScene::renderToPixmap()
|
||||
@@ -206,3 +267,14 @@ QPixmap GraphicsScene::renderToPixmap()
|
||||
|
||||
return pixmap;
|
||||
}
|
||||
|
||||
void GraphicsScene::updateStyle(Qt::ColorScheme colorScheme) {
|
||||
Q_UNUSED(colorScheme)
|
||||
|
||||
QGraphicsItem * item = this->m_theThing;
|
||||
if (item == nullptr) return;
|
||||
QGraphicsTextItem * textItem = qgraphicsitem_cast<QGraphicsTextItem*>(item);
|
||||
if (textItem == nullptr) return;
|
||||
|
||||
textItem->setDefaultTextColor(ColorSchemaToTextForeground(colorScheme));
|
||||
}
|
||||
|
||||
@@ -21,11 +21,26 @@ public:
|
||||
|
||||
bool trySetTransformationMode(Qt::TransformationMode mode, float scaleHint);
|
||||
|
||||
/// Check whether current thing is animation.
|
||||
/// Only animation thing can be paused.
|
||||
bool canPauseAnimation() const;
|
||||
/// Check whether we are pasuing an animation graphics.
|
||||
bool isPauseAnimation() const;
|
||||
/// Toggle animation graphics pause state.
|
||||
/// Return true for successfully toggle, otherwise false.
|
||||
bool togglePauseAnimation();
|
||||
/// Check whether we can skip animation frame.
|
||||
/// Only paused animation graphics can skip frame.
|
||||
bool canSkipAnimationFrame() const;
|
||||
/// Skip paused animation graphics with given frame.
|
||||
/// Return true for successfully skip, otherwise false.
|
||||
bool skipAnimationFrame(int delta = 1);
|
||||
|
||||
QPixmap renderToPixmap();
|
||||
|
||||
private slots:
|
||||
void updateStyle(Qt::ColorScheme colorScheme);
|
||||
|
||||
private:
|
||||
QGraphicsItem * m_theThing = nullptr;
|
||||
};
|
||||
|
||||
+55
-58
@@ -7,6 +7,8 @@
|
||||
#include "graphicsscene.h"
|
||||
#include "settings.h"
|
||||
|
||||
#include <QStyleHints>
|
||||
#include <QOperatingSystemVersion>
|
||||
#include <QDebug>
|
||||
#include <QColorSpace>
|
||||
#include <QMouseEvent>
|
||||
@@ -23,11 +25,12 @@ GraphicsView::GraphicsView(QWidget *parent)
|
||||
setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
setResizeAnchor(QGraphicsView::AnchorUnderMouse);
|
||||
setTransformationAnchor(QGraphicsView::AnchorUnderMouse);
|
||||
setStyleSheet("background-color: rgba(0, 0, 0, 220);"
|
||||
"border-radius: 3px;");
|
||||
setAcceptDrops(false);
|
||||
setCheckerboardEnabled(false);
|
||||
|
||||
updateStyle(qGuiApp->styleHints()->colorScheme());
|
||||
connect(qGuiApp->styleHints(), &QStyleHints::colorSchemeChanged, this, &GraphicsView::updateStyle);
|
||||
|
||||
connect(horizontalScrollBar(), &QScrollBar::valueChanged, this, &GraphicsView::viewportRectChanged);
|
||||
connect(verticalScrollBar(), &QScrollBar::valueChanged, this, &GraphicsView::viewportRectChanged);
|
||||
}
|
||||
@@ -137,8 +140,7 @@ void GraphicsView::resetTransform()
|
||||
|
||||
void GraphicsView::zoomView(qreal scaleFactor)
|
||||
{
|
||||
m_enableFitInView = false;
|
||||
m_longImageMode = false;
|
||||
m_enableFitToScreen = false;
|
||||
scale(scaleFactor, scaleFactor);
|
||||
applyTransformationModeByScaleFactor();
|
||||
emit navigatorViewRequired(!isThingSmallerThanWindowWith(transform()), transform());
|
||||
@@ -222,6 +224,8 @@ void GraphicsView::fitByOrientation(Qt::Orientation ori, bool scaleDownOnly)
|
||||
QRectF originalScene = sceneRect();
|
||||
QTransform currentTransform = transform();
|
||||
|
||||
// TODO: May be optimized by matrix arithmetic operation?
|
||||
|
||||
if (ori == Qt::Horizontal) {
|
||||
// For horizontal fit (fit by width), position at top (for tall images)
|
||||
// Find the scene point that corresponds to the top-center of the transformed image
|
||||
@@ -274,37 +278,12 @@ void GraphicsView::fitByOrientation(Qt::Orientation ori, bool scaleDownOnly)
|
||||
centerOn(sceneLeftCenter);
|
||||
}
|
||||
|
||||
m_enableFitInView = false;
|
||||
m_enableFitToScreen = false;
|
||||
|
||||
applyTransformationModeByScaleFactor();
|
||||
emit navigatorViewRequired(!isThingSmallerThanWindowWith(transform()), transform());
|
||||
}
|
||||
|
||||
bool GraphicsView::isLongImage() const
|
||||
{
|
||||
// Get the transformed image size (considering rotation and other transforms)
|
||||
QRectF transformedRect = transform().mapRect(sceneRect());
|
||||
QSizeF imageSize = transformedRect.size();
|
||||
|
||||
if (imageSize.isEmpty()) return false;
|
||||
|
||||
qreal aspectRatio = imageSize.width() / imageSize.height();
|
||||
|
||||
// Check if aspect ratio exceeds 5:2 (wide) or 2:5 (tall)
|
||||
return aspectRatio > 2.5 || aspectRatio < 0.4;
|
||||
}
|
||||
|
||||
void GraphicsView::fitLongImage()
|
||||
{
|
||||
QRectF transformedRect = transform().mapRect(sceneRect());
|
||||
|
||||
if (transformedRect.width() < transformedRect.height()) {
|
||||
fitByOrientation(Qt::Horizontal, true);
|
||||
} else {
|
||||
fitByOrientation(Qt::Vertical, true);
|
||||
}
|
||||
}
|
||||
|
||||
void GraphicsView::displayScene()
|
||||
{
|
||||
if (shouldAvoidTransform()) {
|
||||
@@ -312,14 +291,6 @@ void GraphicsView::displayScene()
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if should apply long image mode
|
||||
if (Settings::instance()->autoLongImageMode() && isLongImage()) {
|
||||
m_longImageMode = true;
|
||||
m_firstUserMediaLoaded = true;
|
||||
if (isSceneBiggerThanView()) fitLongImage();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSceneBiggerThanView()) {
|
||||
// Do fit-in-view
|
||||
fitInView(sceneRect(), Qt::KeepAspectRatio);
|
||||
@@ -330,8 +301,7 @@ void GraphicsView::displayScene()
|
||||
emit navigatorViewRequired(false, transform());
|
||||
}
|
||||
|
||||
m_longImageMode = false;
|
||||
m_enableFitInView = true;
|
||||
m_enableFitToScreen = true;
|
||||
m_firstUserMediaLoaded = true;
|
||||
}
|
||||
|
||||
@@ -345,17 +315,12 @@ bool GraphicsView::isSceneBiggerThanView() const
|
||||
}
|
||||
|
||||
// Automately do fit in view when viewport(window) smaller than image original size.
|
||||
void GraphicsView::setEnableAutoFitInView(bool enable)
|
||||
void GraphicsView::setEnableAutoFitToScreen(bool enable)
|
||||
{
|
||||
m_enableFitInView = enable;
|
||||
m_enableFitToScreen = enable;
|
||||
}
|
||||
|
||||
void GraphicsView::setLongImageMode(bool enable)
|
||||
{
|
||||
m_longImageMode = enable;
|
||||
}
|
||||
|
||||
bool GraphicsView::avoidResetTransform() const
|
||||
bool GraphicsView::isAvoidResetTransform() const
|
||||
{
|
||||
return m_avoidResetTransform;
|
||||
}
|
||||
@@ -365,6 +330,15 @@ void GraphicsView::setAvoidResetTransform(bool avoidReset)
|
||||
m_avoidResetTransform = avoidReset;
|
||||
}
|
||||
|
||||
bool GraphicsView::isCheckerboard() const {
|
||||
return m_checkerboardEnabled;
|
||||
}
|
||||
|
||||
void GraphicsView::toggleCheckerboard(bool invertCheckerboardColor)
|
||||
{
|
||||
setCheckerboardEnabled(!m_checkerboardEnabled, invertCheckerboardColor);
|
||||
}
|
||||
|
||||
inline double zeroOrOne(double number)
|
||||
{
|
||||
return qFuzzyIsNull(number) ? 0 : (number > 0 ? 1 : -1);
|
||||
@@ -379,9 +353,37 @@ QTransform GraphicsView::resetScale(const QTransform & orig)
|
||||
orig.dx(), orig.dy());
|
||||
}
|
||||
|
||||
void GraphicsView::toggleCheckerboard(bool invertCheckerboardColor)
|
||||
{
|
||||
setCheckerboardEnabled(!m_checkerboardEnabled, invertCheckerboardColor);
|
||||
void GraphicsView::updateStyle(Qt::ColorScheme colorScheme) {
|
||||
// YYC MARK:
|
||||
// If we are using Windows 10 and lower system or Qt which do not support "Windows 11"
|
||||
// theme (lower than Qt 6.7), we should only support light mode.
|
||||
// Because Qt's dark-mode-aware "Windows 11" theme is only well-supported on Windows 11.
|
||||
#if defined(Q_OS_WIN)
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 7, 0)
|
||||
colorScheme = Qt::ColorScheme::Light;
|
||||
#else
|
||||
if (QOperatingSystemVersion::current() < QOperatingSystemVersion::Windows11) {
|
||||
colorScheme = Qt::ColorScheme::Light;
|
||||
}
|
||||
#endif // QT_VERSION < QT_VERSION_CHECK(6, 7, 0)
|
||||
#endif // defined(Q_OS_WIN)
|
||||
|
||||
switch (colorScheme) {
|
||||
case Qt::ColorScheme::Dark:
|
||||
setStyleSheet(R"#(
|
||||
border-style: none;
|
||||
background-color: black;
|
||||
)#");
|
||||
break;
|
||||
case Qt::ColorScheme::Light:
|
||||
case Qt::ColorScheme::Unknown:
|
||||
default:
|
||||
setStyleSheet(R"#(
|
||||
border-style: none;
|
||||
background-color: white;
|
||||
)#");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void GraphicsView::mousePressEvent(QMouseEvent *event)
|
||||
@@ -427,16 +429,11 @@ void GraphicsView::wheelEvent(QWheelEvent *event)
|
||||
|
||||
void GraphicsView::resizeEvent(QResizeEvent *event)
|
||||
{
|
||||
if (m_longImageMode) {
|
||||
// In long image mode, reapply long image logic on resize
|
||||
// We directly apply the long image mode logic without rechecking
|
||||
// if we should enter long image mode, as the mode is already active
|
||||
fitLongImage();
|
||||
} else if (m_enableFitInView) {
|
||||
if (m_enableFitToScreen) {
|
||||
bool originalSizeSmallerThanWindow = isThingSmallerThanWindowWith(resetScale(transform()));
|
||||
if (originalSizeSmallerThanWindow && scaleFactor() >= 1) {
|
||||
// no longer need to do fitInView()
|
||||
// but we leave the m_enableFitInView value unchanged in case
|
||||
// but we leave the m_enableToScreen value unchanged in case
|
||||
// user resize down the window again.
|
||||
} else if (originalSizeSmallerThanWindow && scaleFactor() < 1) {
|
||||
resetScale();
|
||||
|
||||
+8
-11
@@ -38,24 +38,22 @@ public:
|
||||
|
||||
void displayScene();
|
||||
bool isSceneBiggerThanView() const;
|
||||
void setEnableAutoFitInView(bool enable = true);
|
||||
void setLongImageMode(bool enable = true);
|
||||
void setEnableAutoFitToScreen(bool enable = true);
|
||||
|
||||
bool avoidResetTransform() const;
|
||||
bool isAvoidResetTransform() const;
|
||||
void setAvoidResetTransform(bool avoidReset);
|
||||
|
||||
static QTransform resetScale(const QTransform & orig);
|
||||
bool isCheckerboard() const;
|
||||
void toggleCheckerboard(bool invertCheckerboardColor = false);
|
||||
|
||||
// Long image mode support
|
||||
bool isLongImage() const;
|
||||
void fitLongImage();
|
||||
static QTransform resetScale(const QTransform & orig);
|
||||
|
||||
signals:
|
||||
void navigatorViewRequired(bool required, QTransform transform);
|
||||
void viewportRectChanged();
|
||||
|
||||
public slots:
|
||||
void toggleCheckerboard(bool invertCheckerboardColor = false);
|
||||
private slots:
|
||||
void updateStyle(Qt::ColorScheme colorScheme);
|
||||
|
||||
private:
|
||||
void mousePressEvent(QMouseEvent * event) override;
|
||||
@@ -74,8 +72,7 @@ private:
|
||||
// Consider switch to 3 state for "no fit", "always fit" and "fit when view is smaller"?
|
||||
// ... or even more? e.g. "fit/snap width" things...
|
||||
// Currently it's "no fit" when it's false and "fit when view is smaller" when it's true.
|
||||
bool m_enableFitInView = false;
|
||||
bool m_longImageMode = false;
|
||||
bool m_enableFitToScreen = false;
|
||||
bool m_avoidResetTransform = false;
|
||||
bool m_checkerboardEnabled = false;
|
||||
bool m_useLightCheckerboard = false;
|
||||
|
||||
+5
-12
@@ -4,7 +4,6 @@
|
||||
|
||||
#include "mainwindow.h"
|
||||
|
||||
#include "playlistmanager.h"
|
||||
#include "settings.h"
|
||||
|
||||
#ifdef Q_OS_MACOS
|
||||
@@ -68,21 +67,19 @@ int main(int argc, char *argv[])
|
||||
a.installEventFilter(fileOpenEventHandler);
|
||||
a.connect(fileOpenEventHandler, &FileOpenEventHandler::fileOpen, [&w](const QUrl & url){
|
||||
if (w.isHidden()) {
|
||||
w.setWindowOpacity(1);
|
||||
w.showNormal();
|
||||
} else {
|
||||
w.activateWindow();
|
||||
}
|
||||
w.showUrls({url});
|
||||
w.showFiles({url});
|
||||
w.initWindowSize();
|
||||
});
|
||||
|
||||
// Handle dock icon clicks to show hidden window
|
||||
a.connect(&a, &QApplication::applicationStateChanged, [&w](Qt::ApplicationState state) {
|
||||
if (state == Qt::ApplicationActive && w.isHidden()) {
|
||||
w.showUrls({});
|
||||
w.galleryCurrent(true, true);
|
||||
w.setWindowOpacity(1);
|
||||
w.showFiles({});
|
||||
w.galleryCurrent(false);
|
||||
w.showNormal();
|
||||
w.raise();
|
||||
w.activateWindow();
|
||||
@@ -90,12 +87,8 @@ int main(int argc, char *argv[])
|
||||
});
|
||||
#endif // Q_OS_MACOS
|
||||
|
||||
QStringList urlStrList = parser.positionalArguments();
|
||||
QList<QUrl> && urlList = PlaylistManager::convertToUrlList(urlStrList);
|
||||
|
||||
if (!urlList.isEmpty()) {
|
||||
w.showUrls(urlList);
|
||||
}
|
||||
QStringList fileList = parser.positionalArguments();
|
||||
w.showFiles(fileList);
|
||||
|
||||
w.initWindowSize();
|
||||
|
||||
|
||||
+240
-691
File diff suppressed because it is too large
Load Diff
+31
-70
@@ -5,25 +5,19 @@
|
||||
#ifndef MAINWINDOW_H
|
||||
#define MAINWINDOW_H
|
||||
|
||||
#include "framelesswindow.h"
|
||||
|
||||
#include <QParallelAnimationGroup>
|
||||
#include <QPropertyAnimation>
|
||||
#include <QPushButton>
|
||||
#include <QWidget>
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
class QGraphicsOpacityEffect;
|
||||
class QGraphicsView;
|
||||
class QFileSystemWatcher;
|
||||
namespace Ui {
|
||||
class MainWindow;
|
||||
}
|
||||
QT_END_NAMESPACE
|
||||
|
||||
class ActionManager;
|
||||
class PlaylistManager;
|
||||
class ToolButton;
|
||||
class GraphicsView;
|
||||
class NavigatorView;
|
||||
class BottomButtonGroup;
|
||||
class MainWindow : public FramelessWindow
|
||||
|
||||
class MainWindow : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
@@ -31,42 +25,26 @@ public:
|
||||
explicit MainWindow(QWidget *parent = nullptr);
|
||||
~MainWindow() override;
|
||||
|
||||
void showUrls(const QList<QUrl> &urls);
|
||||
void showFiles(const QStringList& files);
|
||||
void showFiles(const QList<QUrl>& urls);
|
||||
void initWindowSize();
|
||||
void adjustWindowSizeBySceneRect();
|
||||
QUrl currentImageFileUrl() const;
|
||||
|
||||
void clearGallery();
|
||||
void galleryPrev();
|
||||
void galleryNext();
|
||||
void galleryCurrent(bool showLoadImageHintWhenEmpty, bool reloadImage);
|
||||
void galleryCurrent(bool fromFileWatcher);
|
||||
|
||||
static QStringList supportedImageFormats();
|
||||
|
||||
protected slots:
|
||||
void showEvent(QShowEvent *event) override;
|
||||
void enterEvent(QEnterEvent *event) override;
|
||||
void leaveEvent(QEvent *event) override;
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
void mouseMoveEvent(QMouseEvent *event) override;
|
||||
void mouseReleaseEvent(QMouseEvent *event) override;
|
||||
void mouseDoubleClickEvent(QMouseEvent *event) override;
|
||||
void wheelEvent(QWheelEvent *event) override;
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
void contextMenuEvent(QContextMenuEvent *event) override;
|
||||
void dragEnterEvent(QDragEnterEvent *event) override;
|
||||
void dragMoveEvent(QDragMoveEvent *event) override;
|
||||
void dropEvent(QDropEvent *event) override;
|
||||
|
||||
void centerWindow();
|
||||
void closeWindow();
|
||||
void updateWidgetsPosition();
|
||||
void toggleProtectedMode();
|
||||
void toggleStayOnTop();
|
||||
void toggleAvoidResetTransform();
|
||||
bool stayOnTop() const;
|
||||
bool canPaste() const;
|
||||
void quitAppAction(bool force = false);
|
||||
void toggleFullscreen();
|
||||
void toggleMaximize();
|
||||
|
||||
@@ -74,63 +52,46 @@ protected:
|
||||
QSize sizeHint() const override;
|
||||
|
||||
private slots:
|
||||
void on_actionOpen_triggered();
|
||||
void on_actionSaveAs_triggered();
|
||||
|
||||
void on_actionActualSize_triggered();
|
||||
void on_actionToggleMaximize_triggered();
|
||||
void on_actionZoomIn_triggered();
|
||||
void on_actionZoomOut_triggered();
|
||||
void on_actionToggleCheckerboard_triggered();
|
||||
void on_actionRotateClockwise_triggered();
|
||||
void on_actionRotateCounterClockwise_triggered();
|
||||
void on_actionCopyPixmap_triggered();
|
||||
void on_actionCopyFilePath_triggered();
|
||||
|
||||
void on_actionPrevPicture_triggered();
|
||||
void on_actionNextPicture_triggered();
|
||||
|
||||
void on_actionActualSize_triggered();
|
||||
void on_actionFitToScreen_triggered();
|
||||
void on_actionFitByWidth_triggered();
|
||||
void on_actionFitByHeight_triggered();
|
||||
void on_actionZoomIn_triggered();
|
||||
void on_actionZoomOut_triggered();
|
||||
|
||||
void on_actionFlipHorizontal_triggered();
|
||||
void on_actionFlipVertical_triggered();
|
||||
void on_actionRotateClockwise_triggered();
|
||||
void on_actionRotateCounterClockwise_triggered();
|
||||
|
||||
void on_actionToggleAvoidResetTransform_triggered();
|
||||
void on_actionToggleCheckerboard_triggered();
|
||||
void on_actionTogglePauseAnimation_triggered();
|
||||
void on_actionAnimationNextFrame_triggered();
|
||||
|
||||
void on_actionHorizontalFlip_triggered();
|
||||
void on_actionFitInView_triggered();
|
||||
void on_actionFitByWidth_triggered();
|
||||
void on_actionFitLongImage_triggered();
|
||||
void on_actionCopyPixmap_triggered();
|
||||
void on_actionCopyFilePath_triggered();
|
||||
void on_actionPaste_triggered();
|
||||
void on_actionTrash_triggered();
|
||||
void on_actionToggleStayOnTop_triggered();
|
||||
void on_actionToggleProtectMode_triggered();
|
||||
void on_actionToggleAvoidResetTransform_triggered();
|
||||
void on_actionLocateInFileManager_triggered();
|
||||
void on_actionProperties_triggered();
|
||||
|
||||
void on_actionSettings_triggered();
|
||||
void on_actionHelp_triggered();
|
||||
void on_actionProperties_triggered();
|
||||
void on_actionLocateInFileManager_triggered();
|
||||
void on_actionQuitApp_triggered();
|
||||
|
||||
void doCloseWindow();
|
||||
|
||||
private:
|
||||
bool updateFileWatcher(const QString & basePath = QString());
|
||||
void updateGalleryButtonsVisibility();
|
||||
void updateActionState();
|
||||
|
||||
private:
|
||||
QT_PREPEND_NAMESPACE(Ui::MainWindow) *ui;
|
||||
|
||||
ActionManager *m_am;
|
||||
PlaylistManager *m_pm;
|
||||
|
||||
QPoint m_oldMousePos;
|
||||
QPropertyAnimation *m_fadeOutAnimation;
|
||||
QPropertyAnimation *m_floatUpAnimation;
|
||||
QParallelAnimationGroup *m_exitAnimationGroup;
|
||||
QFileSystemWatcher *m_fileSystemWatcher;
|
||||
ToolButton *m_closeButton;
|
||||
ToolButton *m_prevButton;
|
||||
ToolButton *m_nextButton;
|
||||
GraphicsView *m_graphicsView;
|
||||
NavigatorView *m_gv;
|
||||
BottomButtonGroup *m_bottomButtonGroup;
|
||||
bool m_protectedMode = false;
|
||||
bool m_clickedOnWindow = false;
|
||||
};
|
||||
|
||||
#endif // MAINWINDOW_H
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>MainWindow</class>
|
||||
<widget class="QWidget" name="MainWindow">
|
||||
<property name="windowModality">
|
||||
<enum>Qt::NonModal</enum>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>640</width>
|
||||
<height>480</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="acceptDrops">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="m_centralLayout">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="GraphicsView" name="m_gv">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::NoFrame</enum>
|
||||
</property>
|
||||
<property name="verticalScrollBarPolicy">
|
||||
<enum>Qt::ScrollBarAlwaysOff</enum>
|
||||
</property>
|
||||
<property name="horizontalScrollBarPolicy">
|
||||
<enum>Qt::ScrollBarAlwaysOff</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="bottomBar" native="true">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>72</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>72</height>
|
||||
</size>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="bottomBarLayout">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QWidget" name="m_navPH" native="true">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>99</width>
|
||||
<height>72</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>99</width>
|
||||
<height>72</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="leftSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="BottomButtonGroup" name="m_bottomButtonGroup"/>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="rightSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="NavigatorView" name="m_nav">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>99</width>
|
||||
<height>72</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>99</width>
|
||||
<height>72</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::NoFrame</enum>
|
||||
</property>
|
||||
<property name="verticalScrollBarPolicy">
|
||||
<enum>Qt::ScrollBarAlwaysOff</enum>
|
||||
</property>
|
||||
<property name="horizontalScrollBarPolicy">
|
||||
<enum>Qt::ScrollBarAlwaysOff</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
<widget class="QMenu" name="m_subMenuCopy">
|
||||
<property name="title">
|
||||
<string>&Copy</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset theme="edit-copy"/>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QMenu" name="m_subMenuTransform">
|
||||
<property name="title">
|
||||
<string>&Transform</string>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QMenu" name="m_menu"/>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>GraphicsView</class>
|
||||
<extends>QGraphicsView</extends>
|
||||
<header>graphicsview.h</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>NavigatorView</class>
|
||||
<extends>QGraphicsView</extends>
|
||||
<header>navigatorview.h</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>BottomButtonGroup</class>
|
||||
<extends>QGroupBox</extends>
|
||||
<header>bottombuttongroup.h</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
+18
-15
@@ -3,12 +3,12 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "metadatadialog.h"
|
||||
#include "ui_metadatadialog.h"
|
||||
|
||||
#include <QDialogButtonBox>
|
||||
#include <QPainter>
|
||||
#include <QStyledItemDelegate>
|
||||
#include <QTreeView>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHeaderView>
|
||||
|
||||
#include "metadatamodel.h"
|
||||
@@ -73,30 +73,33 @@ protected:
|
||||
};
|
||||
|
||||
MetadataDialog::MetadataDialog(QWidget *parent)
|
||||
: QDialog(parent)
|
||||
, m_treeView(new PropertyTreeView(this))
|
||||
: QDialog(parent), ui(new QT_PREPEND_NAMESPACE(Ui::MetadataDialog))
|
||||
{
|
||||
ui->setupUi(this);
|
||||
setWindowFlag(Qt::WindowContextHelpButtonHint, false);
|
||||
|
||||
// YYC MARK:
|
||||
// Because we use custom QTreeView and we do not want to expose it to public domain.
|
||||
// So I have to use this way as the compromise.
|
||||
|
||||
// Create our custom QTreeView and insert them
|
||||
m_treeView = new PropertyTreeView(this);
|
||||
auto* layout = ui->verticalLayout;
|
||||
const int index = layout->indexOf(ui->m_buttonBox);
|
||||
layout->insertWidget(index, m_treeView);
|
||||
|
||||
// Setup our custom QTreeView
|
||||
m_treeView->setRootIsDecorated(false);
|
||||
m_treeView->setIndentation(0);
|
||||
m_treeView->setItemDelegate(new PropertyTreeItemDelegate(m_treeView));
|
||||
m_treeView->header()->resizeSection(0, sizeHint().width() / 2);
|
||||
|
||||
setWindowTitle(tr("Image Metadata"));
|
||||
|
||||
QDialogButtonBox * buttonBox = new QDialogButtonBox(QDialogButtonBox::Close);
|
||||
|
||||
setLayout(new QVBoxLayout);
|
||||
layout()->addWidget(m_treeView);
|
||||
layout()->addWidget(buttonBox);
|
||||
|
||||
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::close);
|
||||
|
||||
setWindowFlag(Qt::WindowContextHelpButtonHint, false);
|
||||
connect(ui->m_buttonBox, &QDialogButtonBox::rejected, this, &QDialog::close);
|
||||
}
|
||||
|
||||
MetadataDialog::~MetadataDialog()
|
||||
{
|
||||
|
||||
delete ui;
|
||||
}
|
||||
|
||||
void MetadataDialog::setMetadataModel(MetadataModel * model)
|
||||
|
||||
@@ -11,6 +11,12 @@ QT_BEGIN_NAMESPACE
|
||||
class QTreeView;
|
||||
QT_END_NAMESPACE
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
namespace Ui {
|
||||
class MetadataDialog;
|
||||
}
|
||||
QT_END_NAMESPACE
|
||||
|
||||
class MetadataModel;
|
||||
class MetadataDialog : public QDialog
|
||||
{
|
||||
@@ -25,6 +31,7 @@ public:
|
||||
|
||||
private:
|
||||
QTreeView * m_treeView = nullptr;
|
||||
QT_PREPEND_NAMESPACE(Ui::MetadataDialog) *ui;
|
||||
};
|
||||
|
||||
#endif // METADATADIALOG_H
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>MetadataDialog</class>
|
||||
<widget class="QDialog" name="MetadataDialog">
|
||||
<property name="windowModality">
|
||||
<enum>Qt::NonModal</enum>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Image Metadata</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="m_buttonBox">
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Close</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
+38
-9
@@ -5,8 +5,9 @@
|
||||
#include "navigatorview.h"
|
||||
|
||||
#include "graphicsview.h"
|
||||
#include "opacityhelper.h"
|
||||
|
||||
#include <QStyleHints>
|
||||
#include <QOperatingSystemVersion>
|
||||
#include <QMouseEvent>
|
||||
#include <QDebug>
|
||||
#include <QTimer>
|
||||
@@ -14,12 +15,12 @@
|
||||
NavigatorView::NavigatorView(QWidget *parent)
|
||||
: QGraphicsView (parent)
|
||||
, m_viewportRegion(this->rect())
|
||||
, m_opacityHelper(new OpacityHelper(this))
|
||||
{
|
||||
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
setStyleSheet("background-color: rgba(0, 0, 0, 120);"
|
||||
"border-radius: 3px;");
|
||||
|
||||
updateStyle(qGuiApp->styleHints()->colorScheme());
|
||||
connect(qGuiApp->styleHints(), &QStyleHints::colorSchemeChanged, this, &NavigatorView::updateStyle);
|
||||
}
|
||||
|
||||
// doesn't take or manage its ownership
|
||||
@@ -28,11 +29,6 @@ void NavigatorView::setMainView(GraphicsView *mainView)
|
||||
m_mainView = mainView;
|
||||
}
|
||||
|
||||
void NavigatorView::setOpacity(qreal opacity, bool animated)
|
||||
{
|
||||
m_opacityHelper->setOpacity(opacity, animated);
|
||||
}
|
||||
|
||||
void NavigatorView::updateMainViewportRegion()
|
||||
{
|
||||
// Use QTimer::singleShot with lambda to delay the update
|
||||
@@ -45,6 +41,39 @@ void NavigatorView::updateMainViewportRegion()
|
||||
});
|
||||
}
|
||||
|
||||
void NavigatorView::updateStyle(Qt::ColorScheme colorScheme) {
|
||||
// YYC MARK:
|
||||
// If we are using Windows 10 and lower system or Qt which do not support "Windows 11"
|
||||
// theme (lower than Qt 6.7), we should only support light mode.
|
||||
// Because Qt's dark-mode-aware "Windows 11" theme is only well-supported on Windows 11.
|
||||
#if defined(Q_OS_WIN)
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 7, 0)
|
||||
colorScheme = Qt::ColorScheme::Light;
|
||||
#else
|
||||
if (QOperatingSystemVersion::current() < QOperatingSystemVersion::Windows11) {
|
||||
colorScheme = Qt::ColorScheme::Light;
|
||||
}
|
||||
#endif // QT_VERSION < QT_VERSION_CHECK(6, 7, 0)
|
||||
#endif // defined(Q_OS_WIN)
|
||||
|
||||
switch (colorScheme) {
|
||||
case Qt::ColorScheme::Dark:
|
||||
setStyleSheet(R"#(
|
||||
border-style: none;
|
||||
background-color: rgba(255, 255, 255, 70);
|
||||
)#");
|
||||
break;
|
||||
case Qt::ColorScheme::Light:
|
||||
case Qt::ColorScheme::Unknown:
|
||||
default:
|
||||
setStyleSheet(R"#(
|
||||
border-style: none;
|
||||
background-color: rgba(0, 0, 0, 70);
|
||||
)#");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void NavigatorView::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
m_mouseDown = true;
|
||||
|
||||
+3
-3
@@ -7,7 +7,6 @@
|
||||
|
||||
#include <QGraphicsView>
|
||||
|
||||
class OpacityHelper;
|
||||
class GraphicsView;
|
||||
class NavigatorView : public QGraphicsView
|
||||
{
|
||||
@@ -16,11 +15,13 @@ public:
|
||||
NavigatorView(QWidget *parent = nullptr);
|
||||
|
||||
void setMainView(GraphicsView *mainView);
|
||||
void setOpacity(qreal opacity, bool animated = true);
|
||||
|
||||
public slots:
|
||||
void updateMainViewportRegion();
|
||||
|
||||
private slots:
|
||||
void updateStyle(Qt::ColorScheme colorScheme);
|
||||
|
||||
private:
|
||||
void mousePressEvent(QMouseEvent * event) override;
|
||||
void mouseMoveEvent(QMouseEvent * event) override;
|
||||
@@ -32,7 +33,6 @@ private:
|
||||
bool m_mouseDown = false;
|
||||
QPolygon m_viewportRegion;
|
||||
QGraphicsView *m_mainView = nullptr;
|
||||
OpacityHelper *m_opacityHelper = nullptr;
|
||||
};
|
||||
|
||||
#endif // NAVIGATORVIEW_H
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2022 Gary Wang <wzc782970009@gmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "opacityhelper.h"
|
||||
|
||||
#include <QGraphicsOpacityEffect>
|
||||
#include <QPropertyAnimation>
|
||||
|
||||
OpacityHelper::OpacityHelper(QWidget *parent)
|
||||
: QObject(parent)
|
||||
, m_opacityFx(new QGraphicsOpacityEffect(parent))
|
||||
, m_opacityAnimation(new QPropertyAnimation(m_opacityFx, "opacity"))
|
||||
{
|
||||
parent->setGraphicsEffect(m_opacityFx);
|
||||
|
||||
m_opacityAnimation->setDuration(300);
|
||||
}
|
||||
|
||||
void OpacityHelper::setOpacity(qreal opacity, bool animated)
|
||||
{
|
||||
if (!animated) {
|
||||
m_opacityFx->setOpacity(opacity);
|
||||
return;
|
||||
}
|
||||
|
||||
m_opacityAnimation->stop();
|
||||
m_opacityAnimation->setStartValue(m_opacityFx->opacity());
|
||||
m_opacityAnimation->setEndValue(opacity);
|
||||
m_opacityAnimation->start();
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2022 Gary Wang <wzc782970009@gmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef OPACITYHELPER_H
|
||||
#define OPACITYHELPER_H
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
class QGraphicsOpacityEffect;
|
||||
class QPropertyAnimation;
|
||||
QT_END_NAMESPACE
|
||||
|
||||
class OpacityHelper : QObject
|
||||
{
|
||||
public:
|
||||
OpacityHelper(QWidget * parent);
|
||||
|
||||
void setOpacity(qreal opacity, bool animated = true);
|
||||
|
||||
protected:
|
||||
QGraphicsOpacityEffect * m_opacityFx;
|
||||
QPropertyAnimation * m_opacityAnimation;
|
||||
};
|
||||
|
||||
#endif // OPACITYHELPER_H
|
||||
+213
-201
@@ -3,158 +3,20 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "playlistmanager.h"
|
||||
#include "playlistscout.h"
|
||||
|
||||
#include <QCollator>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QUrl>
|
||||
|
||||
PlaylistModel::PlaylistModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
PlaylistModel::~PlaylistModel()
|
||||
= default;
|
||||
|
||||
void PlaylistModel::setPlaylist(const QList<QUrl> &urls)
|
||||
{
|
||||
beginResetModel();
|
||||
m_playlist = urls;
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
QModelIndex PlaylistModel::loadPlaylist(const QList<QUrl> & urls)
|
||||
{
|
||||
if (urls.isEmpty()) return {};
|
||||
if (urls.count() == 1) {
|
||||
return loadPlaylist(urls.constFirst());
|
||||
} else {
|
||||
setPlaylist(urls);
|
||||
return index(0);
|
||||
}
|
||||
}
|
||||
|
||||
QModelIndex PlaylistModel::loadPlaylist(const QUrl &url)
|
||||
{
|
||||
QFileInfo info(url.toLocalFile());
|
||||
QDir dir(info.path());
|
||||
QString && currentFileName = info.fileName();
|
||||
|
||||
if (dir.path() == m_currentDir) {
|
||||
int idx = indexOf(url);
|
||||
return idx == -1 ? appendToPlaylist(url) : index(idx);
|
||||
}
|
||||
|
||||
QStringList entryList = dir.entryList(
|
||||
m_autoLoadSuffixes,
|
||||
QDir::Files | QDir::NoSymLinks, QDir::NoSort);
|
||||
|
||||
QCollator collator;
|
||||
collator.setNumericMode(true);
|
||||
|
||||
std::sort(entryList.begin(), entryList.end(), collator);
|
||||
|
||||
QList<QUrl> playlist;
|
||||
|
||||
int idx = -1;
|
||||
for (int i = 0; i < entryList.count(); i++) {
|
||||
const QString & fileName = entryList.at(i);
|
||||
const QString & oneEntry = dir.absoluteFilePath(fileName);
|
||||
const QUrl & url = QUrl::fromLocalFile(oneEntry);
|
||||
playlist.append(url);
|
||||
if (fileName == currentFileName) {
|
||||
idx = i;
|
||||
}
|
||||
}
|
||||
if (idx == -1) {
|
||||
idx = playlist.count();
|
||||
playlist.append(url);
|
||||
}
|
||||
m_currentDir = dir.path();
|
||||
|
||||
setPlaylist(playlist);
|
||||
|
||||
return index(idx);
|
||||
}
|
||||
|
||||
QModelIndex PlaylistModel::appendToPlaylist(const QUrl &url)
|
||||
{
|
||||
const int lastIndex = rowCount();
|
||||
beginInsertRows(QModelIndex(), lastIndex, lastIndex);
|
||||
m_playlist.append(url);
|
||||
endInsertRows();
|
||||
return index(lastIndex);
|
||||
}
|
||||
|
||||
bool PlaylistModel::removeAt(int index)
|
||||
{
|
||||
if (index < 0 || index >= rowCount()) return false;
|
||||
beginRemoveRows(QModelIndex(), index, index);
|
||||
m_playlist.removeAt(index);
|
||||
endRemoveRows();
|
||||
return true;
|
||||
}
|
||||
|
||||
int PlaylistModel::indexOf(const QUrl &url) const
|
||||
{
|
||||
return m_playlist.indexOf(url);
|
||||
}
|
||||
|
||||
QUrl PlaylistModel::urlByIndex(int index) const
|
||||
{
|
||||
return m_playlist.value(index);
|
||||
}
|
||||
|
||||
QStringList PlaylistModel::autoLoadFilterSuffixes() const
|
||||
{
|
||||
return m_autoLoadSuffixes;
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> PlaylistModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> result = QAbstractListModel::roleNames();
|
||||
result.insert(UrlRole, "url");
|
||||
return result;
|
||||
}
|
||||
|
||||
int PlaylistModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
return m_playlist.count();
|
||||
}
|
||||
|
||||
QVariant PlaylistModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid()) return {};
|
||||
|
||||
switch (role) {
|
||||
case Qt::DisplayRole:
|
||||
return m_playlist.at(index.row()).fileName();
|
||||
case UrlRole:
|
||||
return m_playlist.at(index.row());
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
PlaylistManager::PlaylistManager(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_currentIndex(std::nullopt)
|
||||
, m_playlist()
|
||||
, m_allowedSuffixes()
|
||||
{
|
||||
connect(&m_model, &PlaylistModel::rowsRemoved, this,
|
||||
[this](const QModelIndex &, int, int) {
|
||||
if (m_model.rowCount() <= m_currentIndex) {
|
||||
setProperty("currentIndex", m_currentIndex - 1);
|
||||
}
|
||||
});
|
||||
|
||||
auto onRowCountChanged = [this](){
|
||||
emit totalCountChanged(m_model.rowCount());
|
||||
};
|
||||
|
||||
connect(&m_model, &PlaylistModel::rowsInserted, this, onRowCountChanged);
|
||||
connect(&m_model, &PlaylistModel::rowsRemoved, this, onRowCountChanged);
|
||||
connect(&m_model, &PlaylistModel::modelReset, this, onRowCountChanged);
|
||||
}
|
||||
|
||||
PlaylistManager::~PlaylistManager()
|
||||
@@ -162,35 +24,86 @@ PlaylistManager::~PlaylistManager()
|
||||
|
||||
}
|
||||
|
||||
PlaylistModel *PlaylistManager::model()
|
||||
{
|
||||
return &m_model;
|
||||
// region: Allowed Suffixes Operations
|
||||
|
||||
const QStringList& PlaylistManager::allowedSuffixes() const {
|
||||
return m_allowedSuffixes;
|
||||
}
|
||||
|
||||
void PlaylistManager::setPlaylist(const QList<QUrl> &urls)
|
||||
{
|
||||
m_model.setPlaylist(urls);
|
||||
void PlaylistManager::setAllowedSuffixes(const QStringList& nameFilters) {
|
||||
m_allowedSuffixes = nameFilters;
|
||||
emit allowedSuffixesChanged(m_allowedSuffixes);
|
||||
}
|
||||
|
||||
QModelIndex PlaylistManager::loadPlaylist(const QList<QUrl> &urls)
|
||||
{
|
||||
QModelIndex idx = m_model.loadPlaylist(urls);
|
||||
setProperty("currentIndex", idx.row());
|
||||
return idx;
|
||||
// endregion
|
||||
|
||||
// region: Playlist Loader and Reset
|
||||
|
||||
bool PlaylistManager::loadPlaylist(const QStringList& files) {
|
||||
// Convert string list to url list first
|
||||
QList<QUrl> urls;
|
||||
for (const QString & file : std::as_const(files)) {
|
||||
urls.append(urlFromString(file));
|
||||
}
|
||||
|
||||
return loadPlaylist(urls);
|
||||
}
|
||||
|
||||
QModelIndex PlaylistManager::loadPlaylist(const QUrl &url)
|
||||
{
|
||||
QModelIndex idx = m_model.loadPlaylist(url);
|
||||
setProperty("currentIndex", idx.row());
|
||||
return idx;
|
||||
bool PlaylistManager::loadPlaylist(const QList<QUrl>& urls) {
|
||||
// Check its count, and check M3U8 playlist for single file case.
|
||||
if (urls.isEmpty()) {
|
||||
clearPlaylist();
|
||||
return true;
|
||||
} else if (urls.count() == 1) {
|
||||
const QUrl& firstUrl = urls.first();
|
||||
const QString lowerCaseUrlPath(firstUrl.path().toLower());
|
||||
if (lowerCaseUrlPath.endsWith(".m3u8") || lowerCaseUrlPath.endsWith(".m3u")) {
|
||||
return loadM3U8Playlist(firstUrl);
|
||||
} else {
|
||||
return loadSingleFilePlaylist(firstUrl);
|
||||
}
|
||||
} else {
|
||||
return loadMultipleFilesPlaylist(std::move(urls));
|
||||
}
|
||||
}
|
||||
|
||||
QModelIndex PlaylistManager::loadM3U8Playlist(const QUrl &url)
|
||||
{
|
||||
bool PlaylistManager::loadSingleFilePlaylist(const QUrl& url) {
|
||||
// Check URL at first
|
||||
if (!checkUrl(url))
|
||||
return false;
|
||||
|
||||
// Scout playlist by given file
|
||||
auto scoutResult = PlaylistScout::scout(url, m_allowedSuffixes);
|
||||
if (!scoutResult.has_value()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Setup playlist with index
|
||||
setPlaylist(std::move(scoutResult->playlist), scoutResult->indexHint);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlaylistManager::loadMultipleFilesPlaylist(const QList<QUrl>&& urls) {
|
||||
// Check all of them are valid
|
||||
for (const auto& url : std::as_const(urls)) {
|
||||
if (!checkUrl(url)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Set playlist and return
|
||||
setPlaylist(std::move(urls));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlaylistManager::loadM3U8Playlist(const QUrl& url) {
|
||||
// Check URL at first
|
||||
if (!checkUrl(url))
|
||||
return false;
|
||||
|
||||
QList<QUrl> urls;
|
||||
QFile file(url.toLocalFile());
|
||||
if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
QList<QUrl> urls;
|
||||
while (!file.atEnd()) {
|
||||
QString line = file.readLine();
|
||||
if (line.startsWith('#')) {
|
||||
@@ -198,86 +111,185 @@ QModelIndex PlaylistManager::loadM3U8Playlist(const QUrl &url)
|
||||
}
|
||||
QFileInfo fileInfo(file);
|
||||
QUrl item = QUrl::fromUserInput(line, fileInfo.absolutePath());
|
||||
if (!checkUrl(item)) {
|
||||
return false;
|
||||
}
|
||||
urls.append(item);
|
||||
}
|
||||
return loadPlaylist(urls);
|
||||
} else {
|
||||
return {};
|
||||
return false;
|
||||
}
|
||||
|
||||
setPlaylist(std::move(urls));
|
||||
return true;
|
||||
}
|
||||
|
||||
void PlaylistManager::setPlaylist(const QList<QUrl>&& urls, std::optional<qsizetype> indexHint) {
|
||||
// Setup playlist
|
||||
m_playlist = std::move(urls);
|
||||
|
||||
// Set index with hint
|
||||
if (indexHint.has_value()) {
|
||||
m_currentIndex = *indexHint;
|
||||
}
|
||||
sanitizeIndex();
|
||||
|
||||
// Raise signal
|
||||
emit playlistChanged(totalCount());
|
||||
}
|
||||
|
||||
void PlaylistManager::clearPlaylist() {
|
||||
m_playlist.clear();
|
||||
sanitizeIndex();
|
||||
emit playlistChanged(this->totalCount());
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Playlist Operations
|
||||
|
||||
void PlaylistManager::appendToPlaylist(const QString &file)
|
||||
{
|
||||
return appendToPlaylist(urlFromString(file));
|
||||
}
|
||||
|
||||
void PlaylistManager::appendToPlaylist(const QUrl &url)
|
||||
{
|
||||
m_playlist.append(url);
|
||||
sanitizeIndex();
|
||||
emit playlistChanged(totalCount());
|
||||
}
|
||||
|
||||
bool PlaylistManager::insertIntoPlaylist(qsizetype index, const QString &file)
|
||||
{
|
||||
return insertIntoPlaylist(index, urlFromString(file));
|
||||
}
|
||||
|
||||
bool PlaylistManager::insertIntoPlaylist(qsizetype index, const QUrl &url)
|
||||
{
|
||||
if (index < 0 || index > totalCount()) {
|
||||
return false;
|
||||
} else {
|
||||
m_playlist.insert(index, url);
|
||||
sanitizeIndex();
|
||||
emit playlistChanged(totalCount());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
int PlaylistManager::totalCount() const
|
||||
bool PlaylistManager::removeFromPlaylist(qsizetype index)
|
||||
{
|
||||
return m_model.rowCount();
|
||||
if (index < 0 || index >= totalCount()) {
|
||||
return false;
|
||||
} else {
|
||||
m_playlist.removeAt(index);
|
||||
sanitizeIndex();
|
||||
emit playlistChanged(totalCount());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
QModelIndex PlaylistManager::previousIndex() const
|
||||
{
|
||||
int count = totalCount();
|
||||
if (count == 0) return {};
|
||||
// endregion
|
||||
|
||||
return m_model.index(isFirstIndex() ? count - 1 : m_currentIndex - 1);
|
||||
// region: Index Operations
|
||||
|
||||
qsizetype PlaylistManager::totalCount() const
|
||||
{
|
||||
return m_playlist.count();
|
||||
}
|
||||
|
||||
QModelIndex PlaylistManager::nextIndex() const
|
||||
std::optional<qsizetype> PlaylistManager::previousIndex() const
|
||||
{
|
||||
int count = totalCount();
|
||||
if (count == 0) return {};
|
||||
|
||||
return m_model.index(isLastIndex() ? 0 : m_currentIndex + 1);
|
||||
if (!m_currentIndex.has_value() || totalCount() <= 0) return std::nullopt;
|
||||
return (isFirstIndex() ? totalCount() - 1 : *m_currentIndex - 1);
|
||||
}
|
||||
|
||||
QModelIndex PlaylistManager::curIndex() const
|
||||
std::optional<qsizetype> PlaylistManager::nextIndex() const
|
||||
{
|
||||
return m_model.index(m_currentIndex);
|
||||
if (!m_currentIndex.has_value() || totalCount() <= 0) return std::nullopt;
|
||||
return (isLastIndex() ? 0 : *m_currentIndex + 1);
|
||||
}
|
||||
|
||||
bool PlaylistManager::isFirstIndex() const
|
||||
{
|
||||
return m_currentIndex == 0;
|
||||
return m_currentIndex.has_value() && *m_currentIndex == 0;
|
||||
}
|
||||
|
||||
bool PlaylistManager::isLastIndex() const
|
||||
{
|
||||
return m_currentIndex + 1 == totalCount();
|
||||
return m_currentIndex.has_value() && *m_currentIndex + 1 == totalCount();
|
||||
}
|
||||
|
||||
void PlaylistManager::setCurrentIndex(const QModelIndex &index)
|
||||
std::optional<qsizetype> PlaylistManager::currentIndex() const
|
||||
{
|
||||
if (index.isValid() && index.row() >= 0 && index.row() < totalCount()) {
|
||||
setProperty("currentIndex", index.row());
|
||||
return m_currentIndex;
|
||||
}
|
||||
|
||||
bool PlaylistManager::setCurrentIndex(qsizetype index)
|
||||
{
|
||||
if (!m_currentIndex.has_value()) {
|
||||
return false;
|
||||
}
|
||||
if (index < 0 || index >= totalCount()) {
|
||||
return false;
|
||||
}
|
||||
if (*m_currentIndex == index) {
|
||||
return true;
|
||||
}
|
||||
|
||||
m_currentIndex = index;
|
||||
emit currentIndexChanged(index);
|
||||
return true;
|
||||
}
|
||||
|
||||
QUrl PlaylistManager::urlByIndex(const QModelIndex &index)
|
||||
QUrl PlaylistManager::urlByIndex(qsizetype index) const
|
||||
{
|
||||
return m_model.urlByIndex(index.row());
|
||||
return m_playlist.value(index);
|
||||
}
|
||||
|
||||
QString PlaylistManager::localFileByIndex(const QModelIndex &index)
|
||||
QString PlaylistManager::localFileByIndex(qsizetype index) const
|
||||
{
|
||||
return urlByIndex(index).toLocalFile();
|
||||
}
|
||||
|
||||
bool PlaylistManager::removeAt(const QModelIndex &index)
|
||||
{
|
||||
return m_model.removeAt(index.row());
|
||||
}
|
||||
// endregion
|
||||
|
||||
void PlaylistManager::setAutoLoadFilterSuffixes(const QStringList &nameFilters)
|
||||
{
|
||||
m_model.setProperty("autoLoadFilterSuffixes", nameFilters);
|
||||
}
|
||||
// region: Misc
|
||||
|
||||
QList<QUrl> PlaylistManager::convertToUrlList(const QStringList &files)
|
||||
void PlaylistManager::sanitizeIndex()
|
||||
{
|
||||
QList<QUrl> urlList;
|
||||
for (const QString & str : std::as_const(files)) {
|
||||
QUrl url = QUrl::fromLocalFile(str);
|
||||
if (url.isValid()) {
|
||||
urlList.append(url);
|
||||
auto cnt = totalCount();
|
||||
if (cnt == 0) {
|
||||
// If playlist is empty, current index should be invalid.
|
||||
m_currentIndex = std::nullopt;
|
||||
} else {
|
||||
if (m_currentIndex.has_value()) {
|
||||
// Clamp current index in playlist range.
|
||||
// If it is already in range, not change was done.
|
||||
auto currentIndex = *m_currentIndex;
|
||||
if (currentIndex < 0) {
|
||||
m_currentIndex = 0;
|
||||
} else if (currentIndex >= cnt) {
|
||||
m_currentIndex = cnt - 1;
|
||||
} else {
|
||||
; // No change.
|
||||
}
|
||||
} else {
|
||||
// If there is no set value for current index,
|
||||
// set it at the beginning of playlist.
|
||||
m_currentIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return urlList;
|
||||
}
|
||||
|
||||
bool PlaylistManager::checkUrl(const QUrl& url)
|
||||
{
|
||||
return url.isValid() && url.isLocalFile();
|
||||
}
|
||||
|
||||
QUrl PlaylistManager::urlFromString(const QString& file)
|
||||
{
|
||||
return QUrl::fromLocalFile(file);
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
+107
-65
@@ -5,84 +5,126 @@
|
||||
#pragma once
|
||||
|
||||
#include <QUrl>
|
||||
#include <QAbstractListModel>
|
||||
|
||||
class PlaylistModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum PlaylistRole {
|
||||
UrlRole = Qt::UserRole
|
||||
};
|
||||
Q_ENUM(PlaylistRole)
|
||||
Q_PROPERTY(QStringList autoLoadFilterSuffixes MEMBER m_autoLoadSuffixes NOTIFY autoLoadFilterSuffixesChanged)
|
||||
|
||||
explicit PlaylistModel(QObject *parent = nullptr);
|
||||
~PlaylistModel() override;
|
||||
|
||||
void setPlaylist(const QList<QUrl> & urls);
|
||||
QModelIndex loadPlaylist(const QList<QUrl> & urls);
|
||||
QModelIndex loadPlaylist(const QUrl & url);
|
||||
QModelIndex appendToPlaylist(const QUrl & url);
|
||||
bool removeAt(int index);
|
||||
int indexOf(const QUrl & url) const;
|
||||
QUrl urlByIndex(int index) const;
|
||||
QStringList autoLoadFilterSuffixes() const;
|
||||
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
signals:
|
||||
void autoLoadFilterSuffixesChanged(QStringList suffixes);
|
||||
|
||||
private:
|
||||
// model data
|
||||
QList<QUrl> m_playlist;
|
||||
// properties
|
||||
QStringList m_autoLoadSuffixes = {};
|
||||
// internal
|
||||
QString m_currentDir;
|
||||
};
|
||||
#include <QObject>
|
||||
#include <optional>
|
||||
|
||||
/// The manager of playlist.
|
||||
class PlaylistManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
Q_PROPERTY(int currentIndex MEMBER m_currentIndex NOTIFY currentIndexChanged)
|
||||
Q_PROPERTY(QStringList autoLoadFilterSuffixes WRITE setAutoLoadFilterSuffixes)
|
||||
Q_PROPERTY(PlaylistModel * model READ model CONSTANT)
|
||||
|
||||
explicit PlaylistManager(QObject *parent = nullptr);
|
||||
/// Constructs a PlaylistManager with an optional parent QObject.
|
||||
explicit PlaylistManager(QObject* parent = nullptr);
|
||||
/// Destructor.
|
||||
~PlaylistManager();
|
||||
|
||||
PlaylistModel * model();
|
||||
/// Get the allowed file suffixes of this playlist.
|
||||
/// @return The current list of allowed suffixes.
|
||||
const QStringList& allowedSuffixes() const;
|
||||
/// Set the allowed file suffixes of this playlist.
|
||||
/// @remark This only works for "load the whole directory by one file" case.
|
||||
/// @param[in] nameFilters The new list of allowed suffixes.
|
||||
void setAllowedSuffixes(const QStringList& nameFilters);
|
||||
|
||||
void setPlaylist(const QList<QUrl> & url);
|
||||
Q_INVOKABLE QModelIndex loadPlaylist(const QList<QUrl> & urls);
|
||||
Q_INVOKABLE QModelIndex loadPlaylist(const QUrl & url);
|
||||
Q_INVOKABLE QModelIndex loadM3U8Playlist(const QUrl & url);
|
||||
/// Load user specified files as playlist.
|
||||
/// @remark The wrapper for another overload.
|
||||
/// @return True for successful load, otherwise false.
|
||||
bool loadPlaylist(const QStringList& files);
|
||||
/// Load user specified URLs as playlist.
|
||||
/// @remark This function automatically dispatch these files into correct sub-load functions.
|
||||
/// @return True for successful load, otherwise false.
|
||||
bool loadPlaylist(const QList<QUrl>& urls);
|
||||
/// Clear the entire playlist and reset current index.
|
||||
void clearPlaylist();
|
||||
|
||||
int totalCount() const;
|
||||
QModelIndex previousIndex() const;
|
||||
QModelIndex nextIndex() const;
|
||||
QModelIndex curIndex() const;
|
||||
/// Append a local file path to the playlist.
|
||||
void appendToPlaylist(const QString& file);
|
||||
/// Append a URL to the playlist.
|
||||
void appendToPlaylist(const QUrl& url);
|
||||
/// Insert a local file path at the given index.
|
||||
/// @return True if inserted successfully, false if index is out of range.
|
||||
bool insertIntoPlaylist(qsizetype index, const QString& file);
|
||||
/// Insert a URL at the given index.
|
||||
/// @return True if inserted successfully, false if index is out of range.
|
||||
bool insertIntoPlaylist(qsizetype index, const QUrl& url);
|
||||
/// Remove the entry at the given index.
|
||||
/// @return True if removed successfully, false if index is out of range.
|
||||
bool removeFromPlaylist(qsizetype index);
|
||||
|
||||
/// Returns the total number of items in the playlist.
|
||||
/// @return Total count of entries in the playlist.
|
||||
qsizetype totalCount() const;
|
||||
/// Try fetching previous index.
|
||||
/// @details This previous index is gallery-loop-aware.
|
||||
/// @return std::nullopt if playlist is empty, otherwise the previous index considering gallery loop.
|
||||
std::optional<qsizetype> previousIndex() const;
|
||||
/// Try fetching next index.
|
||||
/// @details This next index is gallery-loop-aware.
|
||||
/// @return std::nullopt if playlist is empty, otherwise the next index considering gallery loop.
|
||||
std::optional<qsizetype> nextIndex() const;
|
||||
/// Check whether current index is the first index.
|
||||
/// @return False if current index is not the first index or playlist is empty, otherwise true.
|
||||
bool isFirstIndex() const;
|
||||
/// Check whether current index is the last index.
|
||||
/// @return False if current index is not the last index or playlist is empty, otherwise true.
|
||||
bool isLastIndex() const;
|
||||
void setCurrentIndex(const QModelIndex & index);
|
||||
QUrl urlByIndex(const QModelIndex & index);
|
||||
QString localFileByIndex(const QModelIndex & index);
|
||||
bool removeAt(const QModelIndex & index);
|
||||
|
||||
void setAutoLoadFilterSuffixes(const QStringList &nameFilters);
|
||||
|
||||
static QList<QUrl> convertToUrlList(const QStringList & files);
|
||||
/// Returns the current index.
|
||||
/// @return std::nullopt if the playlist is empty, otherwise the current index.
|
||||
std::optional<qsizetype> currentIndex() const;
|
||||
/// Set the current index.
|
||||
/// @remark Index only can be set when playlist is not empty.
|
||||
/// @return True if successful set, including "not changed" set, otherwise false.
|
||||
/// This return value only indicates whether we can set index and given index is valid for setting.
|
||||
/// If "no changed" set occurs, this function returns true, but doesn't raise current index changed signal.
|
||||
bool setCurrentIndex(qsizetype index);
|
||||
/// Returns the URL at the given index.
|
||||
/// @return The URL if index is valid.
|
||||
/// This class guarantee that this QUrl is pointing to a local file.
|
||||
QUrl urlByIndex(qsizetype index) const;
|
||||
/// Returns the local file path at the given index.
|
||||
/// @return The local file path if index is valid.
|
||||
/// This class guarantee that local file path always is valid.
|
||||
QString localFileByIndex(qsizetype index) const;
|
||||
|
||||
signals:
|
||||
void currentIndexChanged(int index);
|
||||
void totalCountChanged(int count);
|
||||
void allowedSuffixesChanged(const QStringList& suffixes);
|
||||
/// Raised when index was changed bu setter.
|
||||
/// The argument holds the new current index.
|
||||
/// Please note that this signal will NOT be raised by the whole playlist change caused index change.
|
||||
void currentIndexChanged(qsizetype index);
|
||||
/// The whole playlist was changed, involving loading, editing and deleting some or full entries.
|
||||
/// The argument holds the total count of new play list.
|
||||
void playlistChanged(qsizetype count);
|
||||
|
||||
private:
|
||||
int m_currentIndex = -1;
|
||||
PlaylistModel m_model;
|
||||
/// The fundamental playlist setter.
|
||||
/// @details All playlist load functions will call this function to set playlist at last.
|
||||
/// @param[in] urls ALl urls in this list should be checked by checkUrl() function.
|
||||
/// @param[in] indexHint Try to set current index to this index if possible.
|
||||
void setPlaylist(const QList<QUrl>&& urls, std::optional<qsizetype> indexHint = std::nullopt);
|
||||
/// Load playlist from a single file.
|
||||
/// @details This function scans the same directory for siblings matching allowed suffixes, sorted naturally.
|
||||
/// @return True if the playlist was loaded successfully, false otherwise.
|
||||
bool loadSingleFilePlaylist(const QUrl& url);
|
||||
/// Load playlist from multiple URLs directly.
|
||||
/// @return True if all URLs are valid, false otherwise.
|
||||
bool loadMultipleFilesPlaylist(const QList<QUrl>&& urls);
|
||||
/// Load playlist from an M3U/M3U8 playlist file.
|
||||
/// @return True if loaded successfully, false otherwise.
|
||||
bool loadM3U8Playlist(const QUrl& url);
|
||||
|
||||
/// Make sure that current index is valid.
|
||||
/// @details This function usually is used by those functions manipulating playlist.
|
||||
void sanitizeIndex();
|
||||
|
||||
/// Check whether given QUrl is proper to be put into playlist.
|
||||
/// @details Only the url pointing to local file can be put into playlist.
|
||||
static bool checkUrl(const QUrl& url);
|
||||
/// Convert a string to local file into a QUrl.
|
||||
/// @param[in] file The string pointing to local file.
|
||||
static QUrl urlFromString(const QString& file);
|
||||
|
||||
std::optional<qsizetype> m_currentIndex;
|
||||
QList<QUrl> m_playlist;
|
||||
QStringList m_allowedSuffixes;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// SPDX-FileCopyrightText: 2026 yyc12345 <yyc12321@outlook.com>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "playlistscout.h"
|
||||
|
||||
#include "playlistscouts/qtscout.h"
|
||||
#include "playlistscouts/explorerscout.h"
|
||||
|
||||
std::optional<PlaylistScout::ScoutResult> PlaylistScout::scout(
|
||||
const QUrl &url, const QStringList &allowedSuffixes) {
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
// In Windows, Qt approach can not aware Windows Explorer custom sort order.
|
||||
// So we need to try using Win32 functions for fetching it at first.
|
||||
if (auto rv = PlaylistScout::Explorer::scout(url, allowedSuffixes); rv.has_value()) {
|
||||
return rv;
|
||||
}
|
||||
#endif // Q_OS_WIN
|
||||
|
||||
// TODO: We may add macOS Finder, KDE Dolphin and etc supports in future.
|
||||
|
||||
// Qt way is the last fallback.
|
||||
return PlaylistScout::Qt::scout(url, allowedSuffixes);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// SPDX-FileCopyrightText: 2026 yyc12345 <yyc12321@outlook.com>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QUrl>
|
||||
#include <QStringList>
|
||||
#include <QList>
|
||||
#include <optional>
|
||||
|
||||
namespace PlaylistScout {
|
||||
/// The result of scout
|
||||
struct ScoutResult {
|
||||
/// The final fetched playlist (it can be empty).
|
||||
/// Each item must be a QUrl pointing to a local file.
|
||||
QList<QUrl> playlist;
|
||||
/// The index of given file located in playlist.
|
||||
/// Nullopt if it is not presented in playlist.
|
||||
std::optional<qsizetype> indexHint;
|
||||
};
|
||||
|
||||
/// @brief Scout available image files in given file's parent dir with given allowed suffixes.
|
||||
/// @param[in] url The path to file whose parent dir is our target dir for finding image files.
|
||||
/// @param[in] allowedSuffixes
|
||||
/// Each items in this list is a wildcard pattern for matching allowed file name, like \c *.png .
|
||||
/// The fetched item will be preserved if its name matching any of them.
|
||||
/// If this list is empty, there is no restrictions for finding files.
|
||||
/// @return Scout result if success, otherwise nullopt.
|
||||
std::optional<ScoutResult> scout(const QUrl &url, const QStringList &allowedSuffixes);
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
// SPDX-FileCopyrightText: 2026 yyc12345 <yyc12321@outlook.com>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "explorerscout.h"
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
|
||||
#include <QDir>
|
||||
#include <QDebug>
|
||||
#include <QFileInfo>
|
||||
#include <QRegularExpression>
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include <exdisp.h> // IShellWindows, CLSID_ShellWindows
|
||||
#include <servprov.h> // IServiceProvider
|
||||
#include <shlguid.h> // SID_STopLevelBrowser
|
||||
#include <shlobj.h>
|
||||
|
||||
// YYC MARK:
|
||||
// We use custom RAII wrappers instead of existing COM smart pointers because the
|
||||
// project may be built with MinGW, which does not have good compatibility with
|
||||
// CComPtr and Microsoft's WRL ComPtr (<wrl/client.h>).
|
||||
// Furthermore, PIDLs are not COM objects (they are freed via ILFree, not Release),
|
||||
// so no standard smart pointer covers them regardless of compiler.
|
||||
|
||||
/// RAII wrapper for COM initialization (CoInitialize / CoUninitialize).
|
||||
/// Must be declared before any COM objects (e.g., ComPtr, UniquePidl) so that
|
||||
/// CoUninitialize runs after all COM objects are released during scope exit.
|
||||
class ComGuard {
|
||||
public:
|
||||
ComGuard() : m_hr(CoInitialize(nullptr)) {}
|
||||
~ComGuard() { if (SUCCEEDED(m_hr)) CoUninitialize(); }
|
||||
ComGuard(const ComGuard&) = delete;
|
||||
ComGuard& operator=(const ComGuard&) = delete;
|
||||
|
||||
bool IsInitialized() const { return SUCCEEDED(m_hr); }
|
||||
|
||||
private:
|
||||
HRESULT m_hr;
|
||||
};
|
||||
|
||||
/// RAII wrapper for COM interface pointers.
|
||||
template <typename T>
|
||||
class ComPtr {
|
||||
public:
|
||||
ComPtr() = default;
|
||||
ComPtr(std::nullptr_t) {}
|
||||
~ComPtr() { Reset(); }
|
||||
ComPtr(const ComPtr&) = delete;
|
||||
ComPtr& operator=(const ComPtr&) = delete;
|
||||
ComPtr(ComPtr&& other) noexcept : m_ptr(other.m_ptr) { other.m_ptr = nullptr; }
|
||||
ComPtr& operator=(ComPtr&& other) noexcept {
|
||||
if (this != &other) {
|
||||
Reset();
|
||||
m_ptr = other.m_ptr;
|
||||
other.m_ptr = nullptr;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
T* operator->() const { return m_ptr; }
|
||||
T* Get() const { return m_ptr; }
|
||||
T** GetAddressOf() { return &m_ptr; }
|
||||
explicit operator bool() const { return m_ptr != nullptr; }
|
||||
void Reset() { if (m_ptr) { m_ptr->Release(); m_ptr = nullptr; } }
|
||||
|
||||
private:
|
||||
T* m_ptr = nullptr;
|
||||
};
|
||||
|
||||
/// RAII wrapper for PIDL (Item ID List).
|
||||
class UniquePidl {
|
||||
public:
|
||||
UniquePidl() = default;
|
||||
explicit UniquePidl(LPITEMIDLIST pidl) : m_pidl(pidl) {}
|
||||
~UniquePidl() { Reset(); }
|
||||
UniquePidl(const UniquePidl&) = delete;
|
||||
UniquePidl& operator=(const UniquePidl&) = delete;
|
||||
UniquePidl(UniquePidl&& other) noexcept : m_pidl(other.m_pidl) { other.m_pidl = nullptr; }
|
||||
UniquePidl& operator=(UniquePidl&& other) noexcept {
|
||||
if (this != &other) {
|
||||
Reset();
|
||||
m_pidl = other.m_pidl;
|
||||
other.m_pidl = nullptr;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
LPCITEMIDLIST Get() const { return m_pidl; }
|
||||
LPITEMIDLIST* GetAddressOf() { return &m_pidl; }
|
||||
explicit operator bool() const { return m_pidl != nullptr; }
|
||||
void Reset() { if (m_pidl) { ILFree(m_pidl); m_pidl = nullptr; } }
|
||||
|
||||
private:
|
||||
LPITEMIDLIST m_pidl = nullptr;
|
||||
};
|
||||
|
||||
/// Outputs a diagnostic warning for debugging Windows Explorer integration failures.
|
||||
static void ReportError(const QString& detail)
|
||||
{
|
||||
qWarning("Windows Explorer file list fetch error: %s",
|
||||
qPrintable(detail));
|
||||
}
|
||||
|
||||
/// Normalizes a Windows directory path for case-insensitive comparison:
|
||||
/// converts to native separators, lowercases, and ensures a trailing backslash.
|
||||
static QString NormalizePath(const QString& path)
|
||||
{
|
||||
QString result = QDir::toNativeSeparators(path).toLower();
|
||||
if (!result.endsWith(QChar('\\'))) {
|
||||
result += QChar('\\');
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Checks whether the Explorer window represented by the given IDispatch is
|
||||
/// displaying the target folder. Returns its IFolderView2 on match, or null.
|
||||
static ComPtr<IFolderView2> TryMatchWindow(IDispatch* dispatch, const QString& normalizedTarget)
|
||||
{
|
||||
// Navigate: IDispatch -> IServiceProvider -> IShellBrowser.
|
||||
ComPtr<IServiceProvider> serviceProvider;
|
||||
if (FAILED(dispatch->QueryInterface(IID_PPV_ARGS(serviceProvider.GetAddressOf())))) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ComPtr<IShellBrowser> shellBrowser;
|
||||
if (FAILED(serviceProvider->QueryService(SID_STopLevelBrowser,
|
||||
IID_PPV_ARGS(shellBrowser.GetAddressOf())))) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// IShellBrowser -> active IShellView.
|
||||
ComPtr<IShellView> shellView;
|
||||
if (FAILED(shellBrowser->QueryActiveShellView(shellView.GetAddressOf()))) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Retrieve the folder's path via IPersistIDList.
|
||||
ComPtr<IPersistIDList> persistIdList;
|
||||
if (FAILED(shellView->QueryInterface(IID_PPV_ARGS(persistIdList.GetAddressOf())))) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UniquePidl folderPidl;
|
||||
if (FAILED(persistIdList->GetIDList(folderPidl.GetAddressOf()))) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
PWSTR pathStr = nullptr;
|
||||
if (FAILED(SHGetNameFromIDList(folderPidl.Get(), SIGDN_DESKTOPABSOLUTEPARSING, &pathStr))) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QString windowPath = NormalizePath(QString::fromWCharArray(pathStr));
|
||||
CoTaskMemFree(pathStr);
|
||||
|
||||
if (windowPath != normalizedTarget) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Path matches — query IFolderView2 from the ShellView.
|
||||
ComPtr<IFolderView2> folderView2;
|
||||
if (FAILED(shellView->QueryInterface(IID_PPV_ARGS(folderView2.GetAddressOf())))) {
|
||||
return nullptr;
|
||||
}
|
||||
return folderView2;
|
||||
}
|
||||
|
||||
/// Tries to find a live Explorer window that is currently displaying the given
|
||||
/// folder path. Returns the IFolderView2 from that window, or an empty ComPtr.
|
||||
///
|
||||
/// Explorer's sort order is stored only in the running Explorer process's memory.
|
||||
/// Creating a new shell view via IShellFolder::CreateViewObject yields a default
|
||||
/// sort (always by name), so we must find the actual live IFolderView2 instead.
|
||||
///
|
||||
/// Reference:
|
||||
/// - https://www.stephenli.ca/getting-windows-explorers-current-directory-sort-order
|
||||
/// - https://github.com/d2phap/ImageGlass/blob/75a296287f338edcf409eee3917531b25ff5d927/v9/Components/ImageGlass.Base/FileSystem/FileFinder.cs
|
||||
/// - NuGet Package: D2Phap.EggShell
|
||||
static ComPtr<IFolderView2> FindLiveFolderView(const QString& targetFolderPath)
|
||||
{
|
||||
// Create the Shell windows collection (all open Explorer windows).
|
||||
ComPtr<IShellWindows> shellWindows;
|
||||
if (FAILED(CoCreateInstance(CLSID_ShellWindows, nullptr, CLSCTX_ALL,
|
||||
IID_PPV_ARGS(shellWindows.GetAddressOf())))) {
|
||||
ReportError("Failed to create IShellWindows instance");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QString normalizedTarget = NormalizePath(targetFolderPath);
|
||||
|
||||
// 1. Try the Desktop window first — it may contain the file the user opened.
|
||||
{
|
||||
VARIANT varLoc;
|
||||
VariantInit(&varLoc);
|
||||
varLoc.vt = VT_I4;
|
||||
varLoc.lVal = 0;
|
||||
|
||||
VARIANT varLocRoot;
|
||||
VariantInit(&varLocRoot);
|
||||
|
||||
long desktopHwnd = 0;
|
||||
ComPtr<IDispatch> desktopDispatch;
|
||||
HRESULT findHr = shellWindows->FindWindowSW(
|
||||
&varLoc, &varLocRoot, SWC_DESKTOP,
|
||||
&desktopHwnd, SWFO_NEEDDISPATCH,
|
||||
desktopDispatch.GetAddressOf());
|
||||
|
||||
VariantClear(&varLoc);
|
||||
VariantClear(&varLocRoot);
|
||||
|
||||
if (SUCCEEDED(findHr) && desktopDispatch) {
|
||||
ComPtr<IFolderView2> result = TryMatchWindow(desktopDispatch.Get(), normalizedTarget);
|
||||
if (result) return result;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Iterate regular Explorer windows.
|
||||
LONG count = 0;
|
||||
shellWindows->get_Count(&count);
|
||||
|
||||
for (LONG i = 0; i < count; i++) {
|
||||
VARIANT varIndex;
|
||||
VariantInit(&varIndex);
|
||||
varIndex.vt = VT_I4;
|
||||
varIndex.lVal = i;
|
||||
|
||||
ComPtr<IDispatch> dispatch;
|
||||
HRESULT itemHr = shellWindows->Item(varIndex, dispatch.GetAddressOf());
|
||||
VariantClear(&varIndex);
|
||||
|
||||
if (FAILED(itemHr) || !dispatch) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ComPtr<IFolderView2> result = TryMatchWindow(dispatch.Get(), normalizedTarget);
|
||||
if (result) return result;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::optional<PlaylistScout::ScoutResult> PlaylistScout::Explorer::scout(
|
||||
const QUrl &url, const QStringList &allowedSuffixes) {
|
||||
PlaylistScout::ScoutResult rv;
|
||||
|
||||
QFileInfo info(url.toLocalFile());
|
||||
QString folderPath = info.path();
|
||||
QString currentFileName = info.fileName();
|
||||
|
||||
// Pre-compile wildcard suffix filters into regular expressions for performance.
|
||||
QList<QRegularExpression> nameRegexps;
|
||||
for (const QString &pattern : allowedSuffixes) {
|
||||
nameRegexps.append(QRegularExpression(
|
||||
QRegularExpression::wildcardToRegularExpression(pattern),
|
||||
QRegularExpression::CaseInsensitiveOption));
|
||||
}
|
||||
|
||||
// Initialize COM. The ComGuard must be declared before any COM objects
|
||||
// below so that CoUninitialize runs after all of them are released.
|
||||
ComGuard comGuard;
|
||||
if (!comGuard.IsInitialized()) {
|
||||
ReportError("COM initialization failed");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Find a live Explorer window that is currently displaying the target folder.
|
||||
ComPtr<IFolderView2> folderView2 = FindLiveFolderView(folderPath);
|
||||
if (!folderView2) {
|
||||
ReportError("No open Explorer window matches the folder: " + folderPath);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Get items already sorted in Explorer's view order, as an IShellItemArray.
|
||||
// SVGIO_ALLVIEW returns all items; SVGIO_FLAG_VIEWORDER respects the view's sort.
|
||||
ComPtr<IShellItemArray> itemArray;
|
||||
if (FAILED(folderView2->Items(SVGIO_ALLVIEW | SVGIO_FLAG_VIEWORDER,
|
||||
IID_PPV_ARGS(itemArray.GetAddressOf())))) {
|
||||
ReportError("IFolderView2::Items(SVGIO_ALLVIEW | SVGIO_FLAG_VIEWORDER) failed");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
DWORD itemCount = 0;
|
||||
itemArray->GetCount(&itemCount);
|
||||
|
||||
// Enumerate sorted items, filter by pattern, and build the playlist.
|
||||
for (DWORD i = 0; i < itemCount; i++) {
|
||||
ComPtr<IShellItem> shellItem;
|
||||
if (FAILED(itemArray->GetItemAt(i, shellItem.GetAddressOf()))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the full parsing path of the item.
|
||||
PWSTR pathStr = nullptr;
|
||||
if (FAILED(shellItem->GetDisplayName(SIGDN_DESKTOPABSOLUTEPARSING, &pathStr)) || !pathStr) {
|
||||
continue;
|
||||
}
|
||||
QString filePath = QString::fromWCharArray(pathStr);
|
||||
CoTaskMemFree(pathStr);
|
||||
|
||||
// Skip directories — the view may include folders alongside files.
|
||||
QFileInfo fileInfo(filePath);
|
||||
if (!fileInfo.isFile()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QString fileName = fileInfo.fileName();
|
||||
|
||||
// Match against compiled wildcard patterns.
|
||||
bool matches = nameRegexps.isEmpty();
|
||||
for (const QRegularExpression &re : std::as_const(nameRegexps)) {
|
||||
if (re.match(fileName).hasMatch()) {
|
||||
matches = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!matches) {
|
||||
continue;
|
||||
}
|
||||
|
||||
rv.playlist.append(QUrl::fromLocalFile(filePath));
|
||||
if (fileName == currentFileName) {
|
||||
rv.indexHint = rv.playlist.size() - 1;
|
||||
}
|
||||
}
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
#endif // Q_OS_WIN
|
||||
@@ -0,0 +1,16 @@
|
||||
// SPDX-FileCopyrightText: 2026 yyc12345 <yyc12321@outlook.com>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../playlistscout.h"
|
||||
#include <QtSystemDetection>
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
|
||||
namespace PlaylistScout::Explorer {
|
||||
std::optional<ScoutResult> scout(const QUrl &url, const QStringList &allowedSuffixes);
|
||||
}
|
||||
|
||||
#endif // Q_OS_WIN
|
||||
@@ -0,0 +1,40 @@
|
||||
// SPDX-FileCopyrightText: 2025 Gary Wang <git@blumia.net>
|
||||
// SPDX-FileCopyrightText: 2026 yyc12345 <yyc12321@outlook.com>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "qtscout.h"
|
||||
|
||||
#include <QFileInfo>
|
||||
#include <QDir>
|
||||
#include <QCollator>
|
||||
|
||||
std::optional<PlaylistScout::ScoutResult> PlaylistScout::Qt::scout(
|
||||
const QUrl &url, const QStringList &allowedSuffixes) {
|
||||
PlaylistScout::ScoutResult rv;
|
||||
|
||||
QFileInfo info(url.toLocalFile());
|
||||
QDir dir(info.path());
|
||||
QString currentFileName = info.fileName();
|
||||
|
||||
QStringList entryList = dir.entryList(
|
||||
allowedSuffixes, QDir::Files | QDir::NoSymLinks, QDir::NoSort);
|
||||
|
||||
QCollator collator;
|
||||
collator.setNumericMode(true);
|
||||
|
||||
std::sort(entryList.begin(), entryList.end(), collator);
|
||||
|
||||
for (qsizetype i = 0; i < entryList.count(); i++) {
|
||||
const QString &fileName = entryList.at(i);
|
||||
const QString &oneEntry = dir.absoluteFilePath(fileName);
|
||||
const QUrl &url = QUrl::fromLocalFile(oneEntry);
|
||||
|
||||
rv.playlist.append(url);
|
||||
if (fileName == currentFileName) {
|
||||
rv.indexHint = i;
|
||||
}
|
||||
}
|
||||
|
||||
return rv;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// SPDX-FileCopyrightText: 2026 yyc12345 <yyc12321@outlook.com>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../playlistscout.h"
|
||||
|
||||
namespace PlaylistScout::Qt {
|
||||
std::optional<ScoutResult> scout(const QUrl &url, const QStringList &allowedSuffixes);
|
||||
}
|
||||
+14
-107
@@ -13,6 +13,14 @@
|
||||
#include <QKeySequence>
|
||||
#include <QMetaEnum>
|
||||
|
||||
Settings *Settings::instance()
|
||||
{
|
||||
// SAFETY:
|
||||
// This is a global singleton and it is leaked by design.
|
||||
static Settings* INSTANCE = new Settings(nullptr);
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
namespace QEnumHelper
|
||||
{
|
||||
template <typename E>
|
||||
@@ -34,27 +42,6 @@ namespace QEnumHelper
|
||||
}
|
||||
}
|
||||
|
||||
Settings *Settings::m_settings_instance = nullptr;
|
||||
|
||||
Settings *Settings::instance()
|
||||
{
|
||||
if (!m_settings_instance) {
|
||||
m_settings_instance = new Settings;
|
||||
}
|
||||
|
||||
return m_settings_instance;
|
||||
}
|
||||
|
||||
bool Settings::stayOnTop() const
|
||||
{
|
||||
return m_qsettings->value("stay_on_top", true).toBool();
|
||||
}
|
||||
|
||||
bool Settings::useBuiltInCloseAnimation() const
|
||||
{
|
||||
return m_qsettings->value("use_built_in_close_animation", true).toBool();
|
||||
}
|
||||
|
||||
bool Settings::useLightCheckerboard() const
|
||||
{
|
||||
return m_qsettings->value("use_light_checkerboard", false).toBool();
|
||||
@@ -65,11 +52,6 @@ bool Settings::loopGallery() const
|
||||
return m_qsettings->value("loop_gallery", true).toBool();
|
||||
}
|
||||
|
||||
bool Settings::autoLongImageMode() const
|
||||
{
|
||||
return m_qsettings->value("auto_long_image_mode", true).toBool();
|
||||
}
|
||||
|
||||
bool Settings::svgTiny12Only() const
|
||||
{
|
||||
// Qt 6.7.0's SVG support is terrible caused by huge memory usage, see QTBUG-124287
|
||||
@@ -83,46 +65,24 @@ bool Settings::svgTiny12Only() const
|
||||
#endif // QT_VERSION < QT_VERSION_CHECK(6, 9, 3)
|
||||
}
|
||||
|
||||
Settings::DoubleClickBehavior Settings::doubleClickBehavior() const
|
||||
{
|
||||
QString result = m_qsettings->value("double_click_behavior", "Close").toString();
|
||||
|
||||
return QEnumHelper::fromString<DoubleClickBehavior>(result, DoubleClickBehavior::Close);
|
||||
}
|
||||
|
||||
Settings::MouseWheelBehavior Settings::mouseWheelBehavior() const
|
||||
{
|
||||
QString result = m_qsettings->value("mouse_wheel_behavior", "Zoom").toString();
|
||||
|
||||
QString result = m_qsettings->value("mouse_wheel_behavior", "").toString();
|
||||
return QEnumHelper::fromString<MouseWheelBehavior>(result, MouseWheelBehavior::Zoom);
|
||||
}
|
||||
|
||||
Settings::WindowSizeBehavior Settings::initWindowSizeBehavior() const
|
||||
{
|
||||
QString result = m_qsettings->value("init_window_size_behavior", "Auto").toString();
|
||||
|
||||
return QEnumHelper::fromString<WindowSizeBehavior>(result, WindowSizeBehavior::Auto);
|
||||
QString result = m_qsettings->value("init_window_size_behavior", "").toString();
|
||||
return QEnumHelper::fromString<WindowSizeBehavior>(result, WindowSizeBehavior::Windowed);
|
||||
}
|
||||
|
||||
Qt::HighDpiScaleFactorRoundingPolicy Settings::hiDpiScaleFactorBehavior() const
|
||||
{
|
||||
QString result = m_qsettings->value("hidpi_scale_factor_behavior", "PassThrough").toString();
|
||||
|
||||
QString result = m_qsettings->value("hidpi_scale_factor_behavior", "").toString();
|
||||
return QEnumHelper::fromString<Qt::HighDpiScaleFactorRoundingPolicy>(result, Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
|
||||
}
|
||||
|
||||
void Settings::setStayOnTop(bool on)
|
||||
{
|
||||
m_qsettings->setValue("stay_on_top", on);
|
||||
m_qsettings->sync();
|
||||
}
|
||||
|
||||
void Settings::setUseBuiltInCloseAnimation(bool on)
|
||||
{
|
||||
m_qsettings->setValue("use_built_in_close_animation", on);
|
||||
m_qsettings->sync();
|
||||
}
|
||||
|
||||
void Settings::setUseLightCheckerboard(bool light)
|
||||
{
|
||||
m_qsettings->setValue("use_light_checkerboard", light);
|
||||
@@ -135,24 +95,12 @@ void Settings::setLoopGallery(bool on)
|
||||
m_qsettings->sync();
|
||||
}
|
||||
|
||||
void Settings::setAutoLongImageMode(bool on)
|
||||
{
|
||||
m_qsettings->setValue("auto_long_image_mode", on);
|
||||
m_qsettings->sync();
|
||||
}
|
||||
|
||||
void Settings::setSvgTiny12Only(bool on)
|
||||
{
|
||||
m_qsettings->setValue("svg_tiny12_only", on);
|
||||
m_qsettings->sync();
|
||||
}
|
||||
|
||||
void Settings::setDoubleClickBehavior(DoubleClickBehavior dcb)
|
||||
{
|
||||
m_qsettings->setValue("double_click_behavior", QEnumHelper::toString(dcb));
|
||||
m_qsettings->sync();
|
||||
}
|
||||
|
||||
void Settings::setMouseWheelBehavior(MouseWheelBehavior mwb)
|
||||
{
|
||||
m_qsettings->setValue("mouse_wheel_behavior", QEnumHelper::toString(mwb));
|
||||
@@ -171,47 +119,6 @@ void Settings::setHiDpiScaleFactorBehavior(Qt::HighDpiScaleFactorRoundingPolicy
|
||||
m_qsettings->sync();
|
||||
}
|
||||
|
||||
void Settings::applyUserShortcuts(QWidget *widget)
|
||||
{
|
||||
m_qsettings->beginGroup("shortcuts");
|
||||
const QStringList shortcutNames = m_qsettings->allKeys();
|
||||
for (const QString & name : shortcutNames) {
|
||||
QList<QKeySequence> shortcuts = m_qsettings->value(name).value<QList<QKeySequence>>();
|
||||
setShortcutsForAction(widget, name, shortcuts, false);
|
||||
}
|
||||
m_qsettings->endGroup();
|
||||
}
|
||||
|
||||
bool Settings::setShortcutsForAction(QWidget *widget, const QString &objectName,
|
||||
QList<QKeySequence> shortcuts, bool writeConfig)
|
||||
{
|
||||
QAction * targetAction = nullptr;
|
||||
for (QAction * action : widget->actions()) {
|
||||
if (action->objectName() == objectName) {
|
||||
targetAction = action;
|
||||
} else {
|
||||
for (const QKeySequence & shortcut : std::as_const(shortcuts)) {
|
||||
if (action->shortcuts().contains(shortcut)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (targetAction) {
|
||||
targetAction->setShortcuts(shortcuts);
|
||||
}
|
||||
|
||||
if (targetAction && writeConfig) {
|
||||
m_qsettings->beginGroup("shortcuts");
|
||||
m_qsettings->setValue(objectName, QVariant::fromValue(shortcuts));
|
||||
m_qsettings->endGroup();
|
||||
m_qsettings->sync();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#if defined(FLAG_PORTABLE_MODE_SUPPORT) && defined(Q_OS_WIN)
|
||||
#include <windows.h>
|
||||
// QCoreApplication::applicationDirPath() parses the "applicationDirPath" from arg0, which...
|
||||
@@ -230,8 +137,8 @@ QString getApplicationDirPath()
|
||||
}
|
||||
#endif // defined(FLAG_PORTABLE_MODE_SUPPORT) && defined(Q_OS_WIN)
|
||||
|
||||
Settings::Settings()
|
||||
: QObject(qApp)
|
||||
Settings::Settings(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
QString configPath;
|
||||
|
||||
|
||||
+1
-24
@@ -11,14 +11,6 @@ class Settings : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum DoubleClickBehavior {
|
||||
Ignore,
|
||||
Close,
|
||||
Maximize,
|
||||
FullScreen,
|
||||
};
|
||||
Q_ENUM(DoubleClickBehavior)
|
||||
|
||||
enum MouseWheelBehavior {
|
||||
Zoom,
|
||||
Switch,
|
||||
@@ -26,7 +18,6 @@ public:
|
||||
Q_ENUM(MouseWheelBehavior)
|
||||
|
||||
enum WindowSizeBehavior {
|
||||
Auto,
|
||||
Maximized,
|
||||
Windowed,
|
||||
};
|
||||
@@ -34,36 +25,22 @@ public:
|
||||
|
||||
static Settings *instance();
|
||||
|
||||
bool stayOnTop() const;
|
||||
bool useBuiltInCloseAnimation() const;
|
||||
bool useLightCheckerboard() const;
|
||||
bool loopGallery() const;
|
||||
bool autoLongImageMode() const;
|
||||
bool svgTiny12Only() const;
|
||||
DoubleClickBehavior doubleClickBehavior() const;
|
||||
MouseWheelBehavior mouseWheelBehavior() const;
|
||||
WindowSizeBehavior initWindowSizeBehavior() const;
|
||||
Qt::HighDpiScaleFactorRoundingPolicy hiDpiScaleFactorBehavior() const;
|
||||
|
||||
void setStayOnTop(bool on);
|
||||
void setUseBuiltInCloseAnimation(bool on);
|
||||
void setUseLightCheckerboard(bool light);
|
||||
void setLoopGallery(bool on);
|
||||
void setAutoLongImageMode(bool on);
|
||||
void setSvgTiny12Only(bool on);
|
||||
void setDoubleClickBehavior(DoubleClickBehavior dcb);
|
||||
void setMouseWheelBehavior(MouseWheelBehavior mwb);
|
||||
void setInitWindowSizeBehavior(WindowSizeBehavior wsb);
|
||||
void setHiDpiScaleFactorBehavior(Qt::HighDpiScaleFactorRoundingPolicy hidpi);
|
||||
|
||||
void applyUserShortcuts(QWidget * widget);
|
||||
bool setShortcutsForAction(QWidget * widget, const QString & objectName,
|
||||
QList<QKeySequence> shortcuts, bool writeConfig = true);
|
||||
|
||||
private:
|
||||
Settings();
|
||||
|
||||
static Settings *m_settings_instance;
|
||||
Settings(QObject *parent = nullptr);
|
||||
|
||||
QSettings *m_qsettings;
|
||||
|
||||
|
||||
+97
-171
@@ -3,158 +3,94 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "settingsdialog.h"
|
||||
#include "ui_settingsdialog.h"
|
||||
|
||||
#include "settings.h"
|
||||
#include "shortcutedit.h"
|
||||
|
||||
#include <QAction>
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QFormLayout>
|
||||
#include <QKeySequenceEdit>
|
||||
#include <QScrollArea>
|
||||
#include <QSplitter>
|
||||
#include <QApplication>
|
||||
#include <QTimer>
|
||||
#include <QStringListModel>
|
||||
#include <QMessageBox>
|
||||
#include <type_traits>
|
||||
|
||||
template <typename E, typename = std::enable_if_t<std::is_enum_v<E>>>
|
||||
class EnumComboBoxTransformer {
|
||||
public:
|
||||
using Pair = QPair<E, QString>;
|
||||
using Pairs = QList<QPair<E, QString>>;
|
||||
|
||||
EnumComboBoxTransformer(Pairs &&pairs) : pairs(std::move(pairs)) {}
|
||||
~EnumComboBoxTransformer() = default;
|
||||
Q_DISABLE_COPY_MOVE(EnumComboBoxTransformer)
|
||||
|
||||
/// Build Qt model used for QComboBox
|
||||
QStringListModel* buildComboBoxModel(QObject *parent = nullptr) const {
|
||||
QStringList dropDownEntries;
|
||||
for (const auto& pair : std::as_const(this->pairs)) {
|
||||
dropDownEntries.append(pair.second);
|
||||
}
|
||||
return new QStringListModel(dropDownEntries, parent);
|
||||
}
|
||||
/// Convert QComboBox index to enum value.
|
||||
E to_value(int index) const {
|
||||
return this->pairs.at(static_cast<qsizetype>(index)).first;
|
||||
}
|
||||
/// Convert enum value to QComboBox index.
|
||||
int to_index(E value) const {
|
||||
for (qsizetype i = 0; i < pairs.size(); ++i) {
|
||||
if (pairs.at(i).first == value) {
|
||||
return static_cast<int>(i);
|
||||
}
|
||||
}
|
||||
// Value not found
|
||||
throw std::invalid_argument("invalid enum value for finding index");
|
||||
}
|
||||
|
||||
private:
|
||||
Pairs pairs;
|
||||
};
|
||||
|
||||
static EnumComboBoxTransformer<Settings::MouseWheelBehavior, QString> MW_OPTIONS {
|
||||
{
|
||||
{ Settings::MouseWheelBehavior::Zoom, QCoreApplication::translate("SettingsDialog", "Zoom in and out") },
|
||||
{ Settings::MouseWheelBehavior::Switch, QCoreApplication::translate("SettingsDialog", "View next or previous item") }
|
||||
}
|
||||
};
|
||||
|
||||
static EnumComboBoxTransformer<Settings::WindowSizeBehavior, QString> IWS_OPTIONS {
|
||||
{
|
||||
{ Settings::WindowSizeBehavior::Maximized, QCoreApplication::translate("SettingsDialog", "Maximized") },
|
||||
{ Settings::WindowSizeBehavior::Windowed, QCoreApplication::translate("SettingsDialog", "Windowed") }
|
||||
}
|
||||
};
|
||||
|
||||
static EnumComboBoxTransformer<Qt::HighDpiScaleFactorRoundingPolicy, QString> HIDPI_OPTIONS {
|
||||
{
|
||||
{ Qt::HighDpiScaleFactorRoundingPolicy::Round, QCoreApplication::translate("SettingsDialog", "Round (Integer scaling)", "This option means round up for .5 and above") },
|
||||
{ Qt::HighDpiScaleFactorRoundingPolicy::Ceil, QCoreApplication::translate("SettingsDialog", "Ceil (Integer scaling)", "This option means always round up") },
|
||||
{ Qt::HighDpiScaleFactorRoundingPolicy::Floor, QCoreApplication::translate("SettingsDialog", "Floor (Integer scaling)", "This option means always round down") },
|
||||
{ Qt::HighDpiScaleFactorRoundingPolicy::PassThrough, QCoreApplication::translate("SettingsDialog", "Follow system (Fractional scaling)", "This option means don't round") }
|
||||
}
|
||||
};
|
||||
|
||||
SettingsDialog::SettingsDialog(QWidget *parent)
|
||||
: QDialog(parent)
|
||||
, m_stayOnTop(new QCheckBox)
|
||||
, m_useBuiltInCloseAnimation(new QCheckBox)
|
||||
, m_useLightCheckerboard(new QCheckBox)
|
||||
, m_loopGallery(new QCheckBox)
|
||||
, m_autoLongImageMode(new QCheckBox)
|
||||
, m_svgTiny12Only(new QCheckBox)
|
||||
, m_doubleClickBehavior(new QComboBox)
|
||||
, m_mouseWheelBehavior(new QComboBox)
|
||||
, m_initWindowSizeBehavior(new QComboBox)
|
||||
, m_hiDpiRoundingPolicyBehavior(new QComboBox)
|
||||
: QDialog(parent), ui(new QT_PREPEND_NAMESPACE(Ui::SettingsDialog))
|
||||
{
|
||||
this->setWindowTitle(tr("Settings"));
|
||||
ui->setupUi(this);
|
||||
setWindowFlag(Qt::WindowContextHelpButtonHint, false);
|
||||
|
||||
QHBoxLayout * mainLayout = new QHBoxLayout(this);
|
||||
QTabWidget * settingsTabs = new QTabWidget(this);
|
||||
mainLayout->addWidget(settingsTabs);
|
||||
|
||||
QWidget * settingsFormHolder = new QWidget;
|
||||
QFormLayout * settingsForm = new QFormLayout(settingsFormHolder);
|
||||
settingsTabs->addTab(settingsFormHolder, tr("Options"));
|
||||
|
||||
QSplitter * shortcutEditorSplitter = new QSplitter;
|
||||
shortcutEditorSplitter->setOrientation(Qt::Vertical);
|
||||
shortcutEditorSplitter->setChildrenCollapsible(false);
|
||||
QScrollArea * shortcutScrollArea = new QScrollArea;
|
||||
shortcutEditorSplitter->addWidget(shortcutScrollArea);
|
||||
shortcutScrollArea->setWidgetResizable(true);
|
||||
shortcutScrollArea->setMinimumHeight(200);
|
||||
QWidget * shortcutsFormHolder = new QWidget;
|
||||
QFormLayout * shortcutsForm = new QFormLayout(shortcutsFormHolder);
|
||||
shortcutScrollArea->setWidget(shortcutsFormHolder);
|
||||
settingsTabs->addTab(shortcutEditorSplitter, tr("Shortcuts"));
|
||||
|
||||
for (const QAction * action : parent->actions()) {
|
||||
ShortcutEdit * shortcutEdit = new ShortcutEdit;
|
||||
shortcutEdit->setObjectName(QLatin1String("shortcut_") + action->objectName());
|
||||
shortcutEdit->setShortcuts(action->shortcuts());
|
||||
shortcutsForm->addRow(action->text(), shortcutEdit);
|
||||
connect(shortcutEdit, &ShortcutEdit::editButtonClicked, this, [=](){
|
||||
if (shortcutEditorSplitter->count() == 1) shortcutEditorSplitter->addWidget(new QWidget);
|
||||
ShortcutEditor * shortcutEditor = new ShortcutEditor(shortcutEdit);
|
||||
shortcutEditor->setDescription(tr("Editing shortcuts for action \"%1\":").arg(action->text()));
|
||||
QWidget * oldEditor = shortcutEditorSplitter->replaceWidget(1, shortcutEditor);
|
||||
shortcutEditorSplitter->setSizes({shortcutEditorSplitter->height(), 1});
|
||||
oldEditor->deleteLater();
|
||||
});
|
||||
connect(shortcutEdit, &ShortcutEdit::applyShortcutsRequested, this, [=](QList<QKeySequence> newShortcuts){
|
||||
bool succ = Settings::instance()->setShortcutsForAction(parent, shortcutEdit->objectName().mid(9),
|
||||
newShortcuts);
|
||||
if (!succ) {
|
||||
QMessageBox::warning(this, tr("Failed to set shortcuts"),
|
||||
tr("Please check if shortcuts are duplicated with existing shortcuts."));
|
||||
}
|
||||
shortcutEdit->setShortcuts(action->shortcuts());
|
||||
});
|
||||
}
|
||||
|
||||
static QList< QPair<Settings::DoubleClickBehavior, QString> > _dc_options {
|
||||
{ Settings::DoubleClickBehavior::Ignore, tr("Do nothing") },
|
||||
{ Settings::DoubleClickBehavior::Close, tr("Close the window") },
|
||||
{ Settings::DoubleClickBehavior::Maximize, tr("Toggle maximize") },
|
||||
{ Settings::DoubleClickBehavior::FullScreen, tr("Toggle fullscreen") }
|
||||
};
|
||||
|
||||
static QList< QPair<Settings::MouseWheelBehavior, QString> > _mw_options {
|
||||
{ Settings::MouseWheelBehavior::Zoom, tr("Zoom in and out") },
|
||||
{ Settings::MouseWheelBehavior::Switch, tr("View next or previous item") }
|
||||
};
|
||||
|
||||
static QList< QPair<Settings::WindowSizeBehavior, QString> > _iws_options {
|
||||
{ Settings::WindowSizeBehavior::Auto, tr("Auto size") },
|
||||
{ Settings::WindowSizeBehavior::Maximized, tr("Maximized") },
|
||||
{ Settings::WindowSizeBehavior::Windowed, tr("Windowed") }
|
||||
};
|
||||
|
||||
static QList< QPair<Qt::HighDpiScaleFactorRoundingPolicy, QString> > _hidpi_options {
|
||||
{ Qt::HighDpiScaleFactorRoundingPolicy::Round, tr("Round (Integer scaling)", "This option means round up for .5 and above") },
|
||||
{ Qt::HighDpiScaleFactorRoundingPolicy::Ceil, tr("Ceil (Integer scaling)", "This option means always round up") },
|
||||
{ Qt::HighDpiScaleFactorRoundingPolicy::Floor, tr("Floor (Integer scaling)", "This option means always round down") },
|
||||
{ Qt::HighDpiScaleFactorRoundingPolicy::PassThrough, tr("Follow system (Fractional scaling)", "This option means don't round") }
|
||||
};
|
||||
|
||||
QStringList dcbDropDown;
|
||||
for (const QPair<Settings::DoubleClickBehavior, QString> & dcOption : _dc_options) {
|
||||
dcbDropDown.append(dcOption.second);
|
||||
}
|
||||
|
||||
QStringList mwbDropDown;
|
||||
for (const QPair<Settings::MouseWheelBehavior, QString> & mwOption : _mw_options) {
|
||||
mwbDropDown.append(mwOption.second);
|
||||
}
|
||||
|
||||
QStringList iwsbDropDown;
|
||||
for (const QPair<Settings::WindowSizeBehavior, QString> & iwsOption : _iws_options) {
|
||||
iwsbDropDown.append(iwsOption.second);
|
||||
}
|
||||
|
||||
QStringList hidpiDropDown;
|
||||
for (const QPair<Qt::HighDpiScaleFactorRoundingPolicy, QString> & hidpiOption : _hidpi_options) {
|
||||
hidpiDropDown.append(hidpiOption.second);
|
||||
}
|
||||
|
||||
settingsForm->addRow(tr("Stay on top when start-up"), m_stayOnTop);
|
||||
settingsForm->addRow(tr("Use built-in close window animation"), m_useBuiltInCloseAnimation);
|
||||
settingsForm->addRow(tr("Use light-color checkerboard"), m_useLightCheckerboard);
|
||||
settingsForm->addRow(tr("Loop the loaded gallery"), m_loopGallery);
|
||||
settingsForm->addRow(tr("Auto long image mode"), m_autoLongImageMode);
|
||||
settingsForm->addRow(tr("Limit SVG support to SVG Tiny 1.2"), m_svgTiny12Only);
|
||||
settingsForm->addRow(tr("Double-click behavior"), m_doubleClickBehavior);
|
||||
settingsForm->addRow(tr("Mouse wheel behavior"), m_mouseWheelBehavior);
|
||||
settingsForm->addRow(tr("Default window size"), m_initWindowSizeBehavior);
|
||||
settingsForm->addRow(tr("HiDPI scale factor rounding policy"), m_hiDpiRoundingPolicyBehavior);
|
||||
|
||||
m_stayOnTop->setChecked(Settings::instance()->stayOnTop());
|
||||
m_useBuiltInCloseAnimation->setChecked(Settings::instance()->useBuiltInCloseAnimation());
|
||||
m_useLightCheckerboard->setChecked(Settings::instance()->useLightCheckerboard());
|
||||
m_loopGallery->setChecked(Settings::instance()->loopGallery());
|
||||
m_autoLongImageMode->setChecked(Settings::instance()->autoLongImageMode());
|
||||
m_svgTiny12Only->setChecked(Settings::instance()->svgTiny12Only());
|
||||
m_doubleClickBehavior->setModel(new QStringListModel(dcbDropDown));
|
||||
Settings::DoubleClickBehavior dcb = Settings::instance()->doubleClickBehavior();
|
||||
m_doubleClickBehavior->setCurrentIndex(static_cast<int>(dcb));
|
||||
m_mouseWheelBehavior->setModel(new QStringListModel(mwbDropDown));
|
||||
Settings::MouseWheelBehavior mwb = Settings::instance()->mouseWheelBehavior();
|
||||
m_mouseWheelBehavior->setCurrentIndex(static_cast<int>(mwb));
|
||||
m_initWindowSizeBehavior->setModel(new QStringListModel(iwsbDropDown));
|
||||
Settings::WindowSizeBehavior iwsb = Settings::instance()->initWindowSizeBehavior();
|
||||
m_initWindowSizeBehavior->setCurrentIndex(static_cast<int>(iwsb));
|
||||
m_hiDpiRoundingPolicyBehavior->setModel(new QStringListModel(hidpiDropDown));
|
||||
Qt::HighDpiScaleFactorRoundingPolicy hidpi = Settings::instance()->hiDpiScaleFactorBehavior();
|
||||
for (int i = 0; i < _hidpi_options.count(); i++) {
|
||||
if (_hidpi_options.at(i).first == hidpi) {
|
||||
m_hiDpiRoundingPolicyBehavior->setCurrentIndex(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
auto* settings = Settings::instance();
|
||||
ui->m_useLightCheckerboard->setChecked(settings->useLightCheckerboard());
|
||||
ui->m_loopGallery->setChecked(settings->loopGallery());
|
||||
ui->m_svgTiny12Only->setChecked(settings->svgTiny12Only());
|
||||
ui->m_mouseWheelBehavior->setModel(MW_OPTIONS.buildComboBoxModel(this));
|
||||
Settings::MouseWheelBehavior mwb = settings->mouseWheelBehavior();
|
||||
ui->m_mouseWheelBehavior->setCurrentIndex(MW_OPTIONS.to_index(mwb));
|
||||
ui->m_initWindowSizeBehavior->setModel(IWS_OPTIONS.buildComboBoxModel(this));
|
||||
Settings::WindowSizeBehavior iwsb = settings->initWindowSizeBehavior();
|
||||
ui->m_initWindowSizeBehavior->setCurrentIndex(IWS_OPTIONS.to_index(iwsb));
|
||||
ui->m_hiDpiRoundingPolicyBehavior->setModel(HIDPI_OPTIONS.buildComboBoxModel(this));
|
||||
Qt::HighDpiScaleFactorRoundingPolicy hidpi = settings->hiDpiScaleFactorBehavior();
|
||||
ui->m_hiDpiRoundingPolicyBehavior->setCurrentIndex(HIDPI_OPTIONS.to_index(hidpi));
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
|
||||
# define QCHECKBOX_CHECKSTATECHANGED QCheckBox::checkStateChanged
|
||||
@@ -164,51 +100,41 @@ SettingsDialog::SettingsDialog(QWidget *parent)
|
||||
# define QT_CHECKSTATE int
|
||||
#endif // QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
|
||||
|
||||
connect(m_stayOnTop, &QCHECKBOX_CHECKSTATECHANGED, this, [ = ](QT_CHECKSTATE state){
|
||||
Settings::instance()->setStayOnTop(state == Qt::Checked);
|
||||
});
|
||||
|
||||
connect(m_useBuiltInCloseAnimation, &QCHECKBOX_CHECKSTATECHANGED, this, [ = ](QT_CHECKSTATE state){
|
||||
Settings::instance()->setUseBuiltInCloseAnimation(state == Qt::Checked);
|
||||
});
|
||||
|
||||
connect(m_useLightCheckerboard, &QCHECKBOX_CHECKSTATECHANGED, this, [ = ](QT_CHECKSTATE state){
|
||||
connect(ui->m_useLightCheckerboard, &QCHECKBOX_CHECKSTATECHANGED, this, [ = ](QT_CHECKSTATE state){
|
||||
Settings::instance()->setUseLightCheckerboard(state == Qt::Checked);
|
||||
});
|
||||
|
||||
connect(m_loopGallery, &QCHECKBOX_CHECKSTATECHANGED, this, [ = ](QT_CHECKSTATE state){
|
||||
connect(ui->m_loopGallery, &QCHECKBOX_CHECKSTATECHANGED, this, [ = ](QT_CHECKSTATE state){
|
||||
Settings::instance()->setLoopGallery(state == Qt::Checked);
|
||||
});
|
||||
|
||||
connect(m_autoLongImageMode, &QCHECKBOX_CHECKSTATECHANGED, this, [ = ](QT_CHECKSTATE state){
|
||||
Settings::instance()->setAutoLongImageMode(state == Qt::Checked);
|
||||
});
|
||||
|
||||
connect(m_svgTiny12Only, &QCHECKBOX_CHECKSTATECHANGED, this, [ = ](QT_CHECKSTATE state){
|
||||
connect(ui->m_svgTiny12Only, &QCHECKBOX_CHECKSTATECHANGED, this, [ = ](QT_CHECKSTATE state){
|
||||
Settings::instance()->setSvgTiny12Only(state == Qt::Checked);
|
||||
});
|
||||
|
||||
connect(m_doubleClickBehavior, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [ = ](int index){
|
||||
Settings::instance()->setDoubleClickBehavior(_dc_options.at(index).first);
|
||||
connect(ui->m_mouseWheelBehavior, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [ = ](int index){
|
||||
Settings::instance()->setMouseWheelBehavior(MW_OPTIONS.to_value(index));
|
||||
});
|
||||
|
||||
connect(m_mouseWheelBehavior, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [ = ](int index){
|
||||
Settings::instance()->setMouseWheelBehavior(_mw_options.at(index).first);
|
||||
connect(ui->m_initWindowSizeBehavior, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [ = ](int index){
|
||||
Settings::instance()->setInitWindowSizeBehavior(IWS_OPTIONS.to_value(index));
|
||||
});
|
||||
|
||||
connect(m_initWindowSizeBehavior, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [ = ](int index){
|
||||
Settings::instance()->setInitWindowSizeBehavior(_iws_options.at(index).first);
|
||||
connect(ui->m_hiDpiRoundingPolicyBehavior, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [ = ](int index){
|
||||
Settings::instance()->setHiDpiScaleFactorBehavior(HIDPI_OPTIONS.to_value(index));
|
||||
});
|
||||
|
||||
connect(m_hiDpiRoundingPolicyBehavior, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [ = ](int index){
|
||||
Settings::instance()->setHiDpiScaleFactorBehavior(_hidpi_options.at(index).first);
|
||||
// YYC MARK:
|
||||
// Use adjustSize() to make sure all ComboBox are expanded by its items.
|
||||
// Because items are added after inserting them into layout.
|
||||
// It is inviable to call this function in ctor because this relys on Qt event system.
|
||||
// It only can be done by QTimer::singleShot().
|
||||
QTimer::singleShot(0, this, [this](){
|
||||
this->adjustSize();
|
||||
});
|
||||
|
||||
adjustSize();
|
||||
setWindowFlag(Qt::WindowContextHelpButtonHint, false);
|
||||
}
|
||||
|
||||
SettingsDialog::~SettingsDialog()
|
||||
{
|
||||
|
||||
delete ui;
|
||||
}
|
||||
|
||||
+7
-17
@@ -5,11 +5,14 @@
|
||||
#ifndef SETTINGSDIALOG_H
|
||||
#define SETTINGSDIALOG_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QDialog>
|
||||
|
||||
class QCheckBox;
|
||||
class QComboBox;
|
||||
QT_BEGIN_NAMESPACE
|
||||
namespace Ui {
|
||||
class SettingsDialog;
|
||||
}
|
||||
QT_END_NAMESPACE
|
||||
|
||||
class SettingsDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
@@ -17,21 +20,8 @@ public:
|
||||
explicit SettingsDialog(QWidget *parent = nullptr);
|
||||
~SettingsDialog();
|
||||
|
||||
signals:
|
||||
|
||||
public slots:
|
||||
|
||||
private:
|
||||
QCheckBox * m_stayOnTop = nullptr;
|
||||
QCheckBox * m_useBuiltInCloseAnimation = nullptr;
|
||||
QCheckBox * m_useLightCheckerboard = nullptr;
|
||||
QCheckBox * m_loopGallery = nullptr;
|
||||
QCheckBox * m_autoLongImageMode = nullptr;
|
||||
QCheckBox * m_svgTiny12Only = nullptr;
|
||||
QComboBox * m_doubleClickBehavior = nullptr;
|
||||
QComboBox * m_mouseWheelBehavior = nullptr;
|
||||
QComboBox * m_initWindowSizeBehavior = nullptr;
|
||||
QComboBox * m_hiDpiRoundingPolicyBehavior = nullptr;
|
||||
QT_PREPEND_NAMESPACE(Ui::SettingsDialog) *ui;
|
||||
};
|
||||
|
||||
#endif // SETTINGSDIALOG_H
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>SettingsDialog</class>
|
||||
<widget class="QDialog" name="SettingsDialog">
|
||||
<property name="windowModality">
|
||||
<enum>Qt::NonModal</enum>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Settings</string>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="mainLayout">
|
||||
<item>
|
||||
<widget class="QTabWidget" name="settingsTabs">
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="optionsTab">
|
||||
<attribute name="title">
|
||||
<string>Options</string>
|
||||
</attribute>
|
||||
<layout class="QFormLayout" name="settingsForm">
|
||||
<item row="0" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="m_useLightCheckerboard">
|
||||
<property name="text">
|
||||
<string>Use light-color checkerboard</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="m_loopGallery">
|
||||
<property name="text">
|
||||
<string>Loop the loaded gallery</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="m_svgTiny12Only">
|
||||
<property name="text">
|
||||
<string>Limit SVG support to SVG Tiny 1.2</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="m_mouseWheelBehaviorLabel">
|
||||
<property name="text">
|
||||
<string>Mouse wheel behavior</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QComboBox" name="m_mouseWheelBehavior"/>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="m_initWindowSizeBehaviorLabel">
|
||||
<property name="text">
|
||||
<string>Default window size</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<widget class="QComboBox" name="m_initWindowSizeBehavior"/>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<widget class="QLabel" name="m_hiDpiRoundingPolicyBehaviorLabel">
|
||||
<property name="text">
|
||||
<string>HiDPI scale factor rounding policy</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="1">
|
||||
<widget class="QComboBox" name="m_hiDpiRoundingPolicyBehavior"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -1,126 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2024 Gary Wang <opensource@blumia.net>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "shortcutedit.h"
|
||||
|
||||
#include <QLabel>
|
||||
#include <QToolButton>
|
||||
#include <QHBoxLayout>
|
||||
#include <QFormLayout>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QKeySequenceEdit>
|
||||
|
||||
ShortcutEditor::ShortcutEditor(ShortcutEdit * shortcutEdit, QWidget * parent)
|
||||
: QWidget(parent)
|
||||
, m_descriptionLabel(new QLabel)
|
||||
, m_shortcutEdit(shortcutEdit)
|
||||
, m_shortcutLayout(new QFormLayout)
|
||||
{
|
||||
Q_CHECK_PTR(m_shortcutEdit);
|
||||
|
||||
QDialogButtonBox * buttons = new QDialogButtonBox(QDialogButtonBox::Apply | QDialogButtonBox::Discard);
|
||||
|
||||
QVBoxLayout * layout = new QVBoxLayout(this);
|
||||
layout->addWidget(m_descriptionLabel);
|
||||
layout->addLayout(m_shortcutLayout);
|
||||
layout->addWidget(buttons);
|
||||
|
||||
connect(buttons, &QDialogButtonBox::clicked, this, [=](QAbstractButton *button){
|
||||
if ((QPushButton *)button == buttons->button(QDialogButtonBox::Apply)) {
|
||||
applyShortcuts();
|
||||
} else {
|
||||
reloadShortcuts();
|
||||
}
|
||||
});
|
||||
connect(shortcutEdit, &ShortcutEdit::shortcutsChanged, this, &ShortcutEditor::reloadShortcuts);
|
||||
|
||||
setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum);
|
||||
|
||||
reloadShortcuts();
|
||||
}
|
||||
|
||||
void ShortcutEditor::setDescription(const QString &desc)
|
||||
{
|
||||
m_descriptionLabel->setText(desc);
|
||||
}
|
||||
|
||||
void ShortcutEditor::reloadShortcuts()
|
||||
{
|
||||
if (!m_keySequenceEdits.isEmpty()) {
|
||||
for (QKeySequenceEdit * keyseqEdit : m_keySequenceEdits) {
|
||||
m_shortcutLayout->removeRow(keyseqEdit);
|
||||
}
|
||||
m_keySequenceEdits.clear();
|
||||
}
|
||||
|
||||
QList<QKeySequence> shortcuts = m_shortcutEdit->shortcuts();
|
||||
shortcuts.append(QKeySequence());
|
||||
for (const QKeySequence & shortcut : shortcuts) {
|
||||
QKeySequenceEdit * keyseqEdit = new QKeySequenceEdit(this);
|
||||
keyseqEdit->setClearButtonEnabled(true);
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
|
||||
keyseqEdit->setMaximumSequenceLength(1);
|
||||
#endif // QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
|
||||
keyseqEdit->setKeySequence(shortcut);
|
||||
m_keySequenceEdits.append(keyseqEdit);
|
||||
}
|
||||
|
||||
for (int i = 0; i < m_keySequenceEdits.count(); i++) {
|
||||
m_shortcutLayout->addRow(tr("Shortcut #%1").arg(i + 1), m_keySequenceEdits.at(i));
|
||||
}
|
||||
}
|
||||
|
||||
void ShortcutEditor::applyShortcuts()
|
||||
{
|
||||
QList<QKeySequence> shortcuts;
|
||||
for (const QKeySequenceEdit * keyseqEdit : m_keySequenceEdits) {
|
||||
if (!keyseqEdit->keySequence().isEmpty() && !shortcuts.contains(keyseqEdit->keySequence())) {
|
||||
shortcuts.append(keyseqEdit->keySequence());
|
||||
}
|
||||
}
|
||||
emit m_shortcutEdit->applyShortcutsRequested(shortcuts);
|
||||
}
|
||||
|
||||
// ----------------------------------------
|
||||
|
||||
ShortcutEdit::ShortcutEdit(QWidget *parent)
|
||||
: QWidget(parent)
|
||||
, m_shortcutsLabel(new QLabel(this))
|
||||
, m_setShortcutButton(new QToolButton(this))
|
||||
{
|
||||
m_setShortcutButton->setText("...");
|
||||
|
||||
QHBoxLayout * layout = new QHBoxLayout(this);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->addWidget(m_shortcutsLabel, 1);
|
||||
layout->addWidget(m_setShortcutButton);
|
||||
|
||||
connect(this, &ShortcutEdit::shortcutsChanged, this, [=](){
|
||||
QStringList shortcutTexts;
|
||||
for (const QKeySequence & shortcut : std::as_const(m_shortcuts)) {
|
||||
shortcutTexts.append(shortcut.toString(QKeySequence::NativeText));
|
||||
}
|
||||
m_shortcutsLabel->setText(shortcutTexts.isEmpty() ? tr("No shortcuts") : shortcutTexts.join(", "));
|
||||
m_shortcutsLabel->setDisabled(shortcutTexts.isEmpty());
|
||||
});
|
||||
|
||||
connect(m_setShortcutButton, &QToolButton::clicked, this, &ShortcutEdit::editButtonClicked);
|
||||
|
||||
adjustSize();
|
||||
}
|
||||
|
||||
ShortcutEdit::~ShortcutEdit()
|
||||
{
|
||||
}
|
||||
|
||||
QList<QKeySequence> ShortcutEdit::shortcuts() const
|
||||
{
|
||||
return m_shortcuts;
|
||||
}
|
||||
|
||||
void ShortcutEdit::setShortcuts(const QList<QKeySequence> &shortcuts)
|
||||
{
|
||||
m_shortcuts = shortcuts;
|
||||
emit shortcutsChanged();
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2024 Gary Wang <opensource@blumia.net>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
#include <QList>
|
||||
#include <QKeySequence>
|
||||
|
||||
class QLabel;
|
||||
class QFormLayout;
|
||||
class QToolButton;
|
||||
class QKeySequenceEdit;
|
||||
class ShortcutEdit;
|
||||
class ShortcutEditor : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ShortcutEditor(ShortcutEdit * shortcutEdit, QWidget * parent = nullptr);
|
||||
~ShortcutEditor() = default;
|
||||
|
||||
void setDescription(const QString & desc);
|
||||
|
||||
void reloadShortcuts();
|
||||
void applyShortcuts();
|
||||
|
||||
private:
|
||||
QLabel * m_descriptionLabel;
|
||||
ShortcutEdit * m_shortcutEdit;
|
||||
QFormLayout * m_shortcutLayout;
|
||||
QList<QKeySequenceEdit *> m_keySequenceEdits;
|
||||
};
|
||||
|
||||
class ShortcutEdit : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QList<QKeySequence> shortcuts MEMBER m_shortcuts WRITE setShortcuts NOTIFY shortcutsChanged)
|
||||
public:
|
||||
explicit ShortcutEdit(QWidget * parent = nullptr);
|
||||
~ShortcutEdit();
|
||||
|
||||
QList<QKeySequence> shortcuts() const;
|
||||
void setShortcuts(const QList<QKeySequence> &shortcuts);
|
||||
|
||||
signals:
|
||||
void shortcutsChanged();
|
||||
void editButtonClicked();
|
||||
void applyShortcutsRequested(QList<QKeySequence> newShortcuts);
|
||||
|
||||
private:
|
||||
QList<QKeySequence> m_shortcuts;
|
||||
QLabel * m_shortcutsLabel;
|
||||
QToolButton * m_setShortcutButton;
|
||||
};
|
||||
@@ -1,38 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2022 Gary Wang <wzc782970009@gmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "toolbutton.h"
|
||||
|
||||
#include "actionmanager.h"
|
||||
#include "opacityhelper.h"
|
||||
|
||||
#include <QPainter>
|
||||
#include <QGraphicsOpacityEffect>
|
||||
#include <QPropertyAnimation>
|
||||
|
||||
ToolButton::ToolButton(bool hoverColor, QWidget *parent)
|
||||
: QPushButton(parent)
|
||||
, m_opacityHelper(new OpacityHelper(this))
|
||||
{
|
||||
setFlat(true);
|
||||
QString qss = "QPushButton {"
|
||||
"background: transparent;"
|
||||
"}";
|
||||
if (hoverColor) {
|
||||
qss += "QPushButton:hover {"
|
||||
"background: red;"
|
||||
"}";
|
||||
}
|
||||
setStyleSheet(qss);
|
||||
}
|
||||
|
||||
void ToolButton::setIconResourcePath(const QString &iconp)
|
||||
{
|
||||
this->setIcon(ActionManager::loadHidpiIcon(iconp, this->iconSize()));
|
||||
}
|
||||
|
||||
void ToolButton::setOpacity(qreal opacity, bool animated)
|
||||
{
|
||||
m_opacityHelper->setOpacity(opacity, animated);
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2022 Gary Wang <wzc782970009@gmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef TOOLBUTTON_H
|
||||
#define TOOLBUTTON_H
|
||||
|
||||
#include <QPushButton>
|
||||
|
||||
class OpacityHelper;
|
||||
class ToolButton : public QPushButton
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
ToolButton(bool hoverColor = false, QWidget * parent = nullptr);
|
||||
void setIconResourcePath(const QString &iconp);
|
||||
|
||||
public slots:
|
||||
void setOpacity(qreal opacity, bool animated = true);
|
||||
|
||||
private:
|
||||
OpacityHelper * m_opacityHelper;
|
||||
};
|
||||
|
||||
#endif // TOOLBUTTON_H
|
||||
@@ -236,27 +236,27 @@
|
||||
<location filename="../mainwindow.cpp" line="734"/>
|
||||
<location filename="../mainwindow.cpp" line="764"/>
|
||||
<source>Save As</source>
|
||||
<translation type="unfinished"></translation>
|
||||
<translation>다른 이름으로 저장</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.cpp" line="710"/>
|
||||
<source>No image is currently open.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
<translation>현재 열려 있는 이미지가 없습니다.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.cpp" line="722"/>
|
||||
<source>%1 Image (*.%2)</source>
|
||||
<translation type="unfinished"></translation>
|
||||
<translation>%1 이미지 (*.%2)</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.cpp" line="728"/>
|
||||
<source>No supported image formats are available.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
<translation>지원되는 이미지 형식을 사용할 수 없습니다.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.cpp" line="765"/>
|
||||
<source>Failed to save image: %1</source>
|
||||
<translation type="unfinished"></translation>
|
||||
<translation>이미지 저장 실패: %1</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.cpp" line="854"/>
|
||||
@@ -320,7 +320,7 @@
|
||||
<message>
|
||||
<location filename="../actionmanager.cpp" line="88"/>
|
||||
<source>Save &As...</source>
|
||||
<translation type="unfinished"></translation>
|
||||
<translation>다른 이름으로 저장(&A)...</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../actionmanager.cpp" line="93"/>
|
||||
|
||||
-158
@@ -1,158 +0,0 @@
|
||||
image:
|
||||
- Visual Studio 2022
|
||||
environment:
|
||||
CMAKE_INSTALL_PREFIX: C:\projects\cmake
|
||||
LIBZ: C:\projects\zlib
|
||||
LIBEXPAT: C:\projects\libexpat
|
||||
LIBAVIF: C:\projects\libavif
|
||||
LIBEXIV2: C:\projects\exiv2
|
||||
PPKG: C:\projects\ppkg
|
||||
matrix:
|
||||
- job_name: mingw_64_qt6_8
|
||||
QTDIR: C:\Qt\6.8\mingw_64
|
||||
MINGW64: C:\Qt\mingw_64\Tools\mingw1310_64
|
||||
KF_BRANCH: v6.24.0
|
||||
EXIV2_VERSION: "0.28.7"
|
||||
EXIV2_CMAKE_OPTIONS: "-DEXIV2_ENABLE_BROTLI=OFF -DEXIV2_ENABLE_INIH=OFF -DEXIV2_BUILD_EXIV2_COMMAND=OFF"
|
||||
PPIC_CMAKE_OPTIONS: "-DPREFER_QT_5=OFF"
|
||||
WINDEPLOYQT_ARGS: "--verbose=2 --no-quick-import --no-translations --no-opengl-sw --no-system-d3d-compiler --skip-plugin-types tls,networkinformation"
|
||||
|
||||
install:
|
||||
- mkdir %CMAKE_INSTALL_PREFIX%
|
||||
- mkdir %LIBZ%
|
||||
- mkdir %LIBEXPAT%
|
||||
- mkdir %LIBAVIF%
|
||||
- mkdir %LIBEXIV2%
|
||||
- mkdir %PPKG%
|
||||
- cd %APPVEYOR_BUILD_FOLDER%
|
||||
- git submodule update --init --recursive
|
||||
- set PATH=%PATH%;%CMAKE_INSTALL_PREFIX%;%QTDIR%\bin;%MINGW64%\bin;%PPKG%
|
||||
- set CC=%MINGW64%\bin\gcc.exe
|
||||
- set CXX=%MINGW64%\bin\g++.exe
|
||||
|
||||
build_script:
|
||||
# prepare
|
||||
- mkdir 3rdparty
|
||||
- choco install ninja
|
||||
- cd %PPKG%
|
||||
- curl -fsSL -o ppkg.exe https://github.com/BLumia/pineapple-package-manager/releases/latest/download/ppkg.exe
|
||||
- cd %APPVEYOR_BUILD_FOLDER%
|
||||
# download and install zlib for KArchive
|
||||
- cd %LIBZ%
|
||||
- curl -fsSL -o zlib132.zip https://zlib.net/zlib132.zip
|
||||
- 7z x zlib132.zip -y
|
||||
- cd zlib-1.3.2
|
||||
- mkdir build
|
||||
- cd build
|
||||
- cmake .. -G "Ninja" -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=%CMAKE_INSTALL_PREFIX%
|
||||
- cmake --build . --config Release
|
||||
- cmake --build . --config Release --target install/strip
|
||||
- cd %APPVEYOR_BUILD_FOLDER%
|
||||
# install ECM so we can build KImageFormats
|
||||
- cd 3rdparty
|
||||
- git clone -b %KF_BRANCH% -q https://invent.kde.org/frameworks/extra-cmake-modules.git
|
||||
- git rev-parse HEAD
|
||||
- cd extra-cmake-modules
|
||||
- cmake -G "Ninja" . -DCMAKE_INSTALL_PREFIX=%CMAKE_INSTALL_PREFIX% -DBUILD_TESTING=OFF
|
||||
- cmake --build .
|
||||
- cmake --build . --target install
|
||||
- cd %APPVEYOR_BUILD_FOLDER%
|
||||
# install AOM for libavif AV1 decoding support...
|
||||
- cd 3rdparty
|
||||
#- git clone -b v3.9.1 --depth 1 https://aomedia.googlesource.com/aom
|
||||
#- cd aom
|
||||
#- mkdir build.aom
|
||||
#- cd build.aom
|
||||
#- 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
|
||||
#- cmake --build . --config Release
|
||||
#- cmake --build . --config Release --target install/strip
|
||||
- mkdir aom
|
||||
- cd aom
|
||||
- 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
|
||||
- ppkg ppkg-aom.zip
|
||||
- 7z x ppkg-aom.zip LICENSE -y
|
||||
- cd %APPVEYOR_BUILD_FOLDER%
|
||||
# install libavif for avif format support of KImageFormats
|
||||
- cd %LIBAVIF%
|
||||
- curl -fsSL -o libavif-v1_3_0.zip https://github.com/AOMediaCodec/libavif/archive/v1.3.0.zip
|
||||
- 7z x libavif-v1_3_0.zip -y
|
||||
- cd libavif-1.3.0
|
||||
- mkdir build
|
||||
- cd build
|
||||
- cmake .. -G "Ninja" -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=%CMAKE_INSTALL_PREFIX% -DAVIF_CODEC_AOM=SYSTEM -DAVIF_CODEC_AOM_DECODE=ON -DAVIF_CODEC_AOM_ENCODE=ON -DAVIF_LIBYUV=LOCAL
|
||||
- cmake --build . --config Release
|
||||
- cmake --build . --config Release --target install/strip
|
||||
- cd %APPVEYOR_BUILD_FOLDER%
|
||||
# install KArchive for kra format support of KImageFormats
|
||||
- cd 3rdparty
|
||||
- git clone -b %KF_BRANCH% -q https://invent.kde.org/frameworks/karchive.git
|
||||
- git rev-parse HEAD
|
||||
- cd karchive
|
||||
- mkdir build
|
||||
- cd build
|
||||
- cmake .. -G "Ninja" -DBUILD_TESTING=OFF -DWITH_LIBZSTD=OFF -DWITH_BZIP2=OFF -DWITH_LIBLZMA=OFF -DWITH_OPENSSL=OFF -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=%CMAKE_INSTALL_PREFIX%
|
||||
- cmake --build . --config Release
|
||||
- cmake --build . --config Release --target install/strip
|
||||
- cd %APPVEYOR_BUILD_FOLDER%
|
||||
# build libexpat for libexiv2
|
||||
- cd %LIBEXPAT%
|
||||
- curl -fsSL -o R_2_7_3.zip https://github.com/libexpat/libexpat/archive/R_2_7_3.zip
|
||||
- 7z x R_2_7_3.zip -y
|
||||
- cd libexpat-R_2_7_3/expat/
|
||||
- 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
|
||||
- cmake --build . --target install/strip
|
||||
- cd %APPVEYOR_BUILD_FOLDER%
|
||||
# build libexiv2
|
||||
- cd %LIBEXIV2%
|
||||
- curl -fsSL -o v%EXIV2_VERSION%.zip https://github.com/Exiv2/exiv2/archive/v%EXIV2_VERSION%.zip
|
||||
- 7z x v%EXIV2_VERSION%.zip -y
|
||||
- cd exiv2-%EXIV2_VERSION%
|
||||
- cmake -G "Ninja" . -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=%CMAKE_INSTALL_PREFIX% %EXIV2_CMAKE_OPTIONS%
|
||||
- cmake --build . --target install/strip
|
||||
- cd %APPVEYOR_BUILD_FOLDER%
|
||||
# install KImageFormats
|
||||
- cd 3rdparty
|
||||
- git clone -b %KF_BRANCH% -q https://invent.kde.org/frameworks/kimageformats.git
|
||||
- git rev-parse HEAD
|
||||
- cd kimageformats
|
||||
- mkdir build
|
||||
- cd build
|
||||
- cmake .. -G "Ninja" -DBUILD_TESTING=OFF -DCMAKE_BUILD_TYPE=Release -DKDE_INSTALL_QTPLUGINDIR=%QTDIR%\plugins
|
||||
- cmake --build . --config Release
|
||||
- cmake --build . --config Release --target install/strip
|
||||
- cd %APPVEYOR_BUILD_FOLDER%
|
||||
# finally...
|
||||
- mkdir build
|
||||
- cd build
|
||||
- cmake .. -G "Ninja" %PPIC_CMAKE_OPTIONS% -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH=%CMAKE_INSTALL_PREFIX% -DCMAKE_INSTALL_PREFIX='%cd%'
|
||||
- cmake --build . --config Release
|
||||
- cmake --build . --config Release --target install/strip
|
||||
# fixme: I don't know how to NOT make the binary installed to the ./bin/ folder...
|
||||
- cd bin
|
||||
- copy %APPVEYOR_BUILD_FOLDER%\LICENSE
|
||||
- copy %CMAKE_INSTALL_PREFIX%\bin\libaom.dll
|
||||
- copy %CMAKE_INSTALL_PREFIX%\bin\libexpat-1.dll
|
||||
- copy %CMAKE_INSTALL_PREFIX%\bin\libexiv2.dll
|
||||
- copy %CMAKE_INSTALL_PREFIX%\bin\libavif.dll
|
||||
- copy %CMAKE_INSTALL_PREFIX%\bin\libz.dll
|
||||
- copy %CMAKE_INSTALL_PREFIX%\bin\libKF?Archive.dll
|
||||
- windeployqt %WINDEPLOYQT_ARGS% .\ppic.exe
|
||||
# copy 3rdparty licenses for the libs we vendored for windows...
|
||||
- mkdir licenses
|
||||
- cd licenses
|
||||
- copy %APPVEYOR_BUILD_FOLDER%\3rdparty\aom\LICENSE License.aom.txt
|
||||
- copy %APPVEYOR_BUILD_FOLDER%\3rdparty\karchive\LICENSES\LGPL-2.0-or-later.txt License.KArchive.txt
|
||||
- copy %APPVEYOR_BUILD_FOLDER%\3rdparty\kimageformats\LICENSES\LGPL-2.1-or-later.txt License.kimageformats.txt
|
||||
- copy %LIBEXPAT%\libexpat-R_2_7_3\expat\COPYING License.expat.txt
|
||||
- copy %LIBAVIF%\libavif-1.3.0\LICENSE License.libavif.txt
|
||||
- copy %LIBEXIV2%\exiv2-%EXIV2_VERSION%\COPYING License.exiv2.txt
|
||||
# TODO: Qt, zlib
|
||||
- cd ..
|
||||
# for debug..
|
||||
- tree /f
|
||||
- cd %APPVEYOR_BUILD_FOLDER%
|
||||
- xcopy %CMAKE_INSTALL_PREFIX% .\cmake-prefix-copy /E /H /C /I
|
||||
|
||||
artifacts:
|
||||
- path: build\bin
|
||||
- path: cmake-prefix-copy
|
||||
@@ -0,0 +1,29 @@
|
||||
<center>
|
||||
<p>
|
||||
<img width='128' height='128' src=':/icons/app-icon.svg'/>
|
||||
<br/>
|
||||
<!-- Application name -->
|
||||
<span>%1</span>
|
||||
<br/>
|
||||
<!-- Version info -->
|
||||
<span>%2</span>
|
||||
</p>
|
||||
<hr/>
|
||||
<p>
|
||||
<!-- Upstream author and fork author -->
|
||||
<span>%3</span><span>: </span><a href='https://github.com/BLumia'>@BLumia</a>
|
||||
<br/>
|
||||
<span>%4</span><span>: </span><a href='https://github.com/yyc12345'>@yyc12345</a>
|
||||
<br/>
|
||||
<!-- Icon author -->
|
||||
<span>%5</span><span>: </span><a href='https://github.com/Lovelyblack'>@Lovelyblack</a>
|
||||
</p>
|
||||
<hr/>
|
||||
<p>
|
||||
<!-- Built Qt version info -->
|
||||
<span>%6</span>
|
||||
<br/>
|
||||
<!-- Source code -->
|
||||
<span>%7</span><span>: </span><a href='https://github.com/BLumia/pineapple-pictures'>GitHub</a>
|
||||
</p>
|
||||
</center>
|
||||
@@ -0,0 +1,29 @@
|
||||
<!-- General help -->
|
||||
<h1 align='center'>%1</h1>
|
||||
<ul>
|
||||
<li>
|
||||
<span>%2</span>
|
||||
<ul>
|
||||
<li>%3</li>
|
||||
<li>%4</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>%5</li>
|
||||
<li>%6</li>
|
||||
</ul>
|
||||
<hr/>
|
||||
<!-- Context menu option help -->
|
||||
<h1 align='center'>%7</h1>
|
||||
<!-- Context menu option list -->
|
||||
<ul>
|
||||
<li>
|
||||
<b>%8</b><span>:</span>
|
||||
<br/>
|
||||
<span>%9</span>
|
||||
</li>
|
||||
<li>
|
||||
<b>%10</b><span>:</span>
|
||||
<br/>
|
||||
<span>%11</span>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -0,0 +1,29 @@
|
||||
<!-- License -->
|
||||
<h1 align='center'><b>%1</b></h1>
|
||||
<p>%2</p>
|
||||
<hr/>
|
||||
<!-- VErbatime license -->
|
||||
<h1 align='center'><b>%3</b></h1>
|
||||
<pre>
|
||||
Expat/MIT License
|
||||
|
||||
Copyright © 2026 BLumia
|
||||
|
||||
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:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
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.
|
||||
</pre>
|
||||
@@ -1,3 +1,14 @@
|
||||
<!-- Contributor title -->
|
||||
<h1 align='center'>%1</h1>
|
||||
<!-- Contributor list -->
|
||||
<a href='https://github.com/BLumia/pineapple-pictures/graphs/contributors'>%2</a>
|
||||
<!-- The message to thank these contributors -->
|
||||
<p>%3</p>
|
||||
|
||||
<!-- Translator title -->
|
||||
<h1 align='center'>%4</h1>
|
||||
<!-- The message to thank these translators -->
|
||||
<p>%5</p>
|
||||
<ul>
|
||||
<li><u>Catalan</u>: Toni Estévez</li>
|
||||
<li><u>Chinese (Simplified)</u>: Percy Hong</li>
|
||||
@@ -0,0 +1,15 @@
|
||||
<!-- Title -->
|
||||
<h1 align='center'><b>%1</b></h1>
|
||||
<!-- List intro message -->
|
||||
<p>%2</p>
|
||||
<p>%3</p>
|
||||
<ul>
|
||||
<li>
|
||||
<a href='https://www.qt.io/'>Qt</a><span>: </span>
|
||||
<span>GPLv2 + GPLv3 + LGPLv2.1 + LGPLv3</span>
|
||||
</li>
|
||||
<li style="display: none;">
|
||||
<a href='https://www.exiv2.org/'>Exiv2</a><span>: </span>
|
||||
<span>GPLv2</span>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -10,6 +10,10 @@
|
||||
<file>icons/window-close.svg</file>
|
||||
<file>icons/go-next.svg</file>
|
||||
<file>icons/go-previous.svg</file>
|
||||
<file>plain/translators.html</file>
|
||||
<file>plain/help.html</file>
|
||||
<file>plain/about.html</file>
|
||||
<file>plain/special-thanks.html</file>
|
||||
<file>plain/license.html</file>
|
||||
<file>plain/third-party-libs.html</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
<url type="bugtracker">https://github.com/BLumia/pineapple-pictures/issues</url>
|
||||
<url type="donation">https://ko-fi.com/blumia</url>
|
||||
<url type="translate">https://hosted.weblate.org/projects/pineapple-pictures/</url>
|
||||
<url type="vcs-browser">https://github.com/BLumia/pineapple-pictures</url>
|
||||
<provides>
|
||||
<binary>ppic</binary>
|
||||
</provides>
|
||||
@@ -101,6 +102,26 @@
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
<releases>
|
||||
<release type="stable" version="1.5.0" date="2026-07-04T00:00:00Z">
|
||||
<description>
|
||||
<p>This release adds the following features:</p>
|
||||
<ul>
|
||||
<li>Introduced an optional titlebar that display file name and allows drag and double-click to move and toggle maximize state</li>
|
||||
<li>Windows: Now the application will use the system's built-in animation for open window animation</li>
|
||||
</ul>
|
||||
<p>This release fixes the following bug:</p>
|
||||
<ul>
|
||||
<li>Show empty url list should also reset window title to blank</li>
|
||||
</ul>
|
||||
<p>This release includes the following changes:</p>
|
||||
<ul>
|
||||
<li>Update translations</li>
|
||||
<li>Add vcs-browser link to AppStream metadata</li>
|
||||
</ul>
|
||||
<p>With contributions from:</p>
|
||||
<p>Heimen Stoffels (vistaus), VenusGirl (VenusGirl)</p>
|
||||
</description>
|
||||
</release>
|
||||
<release type="stable" version="1.4.1" date="2026-04-05T00:00:00Z">
|
||||
<description>
|
||||
<p>This release includes the following changes:</p>
|
||||
|
||||
Vendored
-118
@@ -1,118 +0,0 @@
|
||||
targetApp=ppic.exe
|
||||
friendlyAppName=Pineapple Pictures
|
||||
friendlyAppName[zh_CN]=菠萝看图
|
||||
openCommand="\"{targetAppFullPath}\" \"%1\""
|
||||
|
||||
[ProgId/jpeg]
|
||||
name=JPEG Image
|
||||
name[zh_CN]=JPEG 图像
|
||||
extensions=jpg,jpeg,jfif
|
||||
|
||||
[ProgId/gif]
|
||||
name=GIF Animation Image
|
||||
name[zh_CN]=GIF 动图
|
||||
|
||||
[ProgId/png]
|
||||
name=PNG Image
|
||||
name[zh_CN]=PNG 图像
|
||||
|
||||
[ProgId/svg]
|
||||
name=Scalable Vector Graphics
|
||||
name[zh_CN]=可缩放矢量图像
|
||||
|
||||
[ProgId/webp]
|
||||
name=WebP Image
|
||||
name[zh_CN]=WebP 图像
|
||||
extensions=webp
|
||||
|
||||
[ProgId/avif]
|
||||
name=AV1 Image File
|
||||
name[zh_CN]=AV1 图像文件
|
||||
|
||||
[ProgId/ico]
|
||||
name=Windows Icon Image
|
||||
name[zh_CN]=Windows 图标图像
|
||||
|
||||
[ProgId/icns]
|
||||
name=Apple Icon Image
|
||||
name[zh_CN]=苹果图标图像
|
||||
|
||||
[ProgId/psd]
|
||||
name=Photoshop Document
|
||||
name[zh_CN]=Photoshop 文档
|
||||
extensions=psd,psb,pdd,psdt
|
||||
|
||||
[ProgId/kra]
|
||||
name=Krita Document
|
||||
name[zh_CN]=Krita 文档
|
||||
|
||||
[ProgId/xcf]
|
||||
name=GIMP Document
|
||||
name[zh_CN]=GIMP 文档
|
||||
|
||||
[ProgId/tga]
|
||||
name=TARGA Image
|
||||
name[zh_CN]=TARGA 图像
|
||||
|
||||
[ProgId/tif]
|
||||
name=Tagged Image File Format
|
||||
name[zh_CN]=标签图像文件格式
|
||||
extensions=tif,tiff
|
||||
|
||||
[ProgId/bmp]
|
||||
name=Bitmap
|
||||
name[zh_CN]=位图
|
||||
|
||||
[ProgId/wbmp]
|
||||
name=Wireless Bitmap
|
||||
name[zh_CN]=无线位图
|
||||
|
||||
[ProgId/dds]
|
||||
name=DirectDraw Surface
|
||||
|
||||
[ProgId/hdr]
|
||||
name=High Dynamic Range Image
|
||||
name[zh_CN]=高动态范围图像
|
||||
|
||||
[ProgId/iff]
|
||||
name=Interchange File Format
|
||||
name[zh_CN]=可交换文件格式
|
||||
|
||||
[ProgId/pic]
|
||||
name=Softimage Picture
|
||||
name[zh_CN]=Softimage 图像
|
||||
|
||||
[ProgId/pcx]
|
||||
name=PiCture eXchange PC Paintbrush Image
|
||||
name[zh_CN]=PC Paintbrush 图像
|
||||
|
||||
[ProgId/qoi]
|
||||
name=Quite OK Image
|
||||
name[zh_CN]=挺 OK 图像
|
||||
|
||||
[ProgId/ras]
|
||||
name=Sun Raster Image
|
||||
name[zh_CN]=Sun 栅格图象
|
||||
extensions=ras,sun
|
||||
|
||||
[ProgId/ora]
|
||||
name=OpenRaster Image
|
||||
name[zh_CN]=OpenRaster 图象
|
||||
|
||||
[ProgId/ani]
|
||||
name=Animated Cursor
|
||||
name[zh_CN]=动态光标
|
||||
|
||||
[ProgId/pfm]
|
||||
name=Portable Float Map
|
||||
extensions=pfm,phm
|
||||
|
||||
[ProgId/rgb]
|
||||
name=Silicon Graphics Image
|
||||
extensions=rgb,rgba,bw,sgi
|
||||
|
||||
[ProgId/pxr]
|
||||
name=Pixar Raster Image
|
||||
|
||||
[ProgId/sct]
|
||||
name=Scitex Continuous Tone
|
||||
@@ -1,101 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2025 Gary Wang <git@blumia.net>
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
QT += core widgets gui svg svgwidgets
|
||||
|
||||
TARGET = ppic
|
||||
TEMPLATE = app
|
||||
DEFINES += PPIC_VERSION_STRING=\\\"x.y.z\\\"
|
||||
|
||||
win32 {
|
||||
DEFINES += FLAG_PORTABLE_MODE_SUPPORT=1
|
||||
}
|
||||
|
||||
# The following define makes your compiler emit warnings if you use
|
||||
# any feature of Qt which has been marked as deprecated (the exact warnings
|
||||
# depend on your compiler). Please consult the documentation of the
|
||||
# deprecated API in order to know how to port your code away from it.
|
||||
DEFINES += QT_DEPRECATED_WARNINGS
|
||||
|
||||
# You can also make your code fail to compile if you use deprecated APIs.
|
||||
# In order to do so, uncomment the following line.
|
||||
# You can also select to disable deprecated APIs only up to a certain version of Qt.
|
||||
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
|
||||
|
||||
CONFIG += c++17 lrelease embed_translations
|
||||
|
||||
SOURCES += \
|
||||
app/aboutdialog.cpp \
|
||||
app/main.cpp \
|
||||
app/framelesswindow.cpp \
|
||||
app/mainwindow.cpp \
|
||||
app/graphicsview.cpp \
|
||||
app/bottombuttongroup.cpp \
|
||||
app/graphicsscene.cpp \
|
||||
app/navigatorview.cpp \
|
||||
app/opacityhelper.cpp \
|
||||
app/toolbutton.cpp \
|
||||
app/settings.cpp \
|
||||
app/settingsdialog.cpp \
|
||||
app/metadatamodel.cpp \
|
||||
app/metadatadialog.cpp \
|
||||
app/exiv2wrapper.cpp \
|
||||
app/actionmanager.cpp \
|
||||
app/playlistmanager.cpp \
|
||||
app/shortcutedit.cpp \
|
||||
app/fileopeneventhandler.cpp
|
||||
|
||||
HEADERS += \
|
||||
app/aboutdialog.h \
|
||||
app/framelesswindow.h \
|
||||
app/mainwindow.h \
|
||||
app/graphicsview.h \
|
||||
app/bottombuttongroup.h \
|
||||
app/graphicsscene.h \
|
||||
app/navigatorview.h \
|
||||
app/opacityhelper.h \
|
||||
app/toolbutton.h \
|
||||
app/settings.h \
|
||||
app/settingsdialog.h \
|
||||
app/metadatamodel.h \
|
||||
app/metadatadialog.h \
|
||||
app/exiv2wrapper.h \
|
||||
app/actionmanager.h \
|
||||
app/playlistmanager.h \
|
||||
app/shortcutedit.h \
|
||||
app/fileopeneventhandler.h
|
||||
|
||||
TRANSLATIONS = \
|
||||
app/translations/PineapplePictures_en.ts \
|
||||
app/translations/PineapplePictures_zh_CN.ts \
|
||||
app/translations/PineapplePictures_de.ts \
|
||||
app/translations/PineapplePictures_es.ts \
|
||||
app/translations/PineapplePictures_fr.ts \
|
||||
app/translations/PineapplePictures_nb_NO.ts \
|
||||
app/translations/PineapplePictures_nl.ts \
|
||||
app/translations/PineapplePictures_ru.ts \
|
||||
app/translations/PineapplePictures_si.ts \
|
||||
app/translations/PineapplePictures_id.ts
|
||||
|
||||
# Default rules for deployment.
|
||||
qnx: target.path = /tmp/$${TARGET}/bin
|
||||
else: unix:!android: target.path = /opt/$${TARGET}/bin
|
||||
!isEmpty(target.path): INSTALLS += target
|
||||
|
||||
RESOURCES += \
|
||||
assets/resources.qrc
|
||||
|
||||
# Generate from svg:
|
||||
# magick -density 512x512 -background none app-icon.svg -define icon:auto-resize app-icon.ico
|
||||
RC_ICONS = assets/icons/app-icon.ico
|
||||
|
||||
# Windows only, for rc file (we're not going to use the .rc file in this repo)
|
||||
QMAKE_TARGET_PRODUCT = Pineapple Pictures
|
||||
QMAKE_TARGET_DESCRIPTION = Pineapple Pictures - Image Viewer
|
||||
QMAKE_TARGET_COPYRIGHT = MIT/Expat License - Copyright (C) 2024 Gary Wang
|
||||
|
||||
# MSVC only, since QMake doesn't have a CMAKE_CXX_STANDARD_LIBRARIES or cpp_winlibs similar thing
|
||||
win32-msvc* {
|
||||
LIBS += -luser32
|
||||
}
|
||||
Reference in New Issue
Block a user