From ed5a6023326fd2ab420ded76976501be33e0b389 Mon Sep 17 00:00:00 2001 From: Gary Wang Date: Wed, 23 Jul 2025 21:20:34 +0800 Subject: [PATCH] chore: let's use LF all the time --- LICENSE | 42 +- REUSE.toml | 66 +-- app/aboutdialog.cpp | 362 ++++++++-------- app/aboutdialog.h | 72 ++-- app/actionmanager.cpp | 306 +++++++------- app/actionmanager.h | 116 +++--- app/bottombuttongroup.cpp | 114 ++--- app/bottombuttongroup.h | 54 +-- app/exiv2wrapper.cpp | 276 ++++++------ app/exiv2wrapper.h | 86 ++-- app/framelesswindow.cpp | 272 ++++++------ app/framelesswindow.h | 78 ++-- app/graphicsview.cpp | 742 ++++++++++++++++----------------- app/graphicsview.h | 158 +++---- app/main.cpp | 206 ++++----- app/mainwindow.cpp | 2 +- app/mainwindow.h | 268 ++++++------ app/metadatadialog.cpp | 220 +++++----- app/metadatadialog.h | 60 +-- app/metadatamodel.cpp | 640 ++++++++++++++-------------- app/metadatamodel.h | 104 ++--- app/navigatorview.cpp | 172 ++++---- app/navigatorview.h | 76 ++-- app/opacityhelper.cpp | 62 +-- app/opacityhelper.h | 54 +-- app/playlistmanager.cpp | 566 ++++++++++++------------- app/playlistmanager.h | 176 ++++---- app/settings.cpp | 452 ++++++++++---------- app/settings.h | 138 +++--- app/settingsdialog.cpp | 400 +++++++++--------- app/settingsdialog.h | 70 ++-- app/toolbutton.cpp | 76 ++-- app/toolbutton.h | 50 +-- dist/MacOSXBundleInfo.plist.in | 272 ++++++------ pineapple-pictures.pro | 202 ++++----- 35 files changed, 3505 insertions(+), 3505 deletions(-) diff --git a/LICENSE b/LICENSE index 33a11a4..49c4b2c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,21 @@ -MIT License - -Copyright (c) 2025 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. +MIT License + +Copyright (c) 2025 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. diff --git a/REUSE.toml b/REUSE.toml index 15d9cd9..11c5e82 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -1,33 +1,33 @@ -version = 1 -SPDX-PackageName = "Pineapple Pictures" -SPDX-PackageDownloadLocation = "https://github.com/BLumia/pineapple-pictures" - -[[annotations]] -path = [".gitignore", "appveyor.yml", ".github/**"] -precedence = "aggregate" -SPDX-FileCopyrightText = "None" -SPDX-License-Identifier = "CC0-1.0" - -[[annotations]] -path = ["README**.md", "NEWS", "assets/**.rc", "assets/**.qrc", "dist/**"] -precedence = "aggregate" -SPDX-FileCopyrightText = "None" -SPDX-License-Identifier = "CC0-1.0" - -[[annotations]] -path = ["app/translations/**.ts", "assets/plain/translators.html"] -precedence = "aggregate" -SPDX-FileCopyrightText = "Translators from hosted.weblate.org" -SPDX-License-Identifier = "MIT" - -[[annotations]] -path = "assets/icons/**.svg" -precedence = "aggregate" -SPDX-FileCopyrightText = "2022 Gary Wang" -SPDX-License-Identifier = "MIT" - -[[annotations]] -path = "assets/icons/app-icon.**" -precedence = "aggregate" -SPDX-FileCopyrightText = "2020 Lovelyblack" -SPDX-License-Identifier = "MIT" +version = 1 +SPDX-PackageName = "Pineapple Pictures" +SPDX-PackageDownloadLocation = "https://github.com/BLumia/pineapple-pictures" + +[[annotations]] +path = [".gitignore", "appveyor.yml", ".github/**"] +precedence = "aggregate" +SPDX-FileCopyrightText = "None" +SPDX-License-Identifier = "CC0-1.0" + +[[annotations]] +path = ["README**.md", "NEWS", "assets/**.rc", "assets/**.qrc", "dist/**"] +precedence = "aggregate" +SPDX-FileCopyrightText = "None" +SPDX-License-Identifier = "CC0-1.0" + +[[annotations]] +path = ["app/translations/**.ts", "assets/plain/translators.html"] +precedence = "aggregate" +SPDX-FileCopyrightText = "Translators from hosted.weblate.org" +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = "assets/icons/**.svg" +precedence = "aggregate" +SPDX-FileCopyrightText = "2022 Gary Wang" +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = "assets/icons/app-icon.**" +precedence = "aggregate" +SPDX-FileCopyrightText = "2020 Lovelyblack" +SPDX-License-Identifier = "MIT" diff --git a/app/aboutdialog.cpp b/app/aboutdialog.cpp index 83d4898..3cd2a68 100644 --- a/app/aboutdialog.cpp +++ b/app/aboutdialog.cpp @@ -1,181 +1,181 @@ -// SPDX-FileCopyrightText: 2022 Gary Wang -// -// SPDX-License-Identifier: MIT - -#include "aboutdialog.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace Qt::Literals::StringLiterals; - -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) -{ - this->setWindowTitle(tr("About")); - - const QStringList helpStr { - u"

%1

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

%1

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

%1

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

%1

"_s.arg(tr("Context menu option explanation:")), - u"
    "_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"
  • %1:
    %2
  • "_s - .arg(QCoreApplication::translate("MainWindow", "Stay on top")) - .arg(tr("Make window stay on top of all other windows.")), - u"
  • %1:
    %2
  • "_s - .arg(QCoreApplication::translate("MainWindow", "Protected mode")) - .arg(tr("Avoid close window accidentally. (eg. by double clicking the window)")), - u"
  • %1:
    %2
  • "_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"
"_s - }; - - const QStringList aboutStr { - u"

"_s, - qApp->applicationDisplayName(), - (u"
"_s + tr("Version: %1").arg( -#ifdef GIT_DESCRIBE_VERSION_STRING - GIT_DESCRIBE_VERSION_STRING -#else - qApp->applicationVersion() -#endif // GIT_DESCRIBE_VERSION_STRING - )), - u"
"_s, - tr("Copyright (c) %1 %2", "%1 is year, %2 is the name of copyright holder(s)") - .arg(u"2025"_s, u"@BLumia"_s), - u"
"_s, - tr("Logo designed by %1").arg(u"@Lovelyblack"_s), - u"
"_s, - tr("Built with Qt %1 (%2)").arg(QT_VERSION_STR, QSysInfo::buildCpuArchitecture()), - QStringLiteral("
%2").arg("https://github.com/BLumia/pineapple-pictures", tr("Source code")), - u"
"_s - }; - - QFile translaterHtml(u":/plain/translators.html"_s); - bool canOpenFile = translaterHtml.open(QIODevice::ReadOnly); - const QByteArray & translatorList = canOpenFile ? translaterHtml.readAll() : QByteArrayLiteral(""); - - const QStringList specialThanksStr { - u"

%1

%3

%4

"_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.") - ), - - u"

%1

%2

%3"_s.arg( - tr("Translators"), - tr("I would like to thank the following people who volunteered to translate this application."), - translatorList - ) - }; - - const QStringList licenseStr { - u"

%1

"_s.arg(tr("Your Rights")), - u"

%1

%2

  • %3
  • %4
  • %5
  • %6
"_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"%1"_s), - u"

%1

"_s.arg(tr("The MIT license guarantees you this freedom. Nobody is ever permitted to take it away.")), - u"
%2
"_s - }; - - const QString mitLicense(QStringLiteral(R"(Expat/MIT License - -Copyright (c) 2025 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"

%1

"_s.arg(tr("Third-party Libraries used by %1")), - tr("%1 is built on the following free software libraries:", "Free as in freedom"), - u"
    "_s, -#ifdef HAVE_EXIV2_VERSION - u"
  • %2: %3
  • "_s.arg("https://www.exiv2.org/", "Exiv2", "GPLv2"), -#endif // EXIV2_VERSION - u"
  • %2: %3
  • "_s.arg("https://www.qt.io/", "Qt", "GPLv2 + GPLv3 + LGPLv2.1 + LGPLv3"), - u"
"_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"%1"_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::of(&QDialogButtonBox::clicked), this, [this](){ - 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() -{ - -} - -QSize AboutDialog::sizeHint() const -{ - return QSize(520, 350); -} +// SPDX-FileCopyrightText: 2022 Gary Wang +// +// SPDX-License-Identifier: MIT + +#include "aboutdialog.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Qt::Literals::StringLiterals; + +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) +{ + this->setWindowTitle(tr("About")); + + const QStringList helpStr { + u"

%1

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

%1

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

%1

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

%1

"_s.arg(tr("Context menu option explanation:")), + u"
    "_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"
  • %1:
    %2
  • "_s + .arg(QCoreApplication::translate("MainWindow", "Stay on top")) + .arg(tr("Make window stay on top of all other windows.")), + u"
  • %1:
    %2
  • "_s + .arg(QCoreApplication::translate("MainWindow", "Protected mode")) + .arg(tr("Avoid close window accidentally. (eg. by double clicking the window)")), + u"
  • %1:
    %2
  • "_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"
"_s + }; + + const QStringList aboutStr { + u"

"_s, + qApp->applicationDisplayName(), + (u"
"_s + tr("Version: %1").arg( +#ifdef GIT_DESCRIBE_VERSION_STRING + GIT_DESCRIBE_VERSION_STRING +#else + qApp->applicationVersion() +#endif // GIT_DESCRIBE_VERSION_STRING + )), + u"
"_s, + tr("Copyright (c) %1 %2", "%1 is year, %2 is the name of copyright holder(s)") + .arg(u"2025"_s, u"@BLumia"_s), + u"
"_s, + tr("Logo designed by %1").arg(u"@Lovelyblack"_s), + u"
"_s, + tr("Built with Qt %1 (%2)").arg(QT_VERSION_STR, QSysInfo::buildCpuArchitecture()), + QStringLiteral("
%2").arg("https://github.com/BLumia/pineapple-pictures", tr("Source code")), + u"
"_s + }; + + QFile translaterHtml(u":/plain/translators.html"_s); + bool canOpenFile = translaterHtml.open(QIODevice::ReadOnly); + const QByteArray & translatorList = canOpenFile ? translaterHtml.readAll() : QByteArrayLiteral(""); + + const QStringList specialThanksStr { + u"

%1

%3

%4

"_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.") + ), + + u"

%1

%2

%3"_s.arg( + tr("Translators"), + tr("I would like to thank the following people who volunteered to translate this application."), + translatorList + ) + }; + + const QStringList licenseStr { + u"

%1

"_s.arg(tr("Your Rights")), + u"

%1

%2

  • %3
  • %4
  • %5
  • %6
"_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"%1"_s), + u"

%1

"_s.arg(tr("The MIT license guarantees you this freedom. Nobody is ever permitted to take it away.")), + u"
%2
"_s + }; + + const QString mitLicense(QStringLiteral(R"(Expat/MIT License + +Copyright (c) 2025 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"

%1

"_s.arg(tr("Third-party Libraries used by %1")), + tr("%1 is built on the following free software libraries:", "Free as in freedom"), + u"
    "_s, +#ifdef HAVE_EXIV2_VERSION + u"
  • %2: %3
  • "_s.arg("https://www.exiv2.org/", "Exiv2", "GPLv2"), +#endif // EXIV2_VERSION + u"
  • %2: %3
  • "_s.arg("https://www.qt.io/", "Qt", "GPLv2 + GPLv3 + LGPLv2.1 + LGPLv3"), + u"
"_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"%1"_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::of(&QDialogButtonBox::clicked), this, [this](){ + 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() +{ + +} + +QSize AboutDialog::sizeHint() const +{ + return QSize(520, 350); +} diff --git a/app/aboutdialog.h b/app/aboutdialog.h index d6d9276..393f2b9 100644 --- a/app/aboutdialog.h +++ b/app/aboutdialog.h @@ -1,36 +1,36 @@ -// SPDX-FileCopyrightText: 2022 Gary Wang -// -// SPDX-License-Identifier: MIT - -#ifndef ABOUTDIALOG_H -#define ABOUTDIALOG_H - -#include - -QT_BEGIN_NAMESPACE -class QTextBrowser; -class QTabWidget; -class QDialogButtonBox; -QT_END_NAMESPACE - -class AboutDialog : public QDialog -{ - Q_OBJECT -public: - explicit AboutDialog(QWidget *parent = nullptr); - ~AboutDialog() override; - - 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; -}; - -#endif // ABOUTDIALOG_H +// SPDX-FileCopyrightText: 2022 Gary Wang +// +// SPDX-License-Identifier: MIT + +#ifndef ABOUTDIALOG_H +#define ABOUTDIALOG_H + +#include + +QT_BEGIN_NAMESPACE +class QTextBrowser; +class QTabWidget; +class QDialogButtonBox; +QT_END_NAMESPACE + +class AboutDialog : public QDialog +{ + Q_OBJECT +public: + explicit AboutDialog(QWidget *parent = nullptr); + ~AboutDialog() override; + + 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; +}; + +#endif // ABOUTDIALOG_H diff --git a/app/actionmanager.cpp b/app/actionmanager.cpp index ae028fc..4a89423 100644 --- a/app/actionmanager.cpp +++ b/app/actionmanager.cpp @@ -1,153 +1,153 @@ -// SPDX-FileCopyrightText: 2022 Gary Wang -// -// SPDX-License-Identifier: MIT - -#include "actionmanager.h" - -#include "mainwindow.h" - -#include -#include -#include - -#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) -{ - QSvgRenderer r(resp); - QPixmap pm = QPixmap(sz * qApp->devicePixelRatio()); - pm.fill(Qt::transparent); - QPainter p(&pm); - r.render(&p); - pm.setDevicePixelRatio(qApp->devicePixelRatio()); - return QIcon(pm); -} - -void ActionManager::setupAction(MainWindow *mainWindow) -{ - 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)->setObjectName(an); - w->addAction(*a); - }; - #define CREATE_NEW_ICON_ACTION(w, a, i) create_action(w, &a, ICON_NAME(i), ACTION_NAME(a)) - CREATE_NEW_ICON_ACTION(mainWindow, actionActualSize, zoom-original); - CREATE_NEW_ICON_ACTION(mainWindow, actionToggleMaximize, view-fullscreen); - 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, 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_ACTION(mainWindow, actionTogglePauseAnimation); - CREATE_NEW_ACTION(mainWindow, actionAnimationNextFrame); - - CREATE_NEW_THEMEICON_ACTION(mainWindow, actionOpen, document-open); - CREATE_NEW_ACTION(mainWindow, actionHorizontalFlip); - CREATE_NEW_ACTION(mainWindow, actionFitInView); - CREATE_NEW_ACTION(mainWindow, actionFitByWidth); - 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 - #undef CREATE_NEW_THEMEICON_ACTION - - retranslateUi(mainWindow); - - QMetaObject::connectSlotsByName(mainWindow); -} - -void ActionManager::retranslateUi(MainWindow *mainWindow) -{ - Q_UNUSED(mainWindow); - - actionOpen->setText(QCoreApplication::translate("MainWindow", "&Open...", nullptr)); - - 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)); - - actionPrevPicture->setText(QCoreApplication::translate("MainWindow", "Previous image", nullptr)); - actionNextPicture->setText(QCoreApplication::translate("MainWindow", "Next image", 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)); - 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)); -} - -void ActionManager::setupShortcuts() -{ - actionOpen->setShortcut(QKeySequence::Open); - 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) - }); -} - +// SPDX-FileCopyrightText: 2022 Gary Wang +// +// SPDX-License-Identifier: MIT + +#include "actionmanager.h" + +#include "mainwindow.h" + +#include +#include +#include + +#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) +{ + QSvgRenderer r(resp); + QPixmap pm = QPixmap(sz * qApp->devicePixelRatio()); + pm.fill(Qt::transparent); + QPainter p(&pm); + r.render(&p); + pm.setDevicePixelRatio(qApp->devicePixelRatio()); + return QIcon(pm); +} + +void ActionManager::setupAction(MainWindow *mainWindow) +{ + 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)->setObjectName(an); + w->addAction(*a); + }; + #define CREATE_NEW_ICON_ACTION(w, a, i) create_action(w, &a, ICON_NAME(i), ACTION_NAME(a)) + CREATE_NEW_ICON_ACTION(mainWindow, actionActualSize, zoom-original); + CREATE_NEW_ICON_ACTION(mainWindow, actionToggleMaximize, view-fullscreen); + 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, 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_ACTION(mainWindow, actionTogglePauseAnimation); + CREATE_NEW_ACTION(mainWindow, actionAnimationNextFrame); + + CREATE_NEW_THEMEICON_ACTION(mainWindow, actionOpen, document-open); + CREATE_NEW_ACTION(mainWindow, actionHorizontalFlip); + CREATE_NEW_ACTION(mainWindow, actionFitInView); + CREATE_NEW_ACTION(mainWindow, actionFitByWidth); + 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 + #undef CREATE_NEW_THEMEICON_ACTION + + retranslateUi(mainWindow); + + QMetaObject::connectSlotsByName(mainWindow); +} + +void ActionManager::retranslateUi(MainWindow *mainWindow) +{ + Q_UNUSED(mainWindow); + + actionOpen->setText(QCoreApplication::translate("MainWindow", "&Open...", nullptr)); + + 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)); + + actionPrevPicture->setText(QCoreApplication::translate("MainWindow", "Previous image", nullptr)); + actionNextPicture->setText(QCoreApplication::translate("MainWindow", "Next image", 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)); + 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)); +} + +void ActionManager::setupShortcuts() +{ + actionOpen->setShortcut(QKeySequence::Open); + 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) + }); +} + diff --git a/app/actionmanager.h b/app/actionmanager.h index 3c42408..af5cfcc 100644 --- a/app/actionmanager.h +++ b/app/actionmanager.h @@ -1,58 +1,58 @@ -// SPDX-FileCopyrightText: 2025 Gary Wang -// -// SPDX-License-Identifier: MIT - -#ifndef ACTIONMANAGER_H -#define ACTIONMANAGER_H - -#include - -class MainWindow; - -class ActionManager -{ -public: - explicit ActionManager() = default; - ~ActionManager() = default; - - void setupAction(MainWindow * mainWindow); - void retranslateUi(MainWindow *MainWindow); - void setupShortcuts(); - - static QIcon loadHidpiIcon(const QString &resp, QSize sz = QSize(32, 32)); - -public: - QAction *actionOpen; - - QAction *actionActualSize; - QAction *actionToggleMaximize; - QAction *actionZoomIn; - QAction *actionZoomOut; - QAction *actionToggleCheckerboard; - QAction *actionRotateClockwise; - QAction *actionRotateCounterClockwise; - - QAction *actionPrevPicture; - QAction *actionNextPicture; - - QAction *actionTogglePauseAnimation; - QAction *actionAnimationNextFrame; - - QAction *actionHorizontalFlip; - QAction *actionFitInView; - QAction *actionFitByWidth; - QAction *actionCopyPixmap; - QAction *actionCopyFilePath; - QAction *actionPaste; - QAction *actionTrash; - QAction *actionToggleStayOnTop; - QAction *actionToggleProtectMode; - QAction *actionToggleAvoidResetTransform; - QAction *actionSettings; - QAction *actionHelp; - QAction *actionLocateInFileManager; - QAction *actionProperties; - QAction *actionQuitApp; -}; - -#endif // ACTIONMANAGER_H +// SPDX-FileCopyrightText: 2025 Gary Wang +// +// SPDX-License-Identifier: MIT + +#ifndef ACTIONMANAGER_H +#define ACTIONMANAGER_H + +#include + +class MainWindow; + +class ActionManager +{ +public: + explicit ActionManager() = default; + ~ActionManager() = default; + + void setupAction(MainWindow * mainWindow); + void retranslateUi(MainWindow *MainWindow); + void setupShortcuts(); + + static QIcon loadHidpiIcon(const QString &resp, QSize sz = QSize(32, 32)); + +public: + QAction *actionOpen; + + QAction *actionActualSize; + QAction *actionToggleMaximize; + QAction *actionZoomIn; + QAction *actionZoomOut; + QAction *actionToggleCheckerboard; + QAction *actionRotateClockwise; + QAction *actionRotateCounterClockwise; + + QAction *actionPrevPicture; + QAction *actionNextPicture; + + QAction *actionTogglePauseAnimation; + QAction *actionAnimationNextFrame; + + QAction *actionHorizontalFlip; + QAction *actionFitInView; + QAction *actionFitByWidth; + QAction *actionCopyPixmap; + QAction *actionCopyFilePath; + QAction *actionPaste; + QAction *actionTrash; + QAction *actionToggleStayOnTop; + QAction *actionToggleProtectMode; + QAction *actionToggleAvoidResetTransform; + QAction *actionSettings; + QAction *actionHelp; + QAction *actionLocateInFileManager; + QAction *actionProperties; + QAction *actionQuitApp; +}; + +#endif // ACTIONMANAGER_H diff --git a/app/bottombuttongroup.cpp b/app/bottombuttongroup.cpp index 872cd6a..22eb26b 100644 --- a/app/bottombuttongroup.cpp +++ b/app/bottombuttongroup.cpp @@ -1,57 +1,57 @@ -// SPDX-FileCopyrightText: 2022 Gary Wang -// -// SPDX-License-Identifier: MIT - -#include "bottombuttongroup.h" - -#include "opacityhelper.h" - -#include -#include -#include - -BottomButtonGroup::BottomButtonGroup(const std::vector &actionList, 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;" - "}"); - - auto newActionBtn = [this](QAction * action) -> QToolButton * { - 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)); - } -} - -void BottomButtonGroup::setOpacity(qreal opacity, bool animated) -{ - m_opacityHelper->setOpacity(opacity, animated); -} - -void BottomButtonGroup::addButton(QAbstractButton *button) -{ - layout()->addWidget(button); - updateGeometry(); -} +// SPDX-FileCopyrightText: 2022 Gary Wang +// +// SPDX-License-Identifier: MIT + +#include "bottombuttongroup.h" + +#include "opacityhelper.h" + +#include +#include +#include + +BottomButtonGroup::BottomButtonGroup(const std::vector &actionList, 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;" + "}"); + + auto newActionBtn = [this](QAction * action) -> QToolButton * { + 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)); + } +} + +void BottomButtonGroup::setOpacity(qreal opacity, bool animated) +{ + m_opacityHelper->setOpacity(opacity, animated); +} + +void BottomButtonGroup::addButton(QAbstractButton *button) +{ + layout()->addWidget(button); + updateGeometry(); +} diff --git a/app/bottombuttongroup.h b/app/bottombuttongroup.h index 76cf4f7..3e84aff 100644 --- a/app/bottombuttongroup.h +++ b/app/bottombuttongroup.h @@ -1,27 +1,27 @@ -// SPDX-FileCopyrightText: 2022 Gary Wang -// -// SPDX-License-Identifier: MIT - -#ifndef BOTTOMBUTTONGROUP_H -#define BOTTOMBUTTONGROUP_H - -#include - -#include -#include - -class OpacityHelper; -class BottomButtonGroup : public QGroupBox -{ - Q_OBJECT -public: - explicit BottomButtonGroup(const std::vector & actionList, QWidget *parent = nullptr); - - void setOpacity(qreal opacity, bool animated = true); - void addButton(QAbstractButton *button); - -private: - OpacityHelper * m_opacityHelper; -}; - -#endif // BOTTOMBUTTONGROUP_H +// SPDX-FileCopyrightText: 2022 Gary Wang +// +// SPDX-License-Identifier: MIT + +#ifndef BOTTOMBUTTONGROUP_H +#define BOTTOMBUTTONGROUP_H + +#include + +#include +#include + +class OpacityHelper; +class BottomButtonGroup : public QGroupBox +{ + Q_OBJECT +public: + explicit BottomButtonGroup(const std::vector & actionList, QWidget *parent = nullptr); + + void setOpacity(qreal opacity, bool animated = true); + void addButton(QAbstractButton *button); + +private: + OpacityHelper * m_opacityHelper; +}; + +#endif // BOTTOMBUTTONGROUP_H diff --git a/app/exiv2wrapper.cpp b/app/exiv2wrapper.cpp index e57027a..eb98fe3 100644 --- a/app/exiv2wrapper.cpp +++ b/app/exiv2wrapper.cpp @@ -1,138 +1,138 @@ -// SPDX-FileCopyrightText: 2022 Gary Wang -// -// SPDX-License-Identifier: MIT - -#include "exiv2wrapper.h" - -#ifdef HAVE_EXIV2_VERSION -#include -#else // HAVE_EXIV2_VERSION -namespace Exiv2 { -class Image {}; -} -#endif // HAVE_EXIV2_VERSION - -#include - -#include -#include - -Exiv2Wrapper::Exiv2Wrapper() -{ - -} - -Exiv2Wrapper::~Exiv2Wrapper() -{ - -} - -#ifdef HAVE_EXIV2_VERSION // stupid AppleClang... -template -void Exiv2Wrapper::cacheSection(Collection collection) -{ - const Collection& exifData = collection; - Iterator it = exifData.begin(), end = exifData.end(); - for (; it != end; ++it) { - QString key = QString::fromUtf8(it->key().c_str()); - if (it->tagName().substr(0, 2) == "0x") continue; - // We might get exceptions like "No namespace info available for XMP prefix `Item'" - // when trying to get tagLabel() data from a Xmpdatum if the tag is not common-used. - // We don't care for those rare tags so let's just use a try-cache... - try { - QString label = QString::fromLocal8Bit(it->tagLabel().c_str()); - std::ostringstream stream; - stream << *it; - QString value = QString::fromUtf8(stream.str().c_str()); - m_metadataValue.insert(key, value); - m_metadataLabel.insert(key, label); - qDebug() << key << label << value; -#if EXIV2_TEST_VERSION(0, 28, 0) - } catch (Exiv2::Error & err) { -#else // 0.27.x - } catch (Exiv2::AnyError & err) { -#endif // EXIV2_TEST_VERSION(0, 28, 0) - qWarning() << "Error loading key" << key << ":" << err.what(); - } - } -} -#endif // HAVE_EXIV2_VERSION - -bool Exiv2Wrapper::load(const QString &filePath) -{ -#ifdef HAVE_EXIV2_VERSION - QByteArray filePathByteArray = QFile::encodeName(filePath); - try { - m_exivImage.reset(Exiv2::ImageFactory::open(filePathByteArray.constData()).release()); - m_exivImage->readMetadata(); - } catch (const Exiv2::Error& error) { - m_errMsg = QString::fromUtf8(error.what()); - return false; - } - return true; -#else // HAVE_EXIV2_VERSION - Q_UNUSED(filePath); - return false; -#endif // HAVE_EXIV2_VERSION -} - -void Exiv2Wrapper::cacheSections() -{ -#ifdef HAVE_EXIV2_VERSION - if (m_exivImage->checkMode(Exiv2::mdExif) & Exiv2::amRead) { - cacheSection(m_exivImage->exifData()); - } - - if (m_exivImage->checkMode(Exiv2::mdIptc) & Exiv2::amRead) { - cacheSection(m_exivImage->iptcData()); - } - - if (m_exivImage->checkMode(Exiv2::mdXmp) & Exiv2::amRead) { - cacheSection(m_exivImage->xmpData()); - } - -// qDebug() << m_metadataValue; -// qDebug() << m_metadataLabel; -#endif // HAVE_EXIV2_VERSION -} - -QString Exiv2Wrapper::comment() const -{ -#ifdef HAVE_EXIV2_VERSION - return m_exivImage->comment().c_str(); -#else // HAVE_EXIV2_VERSION - return QString(); -#endif // HAVE_EXIV2_VERSION -} - -QString Exiv2Wrapper::label(const QString &key) const -{ - return m_metadataLabel.value(key); -} - -QString Exiv2Wrapper::value(const QString &key) const -{ - return m_metadataValue.value(key); -} - -QString Exiv2Wrapper::XmpValue(const QString &rawValue) -{ - QString ignored; - return Exiv2Wrapper::XmpValue(rawValue, ignored); -} - -QString Exiv2Wrapper::XmpValue(const QString &rawValue, QString &language) -{ - if (rawValue.size() > 6 && rawValue.startsWith(QLatin1String("lang=\""))) { - int pos = rawValue.indexOf('"', 6); - - if (pos != -1) { - language = rawValue.mid(6, pos - 6); - return (rawValue.mid(pos + 2)); - } - } - - language.clear(); - return rawValue; -} - +// SPDX-FileCopyrightText: 2022 Gary Wang +// +// SPDX-License-Identifier: MIT + +#include "exiv2wrapper.h" + +#ifdef HAVE_EXIV2_VERSION +#include +#else // HAVE_EXIV2_VERSION +namespace Exiv2 { +class Image {}; +} +#endif // HAVE_EXIV2_VERSION + +#include + +#include +#include + +Exiv2Wrapper::Exiv2Wrapper() +{ + +} + +Exiv2Wrapper::~Exiv2Wrapper() +{ + +} + +#ifdef HAVE_EXIV2_VERSION // stupid AppleClang... +template +void Exiv2Wrapper::cacheSection(Collection collection) +{ + const Collection& exifData = collection; + Iterator it = exifData.begin(), end = exifData.end(); + for (; it != end; ++it) { + QString key = QString::fromUtf8(it->key().c_str()); + if (it->tagName().substr(0, 2) == "0x") continue; + // We might get exceptions like "No namespace info available for XMP prefix `Item'" + // when trying to get tagLabel() data from a Xmpdatum if the tag is not common-used. + // We don't care for those rare tags so let's just use a try-cache... + try { + QString label = QString::fromLocal8Bit(it->tagLabel().c_str()); + std::ostringstream stream; + stream << *it; + QString value = QString::fromUtf8(stream.str().c_str()); + m_metadataValue.insert(key, value); + m_metadataLabel.insert(key, label); + qDebug() << key << label << value; +#if EXIV2_TEST_VERSION(0, 28, 0) + } catch (Exiv2::Error & err) { +#else // 0.27.x + } catch (Exiv2::AnyError & err) { +#endif // EXIV2_TEST_VERSION(0, 28, 0) + qWarning() << "Error loading key" << key << ":" << err.what(); + } + } +} +#endif // HAVE_EXIV2_VERSION + +bool Exiv2Wrapper::load(const QString &filePath) +{ +#ifdef HAVE_EXIV2_VERSION + QByteArray filePathByteArray = QFile::encodeName(filePath); + try { + m_exivImage.reset(Exiv2::ImageFactory::open(filePathByteArray.constData()).release()); + m_exivImage->readMetadata(); + } catch (const Exiv2::Error& error) { + m_errMsg = QString::fromUtf8(error.what()); + return false; + } + return true; +#else // HAVE_EXIV2_VERSION + Q_UNUSED(filePath); + return false; +#endif // HAVE_EXIV2_VERSION +} + +void Exiv2Wrapper::cacheSections() +{ +#ifdef HAVE_EXIV2_VERSION + if (m_exivImage->checkMode(Exiv2::mdExif) & Exiv2::amRead) { + cacheSection(m_exivImage->exifData()); + } + + if (m_exivImage->checkMode(Exiv2::mdIptc) & Exiv2::amRead) { + cacheSection(m_exivImage->iptcData()); + } + + if (m_exivImage->checkMode(Exiv2::mdXmp) & Exiv2::amRead) { + cacheSection(m_exivImage->xmpData()); + } + +// qDebug() << m_metadataValue; +// qDebug() << m_metadataLabel; +#endif // HAVE_EXIV2_VERSION +} + +QString Exiv2Wrapper::comment() const +{ +#ifdef HAVE_EXIV2_VERSION + return m_exivImage->comment().c_str(); +#else // HAVE_EXIV2_VERSION + return QString(); +#endif // HAVE_EXIV2_VERSION +} + +QString Exiv2Wrapper::label(const QString &key) const +{ + return m_metadataLabel.value(key); +} + +QString Exiv2Wrapper::value(const QString &key) const +{ + return m_metadataValue.value(key); +} + +QString Exiv2Wrapper::XmpValue(const QString &rawValue) +{ + QString ignored; + return Exiv2Wrapper::XmpValue(rawValue, ignored); +} + +QString Exiv2Wrapper::XmpValue(const QString &rawValue, QString &language) +{ + if (rawValue.size() > 6 && rawValue.startsWith(QLatin1String("lang=\""))) { + int pos = rawValue.indexOf('"', 6); + + if (pos != -1) { + language = rawValue.mid(6, pos - 6); + return (rawValue.mid(pos + 2)); + } + } + + language.clear(); + return rawValue; +} + diff --git a/app/exiv2wrapper.h b/app/exiv2wrapper.h index 4360472..15239d2 100644 --- a/app/exiv2wrapper.h +++ b/app/exiv2wrapper.h @@ -1,43 +1,43 @@ -// SPDX-FileCopyrightText: 2022 Gary Wang -// -// SPDX-License-Identifier: MIT - -#ifndef EXIV2WRAPPER_H -#define EXIV2WRAPPER_H - -#include - -#include -#include - -namespace Exiv2 { -class Image; -} - -class Exiv2Wrapper -{ -public: - Exiv2Wrapper(); - ~Exiv2Wrapper(); - - bool load(const QString& filePath); - void cacheSections(); - - QString comment() const; - QString label(const QString & key) const; - QString value(const QString & key) const; - - static QString XmpValue(const QString &rawValue); - static QString XmpValue(const QString &rawValue, QString & language); - -private: - std::unique_ptr m_exivImage; - QMap m_metadataValue; - QMap m_metadataLabel; - QString m_errMsg; - - template - void cacheSection(Collection collection); -}; - -#endif // EXIV2WRAPPER_H +// SPDX-FileCopyrightText: 2022 Gary Wang +// +// SPDX-License-Identifier: MIT + +#ifndef EXIV2WRAPPER_H +#define EXIV2WRAPPER_H + +#include + +#include +#include + +namespace Exiv2 { +class Image; +} + +class Exiv2Wrapper +{ +public: + Exiv2Wrapper(); + ~Exiv2Wrapper(); + + bool load(const QString& filePath); + void cacheSections(); + + QString comment() const; + QString label(const QString & key) const; + QString value(const QString & key) const; + + static QString XmpValue(const QString &rawValue); + static QString XmpValue(const QString &rawValue, QString & language); + +private: + std::unique_ptr m_exivImage; + QMap m_metadataValue; + QMap m_metadataLabel; + QString m_errMsg; + + template + void cacheSection(Collection collection); +}; + +#endif // EXIV2WRAPPER_H diff --git a/app/framelesswindow.cpp b/app/framelesswindow.cpp index 4183713..5947ff5 100644 --- a/app/framelesswindow.cpp +++ b/app/framelesswindow.cpp @@ -1,136 +1,136 @@ -// SPDX-FileCopyrightText: 2022 Gary Wang -// SPDX-FileCopyrightText: 2023 Tad Young -// -// SPDX-License-Identifier: MIT - -#include "framelesswindow.h" - -#include -#include -#include -#include -#include - -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(o); - if (wg != nullptr) - return mouseHover(static_cast(e), wg); - - break; - } - case QEvent::MouseButtonPress: - return mousePress(static_cast(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; -} - +// SPDX-FileCopyrightText: 2022 Gary Wang +// SPDX-FileCopyrightText: 2023 Tad Young +// +// SPDX-License-Identifier: MIT + +#include "framelesswindow.h" + +#include +#include +#include +#include +#include + +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(o); + if (wg != nullptr) + return mouseHover(static_cast(e), wg); + + break; + } + case QEvent::MouseButtonPress: + return mousePress(static_cast(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; +} + diff --git a/app/framelesswindow.h b/app/framelesswindow.h index 95d9c2e..2be9618 100644 --- a/app/framelesswindow.h +++ b/app/framelesswindow.h @@ -1,39 +1,39 @@ -// SPDX-FileCopyrightText: 2022 Gary Wang -// -// SPDX-License-Identifier: MIT - -#ifndef FRAMELESSWINDOW_H -#define FRAMELESSWINDOW_H - -#include - -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 +// SPDX-FileCopyrightText: 2022 Gary Wang +// +// SPDX-License-Identifier: MIT + +#ifndef FRAMELESSWINDOW_H +#define FRAMELESSWINDOW_H + +#include + +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 diff --git a/app/graphicsview.cpp b/app/graphicsview.cpp index a36d4d7..d61bb95 100644 --- a/app/graphicsview.cpp +++ b/app/graphicsview.cpp @@ -1,371 +1,371 @@ -// SPDX-FileCopyrightText: 2022 Gary Wang -// -// SPDX-License-Identifier: MIT - -#include "graphicsview.h" - -#include "graphicsscene.h" -#include "settings.h" - -#include -#include -#include -#include -#include -#include - -GraphicsView::GraphicsView(QWidget *parent) - : QGraphicsView (parent) -{ - setDragMode(QGraphicsView::ScrollHandDrag); - setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - setResizeAnchor(QGraphicsView::AnchorUnderMouse); - setTransformationAnchor(QGraphicsView::AnchorUnderMouse); - setStyleSheet("background-color: rgba(0, 0, 0, 220);" - "border-radius: 3px;"); - setAcceptDrops(false); - setCheckerboardEnabled(false); - - connect(horizontalScrollBar(), &QScrollBar::valueChanged, this, &GraphicsView::viewportRectChanged); - connect(verticalScrollBar(), &QScrollBar::valueChanged, this, &GraphicsView::viewportRectChanged); -} - -void GraphicsView::showFileFromPath(const QString &filePath) -{ - emit navigatorViewRequired(false, transform()); - - if (filePath.endsWith(".svg")) { - showSvg(filePath); - } else { - QImageReader imageReader(filePath); - imageReader.setAutoTransform(true); - imageReader.setDecideFormatFromContent(true); - imageReader.setAllocationLimit(0); - - // Since if the image format / plugin does not support this feature, imageFormat() will returns an invalid format. - // So we cannot use imageFormat() and check if it returns QImage::Format_Invalid to detect if we support the file. - // QImage::Format imageFormat = imageReader.imageFormat(); - if (imageReader.format().isEmpty()) { - showText(tr("File is not a valid image")); - } else if (imageReader.supportsAnimation() && imageReader.imageCount() > 1) { - showAnimated(filePath); - } else if (!imageReader.canRead()) { - showText(tr("Image data is invalid or currently unsupported")); - } else { - QPixmap && pixmap = QPixmap::fromImageReader(&imageReader); - if (pixmap.isNull()) { - showText(tr("Image data is invalid or currently unsupported")); - } else { - pixmap.setDevicePixelRatio(devicePixelRatioF()); - showImage(pixmap); - } - } - } -} - -void GraphicsView::showImage(const QPixmap &pixmap) -{ - resetTransform(); - scene()->showImage(pixmap); - displayScene(); -} - -void GraphicsView::showImage(const QImage &image) -{ - resetTransform(); - scene()->showImage(QPixmap::fromImage(image)); - displayScene(); -} - -void GraphicsView::showText(const QString &text) -{ - resetTransform(); - scene()->showText(text); - displayScene(); -} - -void GraphicsView::showSvg(const QString &filepath) -{ - resetTransform(); - scene()->showSvg(filepath); - displayScene(); -} - -void GraphicsView::showAnimated(const QString &filepath) -{ - resetTransform(); - scene()->showAnimated(filepath); - displayScene(); -} - -GraphicsScene *GraphicsView::scene() const -{ - return qobject_cast(QGraphicsView::scene()); -} - -void GraphicsView::setScene(GraphicsScene *scene) -{ - return QGraphicsView::setScene(scene); -} - -qreal GraphicsView::scaleFactor() const -{ - return QStyleOptionGraphicsItem::levelOfDetailFromTransform(transform()); -} - -void GraphicsView::resetTransform() -{ - if (!shouldAvoidTransform()) { - QGraphicsView::resetTransform(); - } -} - -void GraphicsView::zoomView(qreal scaleFactor) -{ - m_enableFitInView = false; - scale(scaleFactor, scaleFactor); - applyTransformationModeByScaleFactor(); - emit navigatorViewRequired(!isThingSmallerThanWindowWith(transform()), transform()); -} - -// This is always according to user's view. -// the direction of the rotation will NOT be clockwise because the y-axis points downwards. -void GraphicsView::rotateView(bool clockwise) -{ - resetScale(); - - QTransform tf(0, clockwise ? 1 : -1, 0, - clockwise ? -1 : 1, 0, 0, - 0, 0, 1); - tf = transform() * tf; - setTransform(tf); -} - -void GraphicsView::flipView(bool horizontal) -{ - QTransform tf(horizontal ? -1 : 1, 0, 0, - 0, horizontal ? 1 : -1, 0, - 0, 0, 1); - tf = transform() * tf; - setTransform(tf); - - // Ensure the navigation view is also flipped. - emit navigatorViewRequired(!isThingSmallerThanWindowWith(transform()), transform()); -} - -void GraphicsView::resetScale() -{ - setTransform(resetScale(transform())); - applyTransformationModeByScaleFactor(); - emit navigatorViewRequired(!isThingSmallerThanWindowWith(transform()), transform()); -} - -void GraphicsView::fitInView(const QRectF &rect, Qt::AspectRatioMode aspectRadioMode) -{ - QGraphicsView::fitInView(rect, aspectRadioMode); - applyTransformationModeByScaleFactor(); -} - -void GraphicsView::fitByOrientation(Qt::Orientation ori, bool scaleDownOnly) -{ - resetScale(); - - QRectF viewRect = this->viewport()->rect().adjusted(2, 2, -2, -2); - QRectF imageRect = transform().mapRect(sceneRect()); - - qreal ratio; - - if (ori == Qt::Horizontal) { - ratio = viewRect.width() / imageRect.width(); - } else { - ratio = viewRect.height() / imageRect.height(); - } - - if (scaleDownOnly && ratio > 1) ratio = 1; - - scale(ratio, ratio); - centerOn(imageRect.top(), 0); - m_enableFitInView = false; - - applyTransformationModeByScaleFactor(); - emit navigatorViewRequired(!isThingSmallerThanWindowWith(transform()), transform()); -} - -void GraphicsView::displayScene() -{ - if (shouldAvoidTransform()) { - emit navigatorViewRequired(!isThingSmallerThanWindowWith(transform()), transform()); - return; - } - - if (isSceneBiggerThanView()) { - fitInView(sceneRect(), Qt::KeepAspectRatio); - } - - m_enableFitInView = true; - m_firstUserMediaLoaded = true; -} - -bool GraphicsView::isSceneBiggerThanView() const -{ - if (!isThingSmallerThanWindowWith(transform())) { - return true; - } else { - return false; - } -} - -// Automately do fit in view when viewport(window) smaller than image original size. -void GraphicsView::setEnableAutoFitInView(bool enable) -{ - m_enableFitInView = enable; -} - -bool GraphicsView::avoidResetTransform() const -{ - return m_avoidResetTransform; -} - -void GraphicsView::setAvoidResetTransform(bool avoidReset) -{ - m_avoidResetTransform = avoidReset; -} - -inline double zeroOrOne(double number) -{ - return qFuzzyIsNull(number) ? 0 : (number > 0 ? 1 : -1); -} - -// Note: this only works if we only have 90 degree based rotation -// and no shear/translate. -QTransform GraphicsView::resetScale(const QTransform & orig) -{ - return QTransform(zeroOrOne(orig.m11()), zeroOrOne(orig.m12()), - zeroOrOne(orig.m21()), zeroOrOne(orig.m22()), - orig.dx(), orig.dy()); -} - -void GraphicsView::toggleCheckerboard(bool invertCheckerboardColor) -{ - setCheckerboardEnabled(!m_checkerboardEnabled, invertCheckerboardColor); -} - -void GraphicsView::mousePressEvent(QMouseEvent *event) -{ - if (shouldIgnoreMousePressMoveEvent(event)) { - event->ignore(); - // blumia: return here, or the QMouseEvent event transparency won't - // work if we set a QGraphicsView::ScrollHandDrag drag mode. - return; - } - - return QGraphicsView::mousePressEvent(event); -} - -void GraphicsView::mouseMoveEvent(QMouseEvent *event) -{ - if (shouldIgnoreMousePressMoveEvent(event)) { - event->ignore(); - } - - return QGraphicsView::mouseMoveEvent(event); -} - -void GraphicsView::mouseReleaseEvent(QMouseEvent *event) -{ - if (event->button() == Qt::ForwardButton || event->button() == Qt::BackButton) { - event->ignore(); - } else { - QGraphicsItem *item = itemAt(event->pos()); - if (!item) { - event->ignore(); - } - } - - return QGraphicsView::mouseReleaseEvent(event); -} - -void GraphicsView::wheelEvent(QWheelEvent *event) -{ - event->ignore(); - // blumia: no need for calling parent method. -} - -void GraphicsView::resizeEvent(QResizeEvent *event) -{ - if (m_enableFitInView) { - 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 - // user resize down the window again. - } else if (originalSizeSmallerThanWindow && scaleFactor() < 1) { - resetScale(); - } else { - fitInView(sceneRect(), Qt::KeepAspectRatio); - } - } else { - emit navigatorViewRequired(!isThingSmallerThanWindowWith(transform()), transform()); - } - return QGraphicsView::resizeEvent(event); -} - -bool GraphicsView::isThingSmallerThanWindowWith(const QTransform &transform) const -{ - return rect().size().expandedTo(transform.mapRect(sceneRect()).size().toSize()) - == rect().size(); -} - -bool GraphicsView::shouldIgnoreMousePressMoveEvent(const QMouseEvent *event) const -{ - if (event->buttons() == Qt::NoButton) { - return true; - } - - QGraphicsItem *item = itemAt(event->pos()); - if (!item) { - return true; - } - - if (isThingSmallerThanWindowWith(transform())) { - return true; - } - - return false; -} - -void GraphicsView::setCheckerboardEnabled(bool enabled, bool invertColor) -{ - m_checkerboardEnabled = enabled; - bool isLightCheckerboard = Settings::instance()->useLightCheckerboard() ^ invertColor; - if (m_checkerboardEnabled) { - // Prepare background check-board pattern - QPixmap tilePixmap(0x20, 0x20); - tilePixmap.fill(isLightCheckerboard ? QColor(220, 220, 220, 170) : QColor(35, 35, 35, 170)); - QPainter tilePainter(&tilePixmap); - constexpr QColor color(45, 45, 45, 170); - constexpr QColor invertedColor(210, 210, 210, 170); - tilePainter.fillRect(0, 0, 0x10, 0x10, isLightCheckerboard ? invertedColor : color); - tilePainter.fillRect(0x10, 0x10, 0x10, 0x10, isLightCheckerboard ? invertedColor : color); - tilePainter.end(); - - setBackgroundBrush(tilePixmap); - } else { - setBackgroundBrush(Qt::transparent); - } -} - -void GraphicsView::applyTransformationModeByScaleFactor() -{ - if (this->scaleFactor() < 1) { - scene()->trySetTransformationMode(Qt::SmoothTransformation, this->scaleFactor()); - } else { - scene()->trySetTransformationMode(Qt::FastTransformation, this->scaleFactor()); - } -} - -bool GraphicsView::shouldAvoidTransform() const -{ - return m_firstUserMediaLoaded && m_avoidResetTransform; -} +// SPDX-FileCopyrightText: 2022 Gary Wang +// +// SPDX-License-Identifier: MIT + +#include "graphicsview.h" + +#include "graphicsscene.h" +#include "settings.h" + +#include +#include +#include +#include +#include +#include + +GraphicsView::GraphicsView(QWidget *parent) + : QGraphicsView (parent) +{ + setDragMode(QGraphicsView::ScrollHandDrag); + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setResizeAnchor(QGraphicsView::AnchorUnderMouse); + setTransformationAnchor(QGraphicsView::AnchorUnderMouse); + setStyleSheet("background-color: rgba(0, 0, 0, 220);" + "border-radius: 3px;"); + setAcceptDrops(false); + setCheckerboardEnabled(false); + + connect(horizontalScrollBar(), &QScrollBar::valueChanged, this, &GraphicsView::viewportRectChanged); + connect(verticalScrollBar(), &QScrollBar::valueChanged, this, &GraphicsView::viewportRectChanged); +} + +void GraphicsView::showFileFromPath(const QString &filePath) +{ + emit navigatorViewRequired(false, transform()); + + if (filePath.endsWith(".svg")) { + showSvg(filePath); + } else { + QImageReader imageReader(filePath); + imageReader.setAutoTransform(true); + imageReader.setDecideFormatFromContent(true); + imageReader.setAllocationLimit(0); + + // Since if the image format / plugin does not support this feature, imageFormat() will returns an invalid format. + // So we cannot use imageFormat() and check if it returns QImage::Format_Invalid to detect if we support the file. + // QImage::Format imageFormat = imageReader.imageFormat(); + if (imageReader.format().isEmpty()) { + showText(tr("File is not a valid image")); + } else if (imageReader.supportsAnimation() && imageReader.imageCount() > 1) { + showAnimated(filePath); + } else if (!imageReader.canRead()) { + showText(tr("Image data is invalid or currently unsupported")); + } else { + QPixmap && pixmap = QPixmap::fromImageReader(&imageReader); + if (pixmap.isNull()) { + showText(tr("Image data is invalid or currently unsupported")); + } else { + pixmap.setDevicePixelRatio(devicePixelRatioF()); + showImage(pixmap); + } + } + } +} + +void GraphicsView::showImage(const QPixmap &pixmap) +{ + resetTransform(); + scene()->showImage(pixmap); + displayScene(); +} + +void GraphicsView::showImage(const QImage &image) +{ + resetTransform(); + scene()->showImage(QPixmap::fromImage(image)); + displayScene(); +} + +void GraphicsView::showText(const QString &text) +{ + resetTransform(); + scene()->showText(text); + displayScene(); +} + +void GraphicsView::showSvg(const QString &filepath) +{ + resetTransform(); + scene()->showSvg(filepath); + displayScene(); +} + +void GraphicsView::showAnimated(const QString &filepath) +{ + resetTransform(); + scene()->showAnimated(filepath); + displayScene(); +} + +GraphicsScene *GraphicsView::scene() const +{ + return qobject_cast(QGraphicsView::scene()); +} + +void GraphicsView::setScene(GraphicsScene *scene) +{ + return QGraphicsView::setScene(scene); +} + +qreal GraphicsView::scaleFactor() const +{ + return QStyleOptionGraphicsItem::levelOfDetailFromTransform(transform()); +} + +void GraphicsView::resetTransform() +{ + if (!shouldAvoidTransform()) { + QGraphicsView::resetTransform(); + } +} + +void GraphicsView::zoomView(qreal scaleFactor) +{ + m_enableFitInView = false; + scale(scaleFactor, scaleFactor); + applyTransformationModeByScaleFactor(); + emit navigatorViewRequired(!isThingSmallerThanWindowWith(transform()), transform()); +} + +// This is always according to user's view. +// the direction of the rotation will NOT be clockwise because the y-axis points downwards. +void GraphicsView::rotateView(bool clockwise) +{ + resetScale(); + + QTransform tf(0, clockwise ? 1 : -1, 0, + clockwise ? -1 : 1, 0, 0, + 0, 0, 1); + tf = transform() * tf; + setTransform(tf); +} + +void GraphicsView::flipView(bool horizontal) +{ + QTransform tf(horizontal ? -1 : 1, 0, 0, + 0, horizontal ? 1 : -1, 0, + 0, 0, 1); + tf = transform() * tf; + setTransform(tf); + + // Ensure the navigation view is also flipped. + emit navigatorViewRequired(!isThingSmallerThanWindowWith(transform()), transform()); +} + +void GraphicsView::resetScale() +{ + setTransform(resetScale(transform())); + applyTransformationModeByScaleFactor(); + emit navigatorViewRequired(!isThingSmallerThanWindowWith(transform()), transform()); +} + +void GraphicsView::fitInView(const QRectF &rect, Qt::AspectRatioMode aspectRadioMode) +{ + QGraphicsView::fitInView(rect, aspectRadioMode); + applyTransformationModeByScaleFactor(); +} + +void GraphicsView::fitByOrientation(Qt::Orientation ori, bool scaleDownOnly) +{ + resetScale(); + + QRectF viewRect = this->viewport()->rect().adjusted(2, 2, -2, -2); + QRectF imageRect = transform().mapRect(sceneRect()); + + qreal ratio; + + if (ori == Qt::Horizontal) { + ratio = viewRect.width() / imageRect.width(); + } else { + ratio = viewRect.height() / imageRect.height(); + } + + if (scaleDownOnly && ratio > 1) ratio = 1; + + scale(ratio, ratio); + centerOn(imageRect.top(), 0); + m_enableFitInView = false; + + applyTransformationModeByScaleFactor(); + emit navigatorViewRequired(!isThingSmallerThanWindowWith(transform()), transform()); +} + +void GraphicsView::displayScene() +{ + if (shouldAvoidTransform()) { + emit navigatorViewRequired(!isThingSmallerThanWindowWith(transform()), transform()); + return; + } + + if (isSceneBiggerThanView()) { + fitInView(sceneRect(), Qt::KeepAspectRatio); + } + + m_enableFitInView = true; + m_firstUserMediaLoaded = true; +} + +bool GraphicsView::isSceneBiggerThanView() const +{ + if (!isThingSmallerThanWindowWith(transform())) { + return true; + } else { + return false; + } +} + +// Automately do fit in view when viewport(window) smaller than image original size. +void GraphicsView::setEnableAutoFitInView(bool enable) +{ + m_enableFitInView = enable; +} + +bool GraphicsView::avoidResetTransform() const +{ + return m_avoidResetTransform; +} + +void GraphicsView::setAvoidResetTransform(bool avoidReset) +{ + m_avoidResetTransform = avoidReset; +} + +inline double zeroOrOne(double number) +{ + return qFuzzyIsNull(number) ? 0 : (number > 0 ? 1 : -1); +} + +// Note: this only works if we only have 90 degree based rotation +// and no shear/translate. +QTransform GraphicsView::resetScale(const QTransform & orig) +{ + return QTransform(zeroOrOne(orig.m11()), zeroOrOne(orig.m12()), + zeroOrOne(orig.m21()), zeroOrOne(orig.m22()), + orig.dx(), orig.dy()); +} + +void GraphicsView::toggleCheckerboard(bool invertCheckerboardColor) +{ + setCheckerboardEnabled(!m_checkerboardEnabled, invertCheckerboardColor); +} + +void GraphicsView::mousePressEvent(QMouseEvent *event) +{ + if (shouldIgnoreMousePressMoveEvent(event)) { + event->ignore(); + // blumia: return here, or the QMouseEvent event transparency won't + // work if we set a QGraphicsView::ScrollHandDrag drag mode. + return; + } + + return QGraphicsView::mousePressEvent(event); +} + +void GraphicsView::mouseMoveEvent(QMouseEvent *event) +{ + if (shouldIgnoreMousePressMoveEvent(event)) { + event->ignore(); + } + + return QGraphicsView::mouseMoveEvent(event); +} + +void GraphicsView::mouseReleaseEvent(QMouseEvent *event) +{ + if (event->button() == Qt::ForwardButton || event->button() == Qt::BackButton) { + event->ignore(); + } else { + QGraphicsItem *item = itemAt(event->pos()); + if (!item) { + event->ignore(); + } + } + + return QGraphicsView::mouseReleaseEvent(event); +} + +void GraphicsView::wheelEvent(QWheelEvent *event) +{ + event->ignore(); + // blumia: no need for calling parent method. +} + +void GraphicsView::resizeEvent(QResizeEvent *event) +{ + if (m_enableFitInView) { + 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 + // user resize down the window again. + } else if (originalSizeSmallerThanWindow && scaleFactor() < 1) { + resetScale(); + } else { + fitInView(sceneRect(), Qt::KeepAspectRatio); + } + } else { + emit navigatorViewRequired(!isThingSmallerThanWindowWith(transform()), transform()); + } + return QGraphicsView::resizeEvent(event); +} + +bool GraphicsView::isThingSmallerThanWindowWith(const QTransform &transform) const +{ + return rect().size().expandedTo(transform.mapRect(sceneRect()).size().toSize()) + == rect().size(); +} + +bool GraphicsView::shouldIgnoreMousePressMoveEvent(const QMouseEvent *event) const +{ + if (event->buttons() == Qt::NoButton) { + return true; + } + + QGraphicsItem *item = itemAt(event->pos()); + if (!item) { + return true; + } + + if (isThingSmallerThanWindowWith(transform())) { + return true; + } + + return false; +} + +void GraphicsView::setCheckerboardEnabled(bool enabled, bool invertColor) +{ + m_checkerboardEnabled = enabled; + bool isLightCheckerboard = Settings::instance()->useLightCheckerboard() ^ invertColor; + if (m_checkerboardEnabled) { + // Prepare background check-board pattern + QPixmap tilePixmap(0x20, 0x20); + tilePixmap.fill(isLightCheckerboard ? QColor(220, 220, 220, 170) : QColor(35, 35, 35, 170)); + QPainter tilePainter(&tilePixmap); + constexpr QColor color(45, 45, 45, 170); + constexpr QColor invertedColor(210, 210, 210, 170); + tilePainter.fillRect(0, 0, 0x10, 0x10, isLightCheckerboard ? invertedColor : color); + tilePainter.fillRect(0x10, 0x10, 0x10, 0x10, isLightCheckerboard ? invertedColor : color); + tilePainter.end(); + + setBackgroundBrush(tilePixmap); + } else { + setBackgroundBrush(Qt::transparent); + } +} + +void GraphicsView::applyTransformationModeByScaleFactor() +{ + if (this->scaleFactor() < 1) { + scene()->trySetTransformationMode(Qt::SmoothTransformation, this->scaleFactor()); + } else { + scene()->trySetTransformationMode(Qt::FastTransformation, this->scaleFactor()); + } +} + +bool GraphicsView::shouldAvoidTransform() const +{ + return m_firstUserMediaLoaded && m_avoidResetTransform; +} diff --git a/app/graphicsview.h b/app/graphicsview.h index c03f1a2..f20d0ec 100644 --- a/app/graphicsview.h +++ b/app/graphicsview.h @@ -1,79 +1,79 @@ -// SPDX-FileCopyrightText: 2022 Gary Wang -// -// SPDX-License-Identifier: MIT - -#ifndef GRAPHICSVIEW_H -#define GRAPHICSVIEW_H - -#include -#include - -class GraphicsScene; -class GraphicsView : public QGraphicsView -{ - Q_OBJECT -public: - GraphicsView(QWidget *parent = nullptr); - - void showFileFromPath(const QString &filePath); - - void showImage(const QPixmap &pixmap); - void showImage(const QImage &image); - void showText(const QString &text); - void showSvg(const QString &filepath); - void showAnimated(const QString &filepath); - - GraphicsScene * scene() const; - void setScene(GraphicsScene *scene); - - qreal scaleFactor() const; - - void resetTransform(); - void zoomView(qreal scaleFactor); - void rotateView(bool clockwise = true); - void flipView(bool horizontal = true); - void resetScale(); - void fitInView(const QRectF &rect, Qt::AspectRatioMode aspectRadioMode = Qt::IgnoreAspectRatio); - void fitByOrientation(Qt::Orientation ori = Qt::Horizontal, bool scaleDownOnly = false); - - void displayScene(); - bool isSceneBiggerThanView() const; - void setEnableAutoFitInView(bool enable = true); - - bool avoidResetTransform() const; - void setAvoidResetTransform(bool avoidReset); - - static QTransform resetScale(const QTransform & orig); - -signals: - void navigatorViewRequired(bool required, QTransform transform); - void viewportRectChanged(); - -public slots: - void toggleCheckerboard(bool invertCheckerboardColor = false); - -private: - void mousePressEvent(QMouseEvent * event) override; - void mouseMoveEvent(QMouseEvent * event) override; - void mouseReleaseEvent(QMouseEvent * event) override; - void wheelEvent(QWheelEvent *event) override; - void resizeEvent(QResizeEvent *event) override; - - bool isThingSmallerThanWindowWith(const QTransform &transform) const; - bool shouldIgnoreMousePressMoveEvent(const QMouseEvent *event) const; - void setCheckerboardEnabled(bool enabled, bool invertColor = false); - void applyTransformationModeByScaleFactor(); - - inline bool shouldAvoidTransform() const; - - // 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_avoidResetTransform = false; - bool m_checkerboardEnabled = false; - bool m_useLightCheckerboard = false; - bool m_firstUserMediaLoaded = false; -}; - -#endif // GRAPHICSVIEW_H +// SPDX-FileCopyrightText: 2022 Gary Wang +// +// SPDX-License-Identifier: MIT + +#ifndef GRAPHICSVIEW_H +#define GRAPHICSVIEW_H + +#include +#include + +class GraphicsScene; +class GraphicsView : public QGraphicsView +{ + Q_OBJECT +public: + GraphicsView(QWidget *parent = nullptr); + + void showFileFromPath(const QString &filePath); + + void showImage(const QPixmap &pixmap); + void showImage(const QImage &image); + void showText(const QString &text); + void showSvg(const QString &filepath); + void showAnimated(const QString &filepath); + + GraphicsScene * scene() const; + void setScene(GraphicsScene *scene); + + qreal scaleFactor() const; + + void resetTransform(); + void zoomView(qreal scaleFactor); + void rotateView(bool clockwise = true); + void flipView(bool horizontal = true); + void resetScale(); + void fitInView(const QRectF &rect, Qt::AspectRatioMode aspectRadioMode = Qt::IgnoreAspectRatio); + void fitByOrientation(Qt::Orientation ori = Qt::Horizontal, bool scaleDownOnly = false); + + void displayScene(); + bool isSceneBiggerThanView() const; + void setEnableAutoFitInView(bool enable = true); + + bool avoidResetTransform() const; + void setAvoidResetTransform(bool avoidReset); + + static QTransform resetScale(const QTransform & orig); + +signals: + void navigatorViewRequired(bool required, QTransform transform); + void viewportRectChanged(); + +public slots: + void toggleCheckerboard(bool invertCheckerboardColor = false); + +private: + void mousePressEvent(QMouseEvent * event) override; + void mouseMoveEvent(QMouseEvent * event) override; + void mouseReleaseEvent(QMouseEvent * event) override; + void wheelEvent(QWheelEvent *event) override; + void resizeEvent(QResizeEvent *event) override; + + bool isThingSmallerThanWindowWith(const QTransform &transform) const; + bool shouldIgnoreMousePressMoveEvent(const QMouseEvent *event) const; + void setCheckerboardEnabled(bool enabled, bool invertColor = false); + void applyTransformationModeByScaleFactor(); + + inline bool shouldAvoidTransform() const; + + // 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_avoidResetTransform = false; + bool m_checkerboardEnabled = false; + bool m_useLightCheckerboard = false; + bool m_firstUserMediaLoaded = false; +}; + +#endif // GRAPHICSVIEW_H diff --git a/app/main.cpp b/app/main.cpp index d7a50a5..c10947d 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -1,103 +1,103 @@ -// SPDX-FileCopyrightText: 2022 Gary Wang -// -// SPDX-License-Identifier: MIT - -#include "mainwindow.h" - -#include "playlistmanager.h" -#include "settings.h" - -#ifdef Q_OS_MACOS -#include "fileopeneventhandler.h" -#endif // Q_OS_MACOS - -#include -#include -#include -#include -#include - -using namespace Qt::Literals::StringLiterals; - -int main(int argc, char *argv[]) -{ - QCoreApplication::setApplicationName(u"Pineapple Pictures"_s); - QCoreApplication::setApplicationVersion(PPIC_VERSION_STRING); - QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Settings::instance()->hiDpiScaleFactorBehavior()); - - QApplication a(argc, argv); - - QTranslator translator; -#if defined(TRANSLATION_RESOURCE_EMBEDDING) - const QString qmDir = u":/i18n/"_s; -#elif defined(QM_FILE_INSTALL_ABSOLUTE_DIR) - const QString qmDir = QT_STRINGIFY(QM_FILE_INSTALL_ABSOLUTE_DIR); -#else - const QString qmDir = QDir(QCoreApplication::applicationDirPath()).absoluteFilePath("translations"); -#endif - if (translator.load(QLocale(), u"PineapplePictures"_s, u"_"_s, qmDir)) { - QCoreApplication::installTranslator(&translator); - } - - QGuiApplication::setApplicationDisplayName(QCoreApplication::translate("main", "Pineapple Pictures")); - - // commandline options - QCommandLineOption supportedImageFormats(u"supported-image-formats"_s, QCoreApplication::translate("main", "List supported image format suffixes, and quit program.")); - // parse commandline arguments - QCommandLineParser parser; - parser.addOption(supportedImageFormats); - parser.addPositionalArgument("File list", QCoreApplication::translate("main", "File list.")); - parser.addHelpOption(); - parser.process(a); - - if (parser.isSet(supportedImageFormats)) { -#if QT_VERSION < QT_VERSION_CHECK(6, 9, 0) - fputs(qPrintable(MainWindow::supportedImageFormats().join(QChar('\n'))), stdout); - ::exit(EXIT_SUCCESS); -#else - QCommandLineParser::showMessageAndExit(QCommandLineParser::MessageType::Information, - MainWindow::supportedImageFormats().join(QChar('\n'))); -#endif - } - - MainWindow w; - w.show(); - -#ifdef Q_OS_MACOS - FileOpenEventHandler * fileOpenEventHandler = new FileOpenEventHandler(&a); - 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.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.showNormal(); - w.raise(); - w.activateWindow(); - } - }); -#endif // Q_OS_MACOS - - QStringList urlStrList = parser.positionalArguments(); - QList && urlList = PlaylistManager::convertToUrlList(urlStrList); - - if (!urlList.isEmpty()) { - w.showUrls(urlList); - } - - w.initWindowSize(); - - return QApplication::exec(); -} +// SPDX-FileCopyrightText: 2022 Gary Wang +// +// SPDX-License-Identifier: MIT + +#include "mainwindow.h" + +#include "playlistmanager.h" +#include "settings.h" + +#ifdef Q_OS_MACOS +#include "fileopeneventhandler.h" +#endif // Q_OS_MACOS + +#include +#include +#include +#include +#include + +using namespace Qt::Literals::StringLiterals; + +int main(int argc, char *argv[]) +{ + QCoreApplication::setApplicationName(u"Pineapple Pictures"_s); + QCoreApplication::setApplicationVersion(PPIC_VERSION_STRING); + QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Settings::instance()->hiDpiScaleFactorBehavior()); + + QApplication a(argc, argv); + + QTranslator translator; +#if defined(TRANSLATION_RESOURCE_EMBEDDING) + const QString qmDir = u":/i18n/"_s; +#elif defined(QM_FILE_INSTALL_ABSOLUTE_DIR) + const QString qmDir = QT_STRINGIFY(QM_FILE_INSTALL_ABSOLUTE_DIR); +#else + const QString qmDir = QDir(QCoreApplication::applicationDirPath()).absoluteFilePath("translations"); +#endif + if (translator.load(QLocale(), u"PineapplePictures"_s, u"_"_s, qmDir)) { + QCoreApplication::installTranslator(&translator); + } + + QGuiApplication::setApplicationDisplayName(QCoreApplication::translate("main", "Pineapple Pictures")); + + // commandline options + QCommandLineOption supportedImageFormats(u"supported-image-formats"_s, QCoreApplication::translate("main", "List supported image format suffixes, and quit program.")); + // parse commandline arguments + QCommandLineParser parser; + parser.addOption(supportedImageFormats); + parser.addPositionalArgument("File list", QCoreApplication::translate("main", "File list.")); + parser.addHelpOption(); + parser.process(a); + + if (parser.isSet(supportedImageFormats)) { +#if QT_VERSION < QT_VERSION_CHECK(6, 9, 0) + fputs(qPrintable(MainWindow::supportedImageFormats().join(QChar('\n'))), stdout); + ::exit(EXIT_SUCCESS); +#else + QCommandLineParser::showMessageAndExit(QCommandLineParser::MessageType::Information, + MainWindow::supportedImageFormats().join(QChar('\n'))); +#endif + } + + MainWindow w; + w.show(); + +#ifdef Q_OS_MACOS + FileOpenEventHandler * fileOpenEventHandler = new FileOpenEventHandler(&a); + 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.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.showNormal(); + w.raise(); + w.activateWindow(); + } + }); +#endif // Q_OS_MACOS + + QStringList urlStrList = parser.positionalArguments(); + QList && urlList = PlaylistManager::convertToUrlList(urlStrList); + + if (!urlList.isEmpty()) { + w.showUrls(urlList); + } + + w.initWindowSize(); + + return QApplication::exec(); +} diff --git a/app/mainwindow.cpp b/app/mainwindow.cpp index a1523d4..da365d8 100644 --- a/app/mainwindow.cpp +++ b/app/mainwindow.cpp @@ -313,7 +313,7 @@ QStringList MainWindow::supportedImageFormats() void MainWindow::showEvent(QShowEvent *event) { updateWidgetsPosition(); - + return FramelessWindow::showEvent(event); } diff --git a/app/mainwindow.h b/app/mainwindow.h index 7de8122..de19e8f 100644 --- a/app/mainwindow.h +++ b/app/mainwindow.h @@ -1,134 +1,134 @@ -// SPDX-FileCopyrightText: 2025 Gary Wang -// -// SPDX-License-Identifier: MIT - -#ifndef MAINWINDOW_H -#define MAINWINDOW_H - -#include "framelesswindow.h" - -#include -#include -#include - -QT_BEGIN_NAMESPACE -class QGraphicsOpacityEffect; -class QGraphicsView; -class QFileSystemWatcher; -QT_END_NAMESPACE - -class ActionManager; -class PlaylistManager; -class ToolButton; -class GraphicsView; -class NavigatorView; -class BottomButtonGroup; -class MainWindow : public FramelessWindow -{ - Q_OBJECT - -public: - explicit MainWindow(QWidget *parent = nullptr); - ~MainWindow() override; - - void showUrls(const QList &urls); - void initWindowSize(); - void adjustWindowSizeBySceneRect(); - QUrl currentImageFileUrl() const; - - void clearGallery(); - void galleryPrev(); - void galleryNext(); - void galleryCurrent(bool showLoadImageHintWhenEmpty, bool reloadImage); - - 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(); - -protected: - QSize sizeHint() const override; - -private slots: - void on_actionOpen_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_actionPrevPicture_triggered(); - void on_actionNextPicture_triggered(); - - void on_actionTogglePauseAnimation_triggered(); - void on_actionAnimationNextFrame_triggered(); - - void on_actionHorizontalFlip_triggered(); - void on_actionFitInView_triggered(); - void on_actionFitByWidth_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_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(); - -private: - 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 +// SPDX-FileCopyrightText: 2025 Gary Wang +// +// SPDX-License-Identifier: MIT + +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include "framelesswindow.h" + +#include +#include +#include + +QT_BEGIN_NAMESPACE +class QGraphicsOpacityEffect; +class QGraphicsView; +class QFileSystemWatcher; +QT_END_NAMESPACE + +class ActionManager; +class PlaylistManager; +class ToolButton; +class GraphicsView; +class NavigatorView; +class BottomButtonGroup; +class MainWindow : public FramelessWindow +{ + Q_OBJECT + +public: + explicit MainWindow(QWidget *parent = nullptr); + ~MainWindow() override; + + void showUrls(const QList &urls); + void initWindowSize(); + void adjustWindowSizeBySceneRect(); + QUrl currentImageFileUrl() const; + + void clearGallery(); + void galleryPrev(); + void galleryNext(); + void galleryCurrent(bool showLoadImageHintWhenEmpty, bool reloadImage); + + 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(); + +protected: + QSize sizeHint() const override; + +private slots: + void on_actionOpen_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_actionPrevPicture_triggered(); + void on_actionNextPicture_triggered(); + + void on_actionTogglePauseAnimation_triggered(); + void on_actionAnimationNextFrame_triggered(); + + void on_actionHorizontalFlip_triggered(); + void on_actionFitInView_triggered(); + void on_actionFitByWidth_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_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(); + +private: + 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 diff --git a/app/metadatadialog.cpp b/app/metadatadialog.cpp index ee84f07..ebd065f 100644 --- a/app/metadatadialog.cpp +++ b/app/metadatadialog.cpp @@ -1,110 +1,110 @@ -// SPDX-FileCopyrightText: 2022 Gary Wang -// -// SPDX-License-Identifier: MIT - -#include "metadatadialog.h" - -#include -#include -#include -#include -#include -#include - -#include "metadatamodel.h" - -class PropertyTreeView : public QTreeView -{ -public: - explicit PropertyTreeView(QWidget* parent) : QTreeView(parent) {} - ~PropertyTreeView() {} - -protected: - void rowsInserted(const QModelIndex& parent, int start, int end) override - { - QTreeView::rowsInserted(parent, start, end); - if (!parent.isValid()) { - // we are inserting a section group - for (int row = start; row <= end; ++row) { - setupSection(row); - } - } else { - // we are inserting a property - setRowHidden(parent.row(), QModelIndex(), false); - } - } - - void reset() override - { - QTreeView::reset(); - if (model()) { - for (int row = 0; row < model()->rowCount(); ++row) { - setupSection(row); - } - } - } - -private: - void setupSection(int row) - { - expand(model()->index(row, 0)); - setFirstColumnSpanned(row, QModelIndex(), true); - setRowHidden(row, QModelIndex(), !model()->hasChildren(model()->index(row, 0))); - } -}; - -class PropertyTreeItemDelegate : public QStyledItemDelegate -{ -public: - PropertyTreeItemDelegate(QObject* parent) - : QStyledItemDelegate(parent) - {} - -protected: - void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override - { - QStyleOptionViewItem opt = option; - if (!index.parent().isValid()) { - opt.font.setBold(true); - opt.features.setFlag(QStyleOptionViewItem::Alternate); - } - QStyledItemDelegate::paint(painter, opt, index); - } -}; - -MetadataDialog::MetadataDialog(QWidget *parent) - : QDialog(parent) - , m_treeView(new PropertyTreeView(this)) -{ - 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); -} - -MetadataDialog::~MetadataDialog() -{ - -} - -void MetadataDialog::setMetadataModel(MetadataModel * model) -{ - m_treeView->setModel(model); -} - -QSize MetadataDialog::sizeHint() const -{ - return QSize(520, 350); -} +// SPDX-FileCopyrightText: 2022 Gary Wang +// +// SPDX-License-Identifier: MIT + +#include "metadatadialog.h" + +#include +#include +#include +#include +#include +#include + +#include "metadatamodel.h" + +class PropertyTreeView : public QTreeView +{ +public: + explicit PropertyTreeView(QWidget* parent) : QTreeView(parent) {} + ~PropertyTreeView() {} + +protected: + void rowsInserted(const QModelIndex& parent, int start, int end) override + { + QTreeView::rowsInserted(parent, start, end); + if (!parent.isValid()) { + // we are inserting a section group + for (int row = start; row <= end; ++row) { + setupSection(row); + } + } else { + // we are inserting a property + setRowHidden(parent.row(), QModelIndex(), false); + } + } + + void reset() override + { + QTreeView::reset(); + if (model()) { + for (int row = 0; row < model()->rowCount(); ++row) { + setupSection(row); + } + } + } + +private: + void setupSection(int row) + { + expand(model()->index(row, 0)); + setFirstColumnSpanned(row, QModelIndex(), true); + setRowHidden(row, QModelIndex(), !model()->hasChildren(model()->index(row, 0))); + } +}; + +class PropertyTreeItemDelegate : public QStyledItemDelegate +{ +public: + PropertyTreeItemDelegate(QObject* parent) + : QStyledItemDelegate(parent) + {} + +protected: + void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override + { + QStyleOptionViewItem opt = option; + if (!index.parent().isValid()) { + opt.font.setBold(true); + opt.features.setFlag(QStyleOptionViewItem::Alternate); + } + QStyledItemDelegate::paint(painter, opt, index); + } +}; + +MetadataDialog::MetadataDialog(QWidget *parent) + : QDialog(parent) + , m_treeView(new PropertyTreeView(this)) +{ + 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); +} + +MetadataDialog::~MetadataDialog() +{ + +} + +void MetadataDialog::setMetadataModel(MetadataModel * model) +{ + m_treeView->setModel(model); +} + +QSize MetadataDialog::sizeHint() const +{ + return QSize(520, 350); +} diff --git a/app/metadatadialog.h b/app/metadatadialog.h index e11eeb9..f4b53e6 100644 --- a/app/metadatadialog.h +++ b/app/metadatadialog.h @@ -1,30 +1,30 @@ -// SPDX-FileCopyrightText: 2022 Gary Wang -// -// SPDX-License-Identifier: MIT - -#ifndef METADATADIALOG_H -#define METADATADIALOG_H - -#include - -QT_BEGIN_NAMESPACE -class QTreeView; -QT_END_NAMESPACE - -class MetadataModel; -class MetadataDialog : public QDialog -{ - Q_OBJECT -public: - explicit MetadataDialog(QWidget * parent); - ~MetadataDialog() override; - - void setMetadataModel(MetadataModel * model); - - QSize sizeHint() const override; - -private: - QTreeView * m_treeView = nullptr; -}; - -#endif // METADATADIALOG_H +// SPDX-FileCopyrightText: 2022 Gary Wang +// +// SPDX-License-Identifier: MIT + +#ifndef METADATADIALOG_H +#define METADATADIALOG_H + +#include + +QT_BEGIN_NAMESPACE +class QTreeView; +QT_END_NAMESPACE + +class MetadataModel; +class MetadataDialog : public QDialog +{ + Q_OBJECT +public: + explicit MetadataDialog(QWidget * parent); + ~MetadataDialog() override; + + void setMetadataModel(MetadataModel * model); + + QSize sizeHint() const override; + +private: + QTreeView * m_treeView = nullptr; +}; + +#endif // METADATADIALOG_H diff --git a/app/metadatamodel.cpp b/app/metadatamodel.cpp index 0f829ad..66b9fd9 100644 --- a/app/metadatamodel.cpp +++ b/app/metadatamodel.cpp @@ -1,320 +1,320 @@ -// SPDX-FileCopyrightText: 2022 Gary Wang -// -// SPDX-License-Identifier: MIT - -#include "metadatamodel.h" -#include "exiv2wrapper.h" - -#include -#include -#include -#include -#include - -using namespace Qt::Literals::StringLiterals; - -MetadataModel::MetadataModel(QObject *parent) - : QAbstractItemModel(parent) -{ - -} - -MetadataModel::~MetadataModel() -{ - -} - -void MetadataModel::setFile(const QString &imageFilePath) -{ - QFileInfo fileInfo(imageFilePath); - // It'll be fine if we don't re-use the image reader we pass to the graphics scene for now. - QImageReader imgReader(imageFilePath); - imgReader.setAutoTransform(true); - imgReader.setDecideFormatFromContent(true); - - const QString & itemTypeString = tr("%1 File").arg(QString(imgReader.format().toUpper())); - const QString & sizeString = QLocale().formattedDataSize(fileInfo.size()); - const QString & birthTimeString = QLocale().toString(fileInfo.birthTime(), QLocale::LongFormat); - const QString & lastModifiedTimeString = QLocale().toString(fileInfo.lastModified(), QLocale::LongFormat); - const QString & imageDimensionsString = imageSize(imgReader.size()); - const QString & imageRatioString = imageSizeRatio(imgReader.size()); - - appendSection(u"Description"_s, tr("Description", "Section name.")); - appendSection(u"Origin"_s, tr("Origin", "Section name.")); - appendSection(u"Image"_s, tr("Image", "Section name.")); - appendSection(u"Camera"_s, tr("Camera", "Section name.")); - appendSection(u"AdvancedPhoto"_s, tr("Advanced photo", "Section name.")); - appendSection(u"GPS"_s, tr("GPS", "Section name.")); - appendSection(u"File"_s, tr("File", "Section name.")); - - if (imgReader.supportsOption(QImageIOHandler::Size)) { - appendProperty(u"Image"_s, u"Image.Dimensions"_s, - tr("Dimensions"), imageDimensionsString); - appendProperty(u"Image"_s, u"Image.SizeRatio"_s, - tr("Aspect ratio"), imageRatioString); - } - if (imgReader.supportsAnimation() && imgReader.imageCount() > 1) { - appendProperty(u"Image"_s, u"Image.FrameCount"_s, - tr("Frame count"), QString::number(imgReader.imageCount())); - } - - appendProperty(u"File"_s, u"File.Name"_s, - tr("Name"), fileInfo.fileName()); - appendProperty(u"File"_s, u"File.ItemType"_s, - tr("Item type"), itemTypeString); - appendProperty(u"File"_s, u"File.Path"_s, - tr("Folder path"), QDir::toNativeSeparators(fileInfo.path())); - appendProperty(u"File"_s, u"File.Size"_s, - tr("Size"), sizeString); - appendProperty(u"File"_s, u"File.CreatedTime"_s, - tr("Date created"), birthTimeString); - appendProperty(u"File"_s, u"File.LastModified"_s, - tr("Date modified"), lastModifiedTimeString); - - Exiv2Wrapper wrapper; - if (wrapper.load(imageFilePath)) { - wrapper.cacheSections(); - - appendExivPropertyIfExist(wrapper, u"Description"_s, - u"Xmp.dc.title"_s, tr("Title"), true); - appendExivPropertyIfExist(wrapper, u"Description"_s, - u"Exif.Image.ImageDescription"_s, tr("Subject"), true); - appendExivPropertyIfExist(wrapper, u"Description"_s, - u"Exif.Image.Rating"_s, tr("Rating")); - appendExivPropertyIfExist(wrapper, u"Description"_s, - u"Xmp.dc.subject"_s, tr("Tags")); - appendPropertyIfNotEmpty(u"Description"_s, u"Description.Comments"_s, - tr("Comments"), wrapper.comment()); - - appendExivPropertyIfExist(wrapper, u"Origin"_s, - u"Exif.Image.Artist"_s, tr("Authors")); - appendExivPropertyIfExist(wrapper, u"Origin"_s, - u"Exif.Photo.DateTimeOriginal"_s, tr("Date taken")); - // FIXME: We may fetch the same type of metadata from different metadata collection. - // Current implementation is not pretty and may need to do a rework... - // appendExivPropertyIfExist(wrapper, QStringLiteral("Origin"), - // QStringLiteral("Xmp.xmp.CreatorTool"), tr("Program name")); - appendExivPropertyIfExist(wrapper, u"Origin"_s, - u"Exif.Image.Software"_s, tr("Program name")); - appendExivPropertyIfExist(wrapper, u"Origin"_s, - u"Exif.Image.Copyright"_s, tr("Copyright")); - - appendExivPropertyIfExist(wrapper, u"Image"_s, - u"Exif.Image.XResolution"_s, tr("Horizontal resolution")); - appendExivPropertyIfExist(wrapper, u"Image"_s, - u"Exif.Image.YResolution"_s, tr("Vertical resolution")); - appendExivPropertyIfExist(wrapper, u"Image"_s, - u"Exif.Image.ResolutionUnit"_s, tr("Resolution unit")); - appendExivPropertyIfExist(wrapper, u"Image"_s, - u"Exif.Photo.ColorSpace"_s, tr("Colour representation")); - - appendExivPropertyIfExist(wrapper, u"Camera"_s, - u"Exif.Image.Make"_s, tr("Camera maker")); - appendExivPropertyIfExist(wrapper, u"Camera"_s, - u"Exif.Image.Model"_s, tr("Camera model")); - appendExivPropertyIfExist(wrapper, u"Camera"_s, - u"Exif.Photo.FNumber"_s, tr("F-stop")); - appendExivPropertyIfExist(wrapper, u"Camera"_s, - u"Exif.Photo.ExposureTime"_s, tr("Exposure time")); - appendExivPropertyIfExist(wrapper, u"Camera"_s, - u"Exif.Photo.ISOSpeedRatings"_s, tr("ISO speed")); - appendExivPropertyIfExist(wrapper, u"Camera"_s, - u"Exif.Photo.ExposureBiasValue"_s, tr("Exposure bias")); - appendExivPropertyIfExist(wrapper, u"Camera"_s, - u"Exif.Photo.FocalLength"_s, tr("Focal length")); - appendExivPropertyIfExist(wrapper, u"Camera"_s, - u"Exif.Photo.MaxApertureValue"_s, tr("Max aperture")); - appendExivPropertyIfExist(wrapper, u"Camera"_s, - u"Exif.Photo.MeteringMode"_s, tr("Metering mode")); - appendExivPropertyIfExist(wrapper, u"Camera"_s, - u"Exif.Photo.SubjectDistance"_s, tr("Subject distance")); - appendExivPropertyIfExist(wrapper, u"Camera"_s, - u"Exif.Photo.Flash"_s, tr("Flash mode")); - appendExivPropertyIfExist(wrapper, u"Camera"_s, - u"Exif.Photo.FocalLengthIn35mmFilm"_s, tr("35mm focal length")); - - appendExivPropertyIfExist(wrapper, u"AdvancedPhoto"_s, - u"Exif.Photo.LensModel"_s, tr("Lens model")); - appendExivPropertyIfExist(wrapper, u"AdvancedPhoto"_s, - u"Exif.Photo.Contrast"_s, tr("Contrast")); - appendExivPropertyIfExist(wrapper, u"AdvancedPhoto"_s, - u"Exif.Photo.BrightnessValue"_s, tr("Brightness")); - appendExivPropertyIfExist(wrapper, u"AdvancedPhoto"_s, - u"Exif.Photo.ExposureProgram"_s, tr("Exposure program")); - appendExivPropertyIfExist(wrapper, u"AdvancedPhoto"_s, - u"Exif.Photo.Saturation"_s, tr("Saturation")); - appendExivPropertyIfExist(wrapper, u"AdvancedPhoto"_s, - u"Exif.Photo.Sharpness"_s, tr("Sharpness")); - appendExivPropertyIfExist(wrapper, u"AdvancedPhoto"_s, - u"Exif.Photo.WhiteBalance"_s, tr("White balance")); - appendExivPropertyIfExist(wrapper, u"AdvancedPhoto"_s, - u"Exif.Photo.DigitalZoomRatio"_s, tr("Digital zoom")); - appendExivPropertyIfExist(wrapper, u"AdvancedPhoto"_s, - u"Exif.Photo.ExifVersion"_s, tr("EXIF version")); - - appendExivPropertyIfExist(wrapper, u"GPS"_s, - u"Exif.GPSInfo.GPSLatitudeRef"_s, tr("Latitude reference")); - appendExivPropertyIfExist(wrapper, u"GPS"_s, - u"Exif.GPSInfo.GPSLatitude"_s, tr("Latitude")); - appendExivPropertyIfExist(wrapper, u"GPS"_s, - u"Exif.GPSInfo.GPSLongitudeRef"_s, tr("Longitude reference")); - appendExivPropertyIfExist(wrapper, u"GPS"_s, - u"Exif.GPSInfo.GPSLongitude"_s, tr("Longitude")); - appendExivPropertyIfExist(wrapper, u"GPS"_s, - u"Exif.GPSInfo.GPSAltitudeRef"_s, tr("Altitude reference")); - appendExivPropertyIfExist(wrapper, u"GPS"_s, - u"Exif.GPSInfo.GPSAltitude"_s, tr("Altitude")); - - } -} - -QString MetadataModel::imageSize(const QSize &size) -{ - QString imageSize; - - if (size.isValid()) { - imageSize = tr("%1 x %2").arg(QString::number(size.width()), QString::number(size.height())); - } else { - imageSize = QLatin1Char('-'); - } - - return imageSize; -} - -int simplegcd(int a, int b) { - return b == 0 ? a : simplegcd(b, a % b); -} - -QString MetadataModel::imageSizeRatio(const QSize &size) -{ - if (!size.isValid()) { - return QStringLiteral("-"); - } - int gcd = simplegcd(size.width(), size.height()); - return tr("%1 : %2").arg(QString::number(size.width() / gcd), QString::number(size.height() / gcd)); -} - -bool MetadataModel::appendSection(const QString §ionKey, QStringView sectionDisplayName) -{ - if (m_sections.contains(sectionKey)) { - return false; - } - - m_sections.append(sectionKey); - m_sectionProperties[sectionKey] = qMakePair >(sectionDisplayName.toString(), {}); - - return true; -} - -bool MetadataModel::appendPropertyIfNotEmpty(const QString §ionKey, const QString &propertyKey, const QString &propertyDisplayName, const QString &propertyValue) -{ - if (propertyValue.isEmpty()) return false; - - return appendProperty(sectionKey, propertyKey, propertyDisplayName, propertyValue); -} - -bool MetadataModel::appendProperty(const QString §ionKey, const QString &propertyKey, QStringView propertyDisplayName, QStringView propertyValue) -{ - if (!m_sections.contains(sectionKey)) { - return false; - } - - QList & propertyList = m_sectionProperties[sectionKey].second; - if (!propertyList.contains(propertyKey)) { - propertyList.append(propertyKey); - } - - m_properties[propertyKey] = qMakePair(propertyDisplayName.toString(), propertyValue.toString()); - - return true; -} - -bool MetadataModel::appendExivPropertyIfExist(const Exiv2Wrapper &wrapper, const QString §ionKey, const QString &exiv2propertyKey, const QString &propertyDisplayName, bool isXmpString) -{ - const QString & value = wrapper.value(exiv2propertyKey); - if (!value.isEmpty()) { - appendProperty(sectionKey, exiv2propertyKey, - propertyDisplayName.isEmpty() ? wrapper.label(exiv2propertyKey) : propertyDisplayName, - isXmpString ? Exiv2Wrapper::XmpValue(value) : value); - return true; - } - return false; -} - -QModelIndex MetadataModel::index(int row, int column, const QModelIndex &parent) const -{ - if (!hasIndex(row, column, parent)) { - return QModelIndex(); - } - - if (!parent.isValid()) { - return createIndex(row, column, RowType::SectionRow); - } else { - // internalid param: row means nth section it belongs to. - return createIndex(row, column, RowType::PropertyRow + parent.row()); - } -} - -QModelIndex MetadataModel::parent(const QModelIndex &child) const -{ - if (!child.isValid()) { - return QModelIndex(); - } - - if (child.internalId() == RowType::SectionRow) { - return QModelIndex(); - } else { - return createIndex(child.internalId() - RowType::PropertyRow, 0, SectionRow); - } -} - -int MetadataModel::rowCount(const QModelIndex &parent) const -{ - if (!parent.isValid()) { - return m_sections.count(); - } - - if (parent.internalId() == RowType::SectionRow) { - const QString & sectionKey = m_sections[parent.row()]; - return m_sectionProperties[sectionKey].second.count(); - } - - return 0; -} - -int MetadataModel::columnCount(const QModelIndex &) const -{ - // Always key(display name) and value. - return 2; -} - -QVariant MetadataModel::data(const QModelIndex &index, int role) const -{ - if (!index.isValid()) { - return QVariant(); - } - - if (role != Qt::DisplayRole) { - return QVariant(); - } - - if (index.internalId() == RowType::SectionRow) { - return (index.column() == 0) ? m_sectionProperties[m_sections[index.row()]].first - : QVariant(); - } else { - int sectionIndex = index.internalId() - RowType::PropertyRow; - const QString & sectionKey = m_sections[sectionIndex]; - const QList & propertyList = m_sectionProperties[sectionKey].second; - return (index.column() == 0) ? m_properties[propertyList[index.row()]].first - : m_properties[propertyList[index.row()]].second; - } -} - -QVariant MetadataModel::headerData(int section, Qt::Orientation orientation, int role) const -{ - if (orientation == Qt::Vertical || role != Qt::DisplayRole) { - return QVariant(); - } - - return section == 0 ? tr("Property") : tr("Value"); -} +// SPDX-FileCopyrightText: 2022 Gary Wang +// +// SPDX-License-Identifier: MIT + +#include "metadatamodel.h" +#include "exiv2wrapper.h" + +#include +#include +#include +#include +#include + +using namespace Qt::Literals::StringLiterals; + +MetadataModel::MetadataModel(QObject *parent) + : QAbstractItemModel(parent) +{ + +} + +MetadataModel::~MetadataModel() +{ + +} + +void MetadataModel::setFile(const QString &imageFilePath) +{ + QFileInfo fileInfo(imageFilePath); + // It'll be fine if we don't re-use the image reader we pass to the graphics scene for now. + QImageReader imgReader(imageFilePath); + imgReader.setAutoTransform(true); + imgReader.setDecideFormatFromContent(true); + + const QString & itemTypeString = tr("%1 File").arg(QString(imgReader.format().toUpper())); + const QString & sizeString = QLocale().formattedDataSize(fileInfo.size()); + const QString & birthTimeString = QLocale().toString(fileInfo.birthTime(), QLocale::LongFormat); + const QString & lastModifiedTimeString = QLocale().toString(fileInfo.lastModified(), QLocale::LongFormat); + const QString & imageDimensionsString = imageSize(imgReader.size()); + const QString & imageRatioString = imageSizeRatio(imgReader.size()); + + appendSection(u"Description"_s, tr("Description", "Section name.")); + appendSection(u"Origin"_s, tr("Origin", "Section name.")); + appendSection(u"Image"_s, tr("Image", "Section name.")); + appendSection(u"Camera"_s, tr("Camera", "Section name.")); + appendSection(u"AdvancedPhoto"_s, tr("Advanced photo", "Section name.")); + appendSection(u"GPS"_s, tr("GPS", "Section name.")); + appendSection(u"File"_s, tr("File", "Section name.")); + + if (imgReader.supportsOption(QImageIOHandler::Size)) { + appendProperty(u"Image"_s, u"Image.Dimensions"_s, + tr("Dimensions"), imageDimensionsString); + appendProperty(u"Image"_s, u"Image.SizeRatio"_s, + tr("Aspect ratio"), imageRatioString); + } + if (imgReader.supportsAnimation() && imgReader.imageCount() > 1) { + appendProperty(u"Image"_s, u"Image.FrameCount"_s, + tr("Frame count"), QString::number(imgReader.imageCount())); + } + + appendProperty(u"File"_s, u"File.Name"_s, + tr("Name"), fileInfo.fileName()); + appendProperty(u"File"_s, u"File.ItemType"_s, + tr("Item type"), itemTypeString); + appendProperty(u"File"_s, u"File.Path"_s, + tr("Folder path"), QDir::toNativeSeparators(fileInfo.path())); + appendProperty(u"File"_s, u"File.Size"_s, + tr("Size"), sizeString); + appendProperty(u"File"_s, u"File.CreatedTime"_s, + tr("Date created"), birthTimeString); + appendProperty(u"File"_s, u"File.LastModified"_s, + tr("Date modified"), lastModifiedTimeString); + + Exiv2Wrapper wrapper; + if (wrapper.load(imageFilePath)) { + wrapper.cacheSections(); + + appendExivPropertyIfExist(wrapper, u"Description"_s, + u"Xmp.dc.title"_s, tr("Title"), true); + appendExivPropertyIfExist(wrapper, u"Description"_s, + u"Exif.Image.ImageDescription"_s, tr("Subject"), true); + appendExivPropertyIfExist(wrapper, u"Description"_s, + u"Exif.Image.Rating"_s, tr("Rating")); + appendExivPropertyIfExist(wrapper, u"Description"_s, + u"Xmp.dc.subject"_s, tr("Tags")); + appendPropertyIfNotEmpty(u"Description"_s, u"Description.Comments"_s, + tr("Comments"), wrapper.comment()); + + appendExivPropertyIfExist(wrapper, u"Origin"_s, + u"Exif.Image.Artist"_s, tr("Authors")); + appendExivPropertyIfExist(wrapper, u"Origin"_s, + u"Exif.Photo.DateTimeOriginal"_s, tr("Date taken")); + // FIXME: We may fetch the same type of metadata from different metadata collection. + // Current implementation is not pretty and may need to do a rework... + // appendExivPropertyIfExist(wrapper, QStringLiteral("Origin"), + // QStringLiteral("Xmp.xmp.CreatorTool"), tr("Program name")); + appendExivPropertyIfExist(wrapper, u"Origin"_s, + u"Exif.Image.Software"_s, tr("Program name")); + appendExivPropertyIfExist(wrapper, u"Origin"_s, + u"Exif.Image.Copyright"_s, tr("Copyright")); + + appendExivPropertyIfExist(wrapper, u"Image"_s, + u"Exif.Image.XResolution"_s, tr("Horizontal resolution")); + appendExivPropertyIfExist(wrapper, u"Image"_s, + u"Exif.Image.YResolution"_s, tr("Vertical resolution")); + appendExivPropertyIfExist(wrapper, u"Image"_s, + u"Exif.Image.ResolutionUnit"_s, tr("Resolution unit")); + appendExivPropertyIfExist(wrapper, u"Image"_s, + u"Exif.Photo.ColorSpace"_s, tr("Colour representation")); + + appendExivPropertyIfExist(wrapper, u"Camera"_s, + u"Exif.Image.Make"_s, tr("Camera maker")); + appendExivPropertyIfExist(wrapper, u"Camera"_s, + u"Exif.Image.Model"_s, tr("Camera model")); + appendExivPropertyIfExist(wrapper, u"Camera"_s, + u"Exif.Photo.FNumber"_s, tr("F-stop")); + appendExivPropertyIfExist(wrapper, u"Camera"_s, + u"Exif.Photo.ExposureTime"_s, tr("Exposure time")); + appendExivPropertyIfExist(wrapper, u"Camera"_s, + u"Exif.Photo.ISOSpeedRatings"_s, tr("ISO speed")); + appendExivPropertyIfExist(wrapper, u"Camera"_s, + u"Exif.Photo.ExposureBiasValue"_s, tr("Exposure bias")); + appendExivPropertyIfExist(wrapper, u"Camera"_s, + u"Exif.Photo.FocalLength"_s, tr("Focal length")); + appendExivPropertyIfExist(wrapper, u"Camera"_s, + u"Exif.Photo.MaxApertureValue"_s, tr("Max aperture")); + appendExivPropertyIfExist(wrapper, u"Camera"_s, + u"Exif.Photo.MeteringMode"_s, tr("Metering mode")); + appendExivPropertyIfExist(wrapper, u"Camera"_s, + u"Exif.Photo.SubjectDistance"_s, tr("Subject distance")); + appendExivPropertyIfExist(wrapper, u"Camera"_s, + u"Exif.Photo.Flash"_s, tr("Flash mode")); + appendExivPropertyIfExist(wrapper, u"Camera"_s, + u"Exif.Photo.FocalLengthIn35mmFilm"_s, tr("35mm focal length")); + + appendExivPropertyIfExist(wrapper, u"AdvancedPhoto"_s, + u"Exif.Photo.LensModel"_s, tr("Lens model")); + appendExivPropertyIfExist(wrapper, u"AdvancedPhoto"_s, + u"Exif.Photo.Contrast"_s, tr("Contrast")); + appendExivPropertyIfExist(wrapper, u"AdvancedPhoto"_s, + u"Exif.Photo.BrightnessValue"_s, tr("Brightness")); + appendExivPropertyIfExist(wrapper, u"AdvancedPhoto"_s, + u"Exif.Photo.ExposureProgram"_s, tr("Exposure program")); + appendExivPropertyIfExist(wrapper, u"AdvancedPhoto"_s, + u"Exif.Photo.Saturation"_s, tr("Saturation")); + appendExivPropertyIfExist(wrapper, u"AdvancedPhoto"_s, + u"Exif.Photo.Sharpness"_s, tr("Sharpness")); + appendExivPropertyIfExist(wrapper, u"AdvancedPhoto"_s, + u"Exif.Photo.WhiteBalance"_s, tr("White balance")); + appendExivPropertyIfExist(wrapper, u"AdvancedPhoto"_s, + u"Exif.Photo.DigitalZoomRatio"_s, tr("Digital zoom")); + appendExivPropertyIfExist(wrapper, u"AdvancedPhoto"_s, + u"Exif.Photo.ExifVersion"_s, tr("EXIF version")); + + appendExivPropertyIfExist(wrapper, u"GPS"_s, + u"Exif.GPSInfo.GPSLatitudeRef"_s, tr("Latitude reference")); + appendExivPropertyIfExist(wrapper, u"GPS"_s, + u"Exif.GPSInfo.GPSLatitude"_s, tr("Latitude")); + appendExivPropertyIfExist(wrapper, u"GPS"_s, + u"Exif.GPSInfo.GPSLongitudeRef"_s, tr("Longitude reference")); + appendExivPropertyIfExist(wrapper, u"GPS"_s, + u"Exif.GPSInfo.GPSLongitude"_s, tr("Longitude")); + appendExivPropertyIfExist(wrapper, u"GPS"_s, + u"Exif.GPSInfo.GPSAltitudeRef"_s, tr("Altitude reference")); + appendExivPropertyIfExist(wrapper, u"GPS"_s, + u"Exif.GPSInfo.GPSAltitude"_s, tr("Altitude")); + + } +} + +QString MetadataModel::imageSize(const QSize &size) +{ + QString imageSize; + + if (size.isValid()) { + imageSize = tr("%1 x %2").arg(QString::number(size.width()), QString::number(size.height())); + } else { + imageSize = QLatin1Char('-'); + } + + return imageSize; +} + +int simplegcd(int a, int b) { + return b == 0 ? a : simplegcd(b, a % b); +} + +QString MetadataModel::imageSizeRatio(const QSize &size) +{ + if (!size.isValid()) { + return QStringLiteral("-"); + } + int gcd = simplegcd(size.width(), size.height()); + return tr("%1 : %2").arg(QString::number(size.width() / gcd), QString::number(size.height() / gcd)); +} + +bool MetadataModel::appendSection(const QString §ionKey, QStringView sectionDisplayName) +{ + if (m_sections.contains(sectionKey)) { + return false; + } + + m_sections.append(sectionKey); + m_sectionProperties[sectionKey] = qMakePair >(sectionDisplayName.toString(), {}); + + return true; +} + +bool MetadataModel::appendPropertyIfNotEmpty(const QString §ionKey, const QString &propertyKey, const QString &propertyDisplayName, const QString &propertyValue) +{ + if (propertyValue.isEmpty()) return false; + + return appendProperty(sectionKey, propertyKey, propertyDisplayName, propertyValue); +} + +bool MetadataModel::appendProperty(const QString §ionKey, const QString &propertyKey, QStringView propertyDisplayName, QStringView propertyValue) +{ + if (!m_sections.contains(sectionKey)) { + return false; + } + + QList & propertyList = m_sectionProperties[sectionKey].second; + if (!propertyList.contains(propertyKey)) { + propertyList.append(propertyKey); + } + + m_properties[propertyKey] = qMakePair(propertyDisplayName.toString(), propertyValue.toString()); + + return true; +} + +bool MetadataModel::appendExivPropertyIfExist(const Exiv2Wrapper &wrapper, const QString §ionKey, const QString &exiv2propertyKey, const QString &propertyDisplayName, bool isXmpString) +{ + const QString & value = wrapper.value(exiv2propertyKey); + if (!value.isEmpty()) { + appendProperty(sectionKey, exiv2propertyKey, + propertyDisplayName.isEmpty() ? wrapper.label(exiv2propertyKey) : propertyDisplayName, + isXmpString ? Exiv2Wrapper::XmpValue(value) : value); + return true; + } + return false; +} + +QModelIndex MetadataModel::index(int row, int column, const QModelIndex &parent) const +{ + if (!hasIndex(row, column, parent)) { + return QModelIndex(); + } + + if (!parent.isValid()) { + return createIndex(row, column, RowType::SectionRow); + } else { + // internalid param: row means nth section it belongs to. + return createIndex(row, column, RowType::PropertyRow + parent.row()); + } +} + +QModelIndex MetadataModel::parent(const QModelIndex &child) const +{ + if (!child.isValid()) { + return QModelIndex(); + } + + if (child.internalId() == RowType::SectionRow) { + return QModelIndex(); + } else { + return createIndex(child.internalId() - RowType::PropertyRow, 0, SectionRow); + } +} + +int MetadataModel::rowCount(const QModelIndex &parent) const +{ + if (!parent.isValid()) { + return m_sections.count(); + } + + if (parent.internalId() == RowType::SectionRow) { + const QString & sectionKey = m_sections[parent.row()]; + return m_sectionProperties[sectionKey].second.count(); + } + + return 0; +} + +int MetadataModel::columnCount(const QModelIndex &) const +{ + // Always key(display name) and value. + return 2; +} + +QVariant MetadataModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return QVariant(); + } + + if (role != Qt::DisplayRole) { + return QVariant(); + } + + if (index.internalId() == RowType::SectionRow) { + return (index.column() == 0) ? m_sectionProperties[m_sections[index.row()]].first + : QVariant(); + } else { + int sectionIndex = index.internalId() - RowType::PropertyRow; + const QString & sectionKey = m_sections[sectionIndex]; + const QList & propertyList = m_sectionProperties[sectionKey].second; + return (index.column() == 0) ? m_properties[propertyList[index.row()]].first + : m_properties[propertyList[index.row()]].second; + } +} + +QVariant MetadataModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation == Qt::Vertical || role != Qt::DisplayRole) { + return QVariant(); + } + + return section == 0 ? tr("Property") : tr("Value"); +} diff --git a/app/metadatamodel.h b/app/metadatamodel.h index e199181..a89178d 100644 --- a/app/metadatamodel.h +++ b/app/metadatamodel.h @@ -1,52 +1,52 @@ -// SPDX-FileCopyrightText: 2022 Gary Wang -// -// SPDX-License-Identifier: MIT - -#ifndef METADATAMODEL_H -#define METADATAMODEL_H - -#include - -class Exiv2Wrapper; -class MetadataModel : public QAbstractItemModel -{ - Q_OBJECT - -public: - explicit MetadataModel(QObject *parent = nullptr); - ~MetadataModel(); - - void setFile(const QString & imageFilePath); - static QString imageSize(const QSize &size); - static QString imageSizeRatio(const QSize &size); - bool appendSection(const QString & sectionKey, QStringView sectionDisplayName); - bool appendPropertyIfNotEmpty(const QString & sectionKey, const QString & propertyKey, - const QString & propertyDisplayName, const QString & propertyValue = QString()); - bool appendProperty(const QString & sectionKey, const QString & propertyKey, - QStringView propertyDisplayName, QStringView propertyValue = QString()); - bool appendExivPropertyIfExist(const Exiv2Wrapper & wrapper, const QString & sectionKey, - const QString & exiv2propertyKey, const QString & propertyDisplayName = QString(), - bool isXmpString = false); - -private: - enum RowType : quintptr { - SectionRow, - PropertyRow, - }; - - QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; - QModelIndex parent(const QModelIndex &child) const override; - int rowCount(const QModelIndex &parent = QModelIndex()) const override; - int columnCount(const QModelIndex & = QModelIndex()) const override; - QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; - - // [SECTION_KEY] - QList m_sections; - // {SECTION_KEY: (SECTION_DISPLAY_NAME, [PROPERTY_KEY])} - QMap > > m_sectionProperties; - // {PROPERTY_KEY: (PROPERTY_DISPLAY_NAME, PROPERTY_VALUE)} - QMap > m_properties; -}; - -#endif // METADATAMODEL_H +// SPDX-FileCopyrightText: 2022 Gary Wang +// +// SPDX-License-Identifier: MIT + +#ifndef METADATAMODEL_H +#define METADATAMODEL_H + +#include + +class Exiv2Wrapper; +class MetadataModel : public QAbstractItemModel +{ + Q_OBJECT + +public: + explicit MetadataModel(QObject *parent = nullptr); + ~MetadataModel(); + + void setFile(const QString & imageFilePath); + static QString imageSize(const QSize &size); + static QString imageSizeRatio(const QSize &size); + bool appendSection(const QString & sectionKey, QStringView sectionDisplayName); + bool appendPropertyIfNotEmpty(const QString & sectionKey, const QString & propertyKey, + const QString & propertyDisplayName, const QString & propertyValue = QString()); + bool appendProperty(const QString & sectionKey, const QString & propertyKey, + QStringView propertyDisplayName, QStringView propertyValue = QString()); + bool appendExivPropertyIfExist(const Exiv2Wrapper & wrapper, const QString & sectionKey, + const QString & exiv2propertyKey, const QString & propertyDisplayName = QString(), + bool isXmpString = false); + +private: + enum RowType : quintptr { + SectionRow, + PropertyRow, + }; + + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex &child) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex & = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + + // [SECTION_KEY] + QList m_sections; + // {SECTION_KEY: (SECTION_DISPLAY_NAME, [PROPERTY_KEY])} + QMap > > m_sectionProperties; + // {PROPERTY_KEY: (PROPERTY_DISPLAY_NAME, PROPERTY_VALUE)} + QMap > m_properties; +}; + +#endif // METADATAMODEL_H diff --git a/app/navigatorview.cpp b/app/navigatorview.cpp index 95c6cd1..bc56ce4 100644 --- a/app/navigatorview.cpp +++ b/app/navigatorview.cpp @@ -1,86 +1,86 @@ -// SPDX-FileCopyrightText: 2022 Gary Wang -// -// SPDX-License-Identifier: MIT - -#include "navigatorview.h" - -#include "graphicsview.h" -#include "opacityhelper.h" - -#include -#include - -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;"); -} - -// doesn't take or manage its ownership -void NavigatorView::setMainView(GraphicsView *mainView) -{ - m_mainView = mainView; -} - -void NavigatorView::setOpacity(qreal opacity, bool animated) -{ - m_opacityHelper->setOpacity(opacity, animated); -} - -void NavigatorView::updateMainViewportRegion() -{ - if (m_mainView != nullptr) { - m_viewportRegion = mapFromScene(m_mainView->mapToScene(m_mainView->rect())); - update(); - } -} - -void NavigatorView::mousePressEvent(QMouseEvent *event) -{ - m_mouseDown = true; - - if (m_mainView) { - m_mainView->centerOn(mapToScene(event->pos())); - update(); - } - - event->accept(); -} - -void NavigatorView::mouseMoveEvent(QMouseEvent *event) -{ - if (m_mouseDown && m_mainView) { - m_mainView->centerOn(mapToScene(event->pos())); - update(); - event->accept(); - } else { - event->ignore(); - } -} - -void NavigatorView::mouseReleaseEvent(QMouseEvent *event) -{ - m_mouseDown = false; - - event->accept(); -} - -void NavigatorView::wheelEvent(QWheelEvent *event) -{ - event->ignore(); - return QGraphicsView::wheelEvent(event); -} - -void NavigatorView::paintEvent(QPaintEvent *event) -{ - QGraphicsView::paintEvent(event); - - QPainter painter(viewport()); - painter.setPen(QPen(Qt::gray, 2)); - painter.drawRect(m_viewportRegion.boundingRect()); -} +// SPDX-FileCopyrightText: 2022 Gary Wang +// +// SPDX-License-Identifier: MIT + +#include "navigatorview.h" + +#include "graphicsview.h" +#include "opacityhelper.h" + +#include +#include + +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;"); +} + +// doesn't take or manage its ownership +void NavigatorView::setMainView(GraphicsView *mainView) +{ + m_mainView = mainView; +} + +void NavigatorView::setOpacity(qreal opacity, bool animated) +{ + m_opacityHelper->setOpacity(opacity, animated); +} + +void NavigatorView::updateMainViewportRegion() +{ + if (m_mainView != nullptr) { + m_viewportRegion = mapFromScene(m_mainView->mapToScene(m_mainView->rect())); + update(); + } +} + +void NavigatorView::mousePressEvent(QMouseEvent *event) +{ + m_mouseDown = true; + + if (m_mainView) { + m_mainView->centerOn(mapToScene(event->pos())); + update(); + } + + event->accept(); +} + +void NavigatorView::mouseMoveEvent(QMouseEvent *event) +{ + if (m_mouseDown && m_mainView) { + m_mainView->centerOn(mapToScene(event->pos())); + update(); + event->accept(); + } else { + event->ignore(); + } +} + +void NavigatorView::mouseReleaseEvent(QMouseEvent *event) +{ + m_mouseDown = false; + + event->accept(); +} + +void NavigatorView::wheelEvent(QWheelEvent *event) +{ + event->ignore(); + return QGraphicsView::wheelEvent(event); +} + +void NavigatorView::paintEvent(QPaintEvent *event) +{ + QGraphicsView::paintEvent(event); + + QPainter painter(viewport()); + painter.setPen(QPen(Qt::gray, 2)); + painter.drawRect(m_viewportRegion.boundingRect()); +} diff --git a/app/navigatorview.h b/app/navigatorview.h index 444a68a..96b7a6b 100644 --- a/app/navigatorview.h +++ b/app/navigatorview.h @@ -1,38 +1,38 @@ -// SPDX-FileCopyrightText: 2022 Gary Wang -// -// SPDX-License-Identifier: MIT - -#ifndef NAVIGATORVIEW_H -#define NAVIGATORVIEW_H - -#include - -class OpacityHelper; -class GraphicsView; -class NavigatorView : public QGraphicsView -{ - Q_OBJECT -public: - NavigatorView(QWidget *parent = nullptr); - - void setMainView(GraphicsView *mainView); - void setOpacity(qreal opacity, bool animated = true); - -public slots: - void updateMainViewportRegion(); - -private: - void mousePressEvent(QMouseEvent * event) override; - void mouseMoveEvent(QMouseEvent * event) override; - void mouseReleaseEvent(QMouseEvent * event) override; - - void wheelEvent(QWheelEvent *event) override; - void paintEvent(QPaintEvent *event) override; - - bool m_mouseDown = false; - QPolygon m_viewportRegion; - QGraphicsView *m_mainView = nullptr; - OpacityHelper *m_opacityHelper = nullptr; -}; - -#endif // NAVIGATORVIEW_H +// SPDX-FileCopyrightText: 2022 Gary Wang +// +// SPDX-License-Identifier: MIT + +#ifndef NAVIGATORVIEW_H +#define NAVIGATORVIEW_H + +#include + +class OpacityHelper; +class GraphicsView; +class NavigatorView : public QGraphicsView +{ + Q_OBJECT +public: + NavigatorView(QWidget *parent = nullptr); + + void setMainView(GraphicsView *mainView); + void setOpacity(qreal opacity, bool animated = true); + +public slots: + void updateMainViewportRegion(); + +private: + void mousePressEvent(QMouseEvent * event) override; + void mouseMoveEvent(QMouseEvent * event) override; + void mouseReleaseEvent(QMouseEvent * event) override; + + void wheelEvent(QWheelEvent *event) override; + void paintEvent(QPaintEvent *event) override; + + bool m_mouseDown = false; + QPolygon m_viewportRegion; + QGraphicsView *m_mainView = nullptr; + OpacityHelper *m_opacityHelper = nullptr; +}; + +#endif // NAVIGATORVIEW_H diff --git a/app/opacityhelper.cpp b/app/opacityhelper.cpp index c08418c..f81efe0 100644 --- a/app/opacityhelper.cpp +++ b/app/opacityhelper.cpp @@ -1,31 +1,31 @@ -// SPDX-FileCopyrightText: 2022 Gary Wang -// -// SPDX-License-Identifier: MIT - -#include "opacityhelper.h" - -#include -#include - -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(); -} +// SPDX-FileCopyrightText: 2022 Gary Wang +// +// SPDX-License-Identifier: MIT + +#include "opacityhelper.h" + +#include +#include + +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(); +} diff --git a/app/opacityhelper.h b/app/opacityhelper.h index 801cbb8..7e4647b 100644 --- a/app/opacityhelper.h +++ b/app/opacityhelper.h @@ -1,27 +1,27 @@ -// SPDX-FileCopyrightText: 2022 Gary Wang -// -// SPDX-License-Identifier: MIT - -#ifndef OPACITYHELPER_H -#define OPACITYHELPER_H - -#include - -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 +// SPDX-FileCopyrightText: 2022 Gary Wang +// +// SPDX-License-Identifier: MIT + +#ifndef OPACITYHELPER_H +#define OPACITYHELPER_H + +#include + +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 diff --git a/app/playlistmanager.cpp b/app/playlistmanager.cpp index c86ff02..e3dafd7 100644 --- a/app/playlistmanager.cpp +++ b/app/playlistmanager.cpp @@ -1,283 +1,283 @@ -// SPDX-FileCopyrightText: 2025 Gary Wang -// -// SPDX-License-Identifier: MIT - -#include "playlistmanager.h" - -#include -#include -#include -#include - -PlaylistModel::PlaylistModel(QObject *parent) - : QAbstractListModel(parent) -{ - -} - -PlaylistModel::~PlaylistModel() -= default; - -void PlaylistModel::setPlaylist(const QList &urls) -{ - beginResetModel(); - m_playlist = urls; - endResetModel(); -} - -QModelIndex PlaylistModel::loadPlaylist(const QList & 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 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 PlaylistModel::roleNames() const -{ - QHash 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) -{ - 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() -{ - -} - -PlaylistModel *PlaylistManager::model() -{ - return &m_model; -} - -void PlaylistManager::setPlaylist(const QList &urls) -{ - m_model.setPlaylist(urls); -} - -QModelIndex PlaylistManager::loadPlaylist(const QList &urls) -{ - QModelIndex idx = m_model.loadPlaylist(urls); - setProperty("currentIndex", idx.row()); - return idx; -} - -QModelIndex PlaylistManager::loadPlaylist(const QUrl &url) -{ - QModelIndex idx = m_model.loadPlaylist(url); - setProperty("currentIndex", idx.row()); - return idx; -} - -QModelIndex PlaylistManager::loadM3U8Playlist(const QUrl &url) -{ - QFile file(url.toLocalFile()); - if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { - QList urls; - while (!file.atEnd()) { - QString line = file.readLine(); - if (line.startsWith('#')) { - continue; - } - QFileInfo fileInfo(file); - QUrl item = QUrl::fromUserInput(line, fileInfo.absolutePath()); - urls.append(item); - } - return loadPlaylist(urls); - } else { - return {}; - } -} - -int PlaylistManager::totalCount() const -{ - return m_model.rowCount(); -} - -QModelIndex PlaylistManager::previousIndex() const -{ - int count = totalCount(); - if (count == 0) return {}; - - return m_model.index(isFirstIndex() ? count - 1 : m_currentIndex - 1); -} - -QModelIndex PlaylistManager::nextIndex() const -{ - int count = totalCount(); - if (count == 0) return {}; - - return m_model.index(isLastIndex() ? 0 : m_currentIndex + 1); -} - -QModelIndex PlaylistManager::curIndex() const -{ - return m_model.index(m_currentIndex); -} - -bool PlaylistManager::isFirstIndex() const -{ - return m_currentIndex == 0; -} - -bool PlaylistManager::isLastIndex() const -{ - return m_currentIndex + 1 == totalCount(); -} - -void PlaylistManager::setCurrentIndex(const QModelIndex &index) -{ - if (index.isValid() && index.row() >= 0 && index.row() < totalCount()) { - setProperty("currentIndex", index.row()); - } -} - -QUrl PlaylistManager::urlByIndex(const QModelIndex &index) -{ - return m_model.urlByIndex(index.row()); -} - -QString PlaylistManager::localFileByIndex(const QModelIndex &index) -{ - return urlByIndex(index).toLocalFile(); -} - -bool PlaylistManager::removeAt(const QModelIndex &index) -{ - return m_model.removeAt(index.row()); -} - -void PlaylistManager::setAutoLoadFilterSuffixes(const QStringList &nameFilters) -{ - m_model.setProperty("autoLoadFilterSuffixes", nameFilters); -} - -QList PlaylistManager::convertToUrlList(const QStringList &files) -{ - QList urlList; - for (const QString & str : std::as_const(files)) { - QUrl url = QUrl::fromLocalFile(str); - if (url.isValid()) { - urlList.append(url); - } - } - - return urlList; -} +// SPDX-FileCopyrightText: 2025 Gary Wang +// +// SPDX-License-Identifier: MIT + +#include "playlistmanager.h" + +#include +#include +#include +#include + +PlaylistModel::PlaylistModel(QObject *parent) + : QAbstractListModel(parent) +{ + +} + +PlaylistModel::~PlaylistModel() += default; + +void PlaylistModel::setPlaylist(const QList &urls) +{ + beginResetModel(); + m_playlist = urls; + endResetModel(); +} + +QModelIndex PlaylistModel::loadPlaylist(const QList & 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 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 PlaylistModel::roleNames() const +{ + QHash 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) +{ + 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() +{ + +} + +PlaylistModel *PlaylistManager::model() +{ + return &m_model; +} + +void PlaylistManager::setPlaylist(const QList &urls) +{ + m_model.setPlaylist(urls); +} + +QModelIndex PlaylistManager::loadPlaylist(const QList &urls) +{ + QModelIndex idx = m_model.loadPlaylist(urls); + setProperty("currentIndex", idx.row()); + return idx; +} + +QModelIndex PlaylistManager::loadPlaylist(const QUrl &url) +{ + QModelIndex idx = m_model.loadPlaylist(url); + setProperty("currentIndex", idx.row()); + return idx; +} + +QModelIndex PlaylistManager::loadM3U8Playlist(const QUrl &url) +{ + QFile file(url.toLocalFile()); + if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { + QList urls; + while (!file.atEnd()) { + QString line = file.readLine(); + if (line.startsWith('#')) { + continue; + } + QFileInfo fileInfo(file); + QUrl item = QUrl::fromUserInput(line, fileInfo.absolutePath()); + urls.append(item); + } + return loadPlaylist(urls); + } else { + return {}; + } +} + +int PlaylistManager::totalCount() const +{ + return m_model.rowCount(); +} + +QModelIndex PlaylistManager::previousIndex() const +{ + int count = totalCount(); + if (count == 0) return {}; + + return m_model.index(isFirstIndex() ? count - 1 : m_currentIndex - 1); +} + +QModelIndex PlaylistManager::nextIndex() const +{ + int count = totalCount(); + if (count == 0) return {}; + + return m_model.index(isLastIndex() ? 0 : m_currentIndex + 1); +} + +QModelIndex PlaylistManager::curIndex() const +{ + return m_model.index(m_currentIndex); +} + +bool PlaylistManager::isFirstIndex() const +{ + return m_currentIndex == 0; +} + +bool PlaylistManager::isLastIndex() const +{ + return m_currentIndex + 1 == totalCount(); +} + +void PlaylistManager::setCurrentIndex(const QModelIndex &index) +{ + if (index.isValid() && index.row() >= 0 && index.row() < totalCount()) { + setProperty("currentIndex", index.row()); + } +} + +QUrl PlaylistManager::urlByIndex(const QModelIndex &index) +{ + return m_model.urlByIndex(index.row()); +} + +QString PlaylistManager::localFileByIndex(const QModelIndex &index) +{ + return urlByIndex(index).toLocalFile(); +} + +bool PlaylistManager::removeAt(const QModelIndex &index) +{ + return m_model.removeAt(index.row()); +} + +void PlaylistManager::setAutoLoadFilterSuffixes(const QStringList &nameFilters) +{ + m_model.setProperty("autoLoadFilterSuffixes", nameFilters); +} + +QList PlaylistManager::convertToUrlList(const QStringList &files) +{ + QList urlList; + for (const QString & str : std::as_const(files)) { + QUrl url = QUrl::fromLocalFile(str); + if (url.isValid()) { + urlList.append(url); + } + } + + return urlList; +} diff --git a/app/playlistmanager.h b/app/playlistmanager.h index 6bc2aaf..6d496ef 100644 --- a/app/playlistmanager.h +++ b/app/playlistmanager.h @@ -1,88 +1,88 @@ -// SPDX-FileCopyrightText: 2025 Gary Wang -// -// SPDX-License-Identifier: MIT - -#pragma once - -#include -#include - -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 & urls); - QModelIndex loadPlaylist(const QList & 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 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 m_playlist; - // properties - QStringList m_autoLoadSuffixes = {}; - // internal - QString m_currentDir; -}; - -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); - ~PlaylistManager(); - - PlaylistModel * model(); - - void setPlaylist(const QList & url); - Q_INVOKABLE QModelIndex loadPlaylist(const QList & urls); - Q_INVOKABLE QModelIndex loadPlaylist(const QUrl & url); - Q_INVOKABLE QModelIndex loadM3U8Playlist(const QUrl & url); - - int totalCount() const; - QModelIndex previousIndex() const; - QModelIndex nextIndex() const; - QModelIndex curIndex() const; - bool isFirstIndex() const; - 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 convertToUrlList(const QStringList & files); - -signals: - void currentIndexChanged(int index); - void totalCountChanged(int count); - -private: - int m_currentIndex = -1; - PlaylistModel m_model; -}; +// SPDX-FileCopyrightText: 2025 Gary Wang +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include +#include + +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 & urls); + QModelIndex loadPlaylist(const QList & 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 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 m_playlist; + // properties + QStringList m_autoLoadSuffixes = {}; + // internal + QString m_currentDir; +}; + +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); + ~PlaylistManager(); + + PlaylistModel * model(); + + void setPlaylist(const QList & url); + Q_INVOKABLE QModelIndex loadPlaylist(const QList & urls); + Q_INVOKABLE QModelIndex loadPlaylist(const QUrl & url); + Q_INVOKABLE QModelIndex loadM3U8Playlist(const QUrl & url); + + int totalCount() const; + QModelIndex previousIndex() const; + QModelIndex nextIndex() const; + QModelIndex curIndex() const; + bool isFirstIndex() const; + 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 convertToUrlList(const QStringList & files); + +signals: + void currentIndexChanged(int index); + void totalCountChanged(int count); + +private: + int m_currentIndex = -1; + PlaylistModel m_model; +}; diff --git a/app/settings.cpp b/app/settings.cpp index a63998b..d8f2b43 100644 --- a/app/settings.cpp +++ b/app/settings.cpp @@ -1,226 +1,226 @@ -// SPDX-FileCopyrightText: 2022 Gary Wang -// -// SPDX-License-Identifier: MIT - -#include "settings.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace QEnumHelper -{ - template - E fromString(const QString &text, const E defaultValue) - { - bool ok; - E result = static_cast(QMetaEnum::fromType().keyToValue(text.toUtf8(), &ok)); - if (!ok) { - return defaultValue; - } - return result; - } - - template - QString toString(E value) - { - const int intValue = static_cast(value); - return QString::fromUtf8(QMetaEnum::fromType().valueToKey(intValue)); - } -} - -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(); -} - -bool Settings::loopGallery() const -{ - return m_qsettings->value("loop_gallery", true).toBool(); -} - -Settings::DoubleClickBehavior Settings::doubleClickBehavior() const -{ - QString result = m_qsettings->value("double_click_behavior", "Close").toString(); - - return QEnumHelper::fromString(result, DoubleClickBehavior::Close); -} - -Settings::MouseWheelBehavior Settings::mouseWheelBehavior() const -{ - QString result = m_qsettings->value("mouse_wheel_behavior", "Zoom").toString(); - - return QEnumHelper::fromString(result, MouseWheelBehavior::Zoom); -} - -Settings::WindowSizeBehavior Settings::initWindowSizeBehavior() const -{ - QString result = m_qsettings->value("init_window_size_behavior", "Auto").toString(); - - return QEnumHelper::fromString(result, WindowSizeBehavior::Auto); -} - -Qt::HighDpiScaleFactorRoundingPolicy Settings::hiDpiScaleFactorBehavior() const -{ - QString result = m_qsettings->value("hidpi_scale_factor_behavior", "PassThrough").toString(); - - return QEnumHelper::fromString(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); - m_qsettings->sync(); -} - -void Settings::setLoopGallery(bool on) -{ - m_qsettings->setValue("loop_gallery", 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)); - m_qsettings->sync(); -} - -void Settings::setInitWindowSizeBehavior(WindowSizeBehavior wsb) -{ - m_qsettings->setValue("init_window_size_behavior", QEnumHelper::toString(wsb)); - m_qsettings->sync(); -} - -void Settings::setHiDpiScaleFactorBehavior(Qt::HighDpiScaleFactorRoundingPolicy hidpi) -{ - m_qsettings->setValue("hidpi_scale_factor_behavior", QEnumHelper::toString(hidpi)); - m_qsettings->sync(); -} - -void Settings::applyUserShortcuts(QWidget *widget) -{ - m_qsettings->beginGroup("shortcuts"); - const QStringList shortcutNames = m_qsettings->allKeys(); - for (const QString & name : shortcutNames) { - QList shortcuts = m_qsettings->value(name).value>(); - setShortcutsForAction(widget, name, shortcuts, false); - } - m_qsettings->endGroup(); -} - -bool Settings::setShortcutsForAction(QWidget *widget, const QString &objectName, - QList 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 -// QCoreApplication::applicationDirPath() parses the "applicationDirPath" from arg0, which... -// 1. rely on a QApplication object instance -// but we need to call QGuiApplication::setHighDpiScaleFactorRoundingPolicy() before QApplication get created -// 2. arg0 is NOT garanteed to be the path of execution -// see also: https://stackoverflow.com/questions/383973/is-args0-guaranteed-to-be-the-path-of-execution -// This function is here mainly for #1. -QString getApplicationDirPath() -{ - WCHAR buffer[MAX_PATH]; - GetModuleFileNameW(NULL, buffer, MAX_PATH); - QString appPath = QString::fromWCharArray(buffer); - - return appPath.left(appPath.lastIndexOf('\\')); -} -#endif // defined(FLAG_PORTABLE_MODE_SUPPORT) && defined(Q_OS_WIN) - -Settings::Settings() - : QObject(qApp) -{ - QString configPath; - -#if defined(FLAG_PORTABLE_MODE_SUPPORT) && defined(Q_OS_WIN) - QString portableConfigDirPath = QDir(getApplicationDirPath()).absoluteFilePath("data"); - QFileInfo portableConfigDirInfo(portableConfigDirPath); - if (portableConfigDirInfo.exists() && portableConfigDirInfo.isDir() && portableConfigDirInfo.isWritable()) { - // we can use it. - configPath = portableConfigDirPath; - } -#endif // defined(FLAG_PORTABLE_MODE_SUPPORT) && defined(Q_OS_WIN) - - if (configPath.isEmpty()) { - // Should be %LOCALAPPDATA%\ under Windows, ~/.config/ under Linux. - configPath = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation); - } - - m_qsettings = new QSettings(QDir(configPath).absoluteFilePath("config.ini"), QSettings::IniFormat, this); - - qRegisterMetaType>(); -} - +// SPDX-FileCopyrightText: 2022 Gary Wang +// +// SPDX-License-Identifier: MIT + +#include "settings.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace QEnumHelper +{ + template + E fromString(const QString &text, const E defaultValue) + { + bool ok; + E result = static_cast(QMetaEnum::fromType().keyToValue(text.toUtf8(), &ok)); + if (!ok) { + return defaultValue; + } + return result; + } + + template + QString toString(E value) + { + const int intValue = static_cast(value); + return QString::fromUtf8(QMetaEnum::fromType().valueToKey(intValue)); + } +} + +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(); +} + +bool Settings::loopGallery() const +{ + return m_qsettings->value("loop_gallery", true).toBool(); +} + +Settings::DoubleClickBehavior Settings::doubleClickBehavior() const +{ + QString result = m_qsettings->value("double_click_behavior", "Close").toString(); + + return QEnumHelper::fromString(result, DoubleClickBehavior::Close); +} + +Settings::MouseWheelBehavior Settings::mouseWheelBehavior() const +{ + QString result = m_qsettings->value("mouse_wheel_behavior", "Zoom").toString(); + + return QEnumHelper::fromString(result, MouseWheelBehavior::Zoom); +} + +Settings::WindowSizeBehavior Settings::initWindowSizeBehavior() const +{ + QString result = m_qsettings->value("init_window_size_behavior", "Auto").toString(); + + return QEnumHelper::fromString(result, WindowSizeBehavior::Auto); +} + +Qt::HighDpiScaleFactorRoundingPolicy Settings::hiDpiScaleFactorBehavior() const +{ + QString result = m_qsettings->value("hidpi_scale_factor_behavior", "PassThrough").toString(); + + return QEnumHelper::fromString(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); + m_qsettings->sync(); +} + +void Settings::setLoopGallery(bool on) +{ + m_qsettings->setValue("loop_gallery", 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)); + m_qsettings->sync(); +} + +void Settings::setInitWindowSizeBehavior(WindowSizeBehavior wsb) +{ + m_qsettings->setValue("init_window_size_behavior", QEnumHelper::toString(wsb)); + m_qsettings->sync(); +} + +void Settings::setHiDpiScaleFactorBehavior(Qt::HighDpiScaleFactorRoundingPolicy hidpi) +{ + m_qsettings->setValue("hidpi_scale_factor_behavior", QEnumHelper::toString(hidpi)); + m_qsettings->sync(); +} + +void Settings::applyUserShortcuts(QWidget *widget) +{ + m_qsettings->beginGroup("shortcuts"); + const QStringList shortcutNames = m_qsettings->allKeys(); + for (const QString & name : shortcutNames) { + QList shortcuts = m_qsettings->value(name).value>(); + setShortcutsForAction(widget, name, shortcuts, false); + } + m_qsettings->endGroup(); +} + +bool Settings::setShortcutsForAction(QWidget *widget, const QString &objectName, + QList 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 +// QCoreApplication::applicationDirPath() parses the "applicationDirPath" from arg0, which... +// 1. rely on a QApplication object instance +// but we need to call QGuiApplication::setHighDpiScaleFactorRoundingPolicy() before QApplication get created +// 2. arg0 is NOT garanteed to be the path of execution +// see also: https://stackoverflow.com/questions/383973/is-args0-guaranteed-to-be-the-path-of-execution +// This function is here mainly for #1. +QString getApplicationDirPath() +{ + WCHAR buffer[MAX_PATH]; + GetModuleFileNameW(NULL, buffer, MAX_PATH); + QString appPath = QString::fromWCharArray(buffer); + + return appPath.left(appPath.lastIndexOf('\\')); +} +#endif // defined(FLAG_PORTABLE_MODE_SUPPORT) && defined(Q_OS_WIN) + +Settings::Settings() + : QObject(qApp) +{ + QString configPath; + +#if defined(FLAG_PORTABLE_MODE_SUPPORT) && defined(Q_OS_WIN) + QString portableConfigDirPath = QDir(getApplicationDirPath()).absoluteFilePath("data"); + QFileInfo portableConfigDirInfo(portableConfigDirPath); + if (portableConfigDirInfo.exists() && portableConfigDirInfo.isDir() && portableConfigDirInfo.isWritable()) { + // we can use it. + configPath = portableConfigDirPath; + } +#endif // defined(FLAG_PORTABLE_MODE_SUPPORT) && defined(Q_OS_WIN) + + if (configPath.isEmpty()) { + // Should be %LOCALAPPDATA%\ under Windows, ~/.config/ under Linux. + configPath = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation); + } + + m_qsettings = new QSettings(QDir(configPath).absoluteFilePath("config.ini"), QSettings::IniFormat, this); + + qRegisterMetaType>(); +} + diff --git a/app/settings.h b/app/settings.h index 80c4cd0..2ba1efa 100644 --- a/app/settings.h +++ b/app/settings.h @@ -1,69 +1,69 @@ -// SPDX-FileCopyrightText: 2022 Gary Wang -// -// SPDX-License-Identifier: MIT - -#pragma once - -#include -#include - -class Settings : public QObject -{ - Q_OBJECT -public: - enum DoubleClickBehavior { - Ignore, - Close, - Maximize, - FullScreen, - }; - Q_ENUM(DoubleClickBehavior) - - enum MouseWheelBehavior { - Zoom, - Switch, - }; - Q_ENUM(MouseWheelBehavior) - - enum WindowSizeBehavior { - Auto, - Maximized, - Windowed, - }; - Q_ENUM(WindowSizeBehavior) - - static Settings *instance(); - - bool stayOnTop() const; - bool useBuiltInCloseAnimation() const; - bool useLightCheckerboard() const; - bool loopGallery() 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 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 shortcuts, bool writeConfig = true); - -private: - Settings(); - - static Settings *m_settings_instance; - - QSettings *m_qsettings; - -signals: - -public slots: -}; +// SPDX-FileCopyrightText: 2022 Gary Wang +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include +#include + +class Settings : public QObject +{ + Q_OBJECT +public: + enum DoubleClickBehavior { + Ignore, + Close, + Maximize, + FullScreen, + }; + Q_ENUM(DoubleClickBehavior) + + enum MouseWheelBehavior { + Zoom, + Switch, + }; + Q_ENUM(MouseWheelBehavior) + + enum WindowSizeBehavior { + Auto, + Maximized, + Windowed, + }; + Q_ENUM(WindowSizeBehavior) + + static Settings *instance(); + + bool stayOnTop() const; + bool useBuiltInCloseAnimation() const; + bool useLightCheckerboard() const; + bool loopGallery() 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 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 shortcuts, bool writeConfig = true); + +private: + Settings(); + + static Settings *m_settings_instance; + + QSettings *m_qsettings; + +signals: + +public slots: +}; diff --git a/app/settingsdialog.cpp b/app/settingsdialog.cpp index f20490b..e7a2957 100644 --- a/app/settingsdialog.cpp +++ b/app/settingsdialog.cpp @@ -1,200 +1,200 @@ -// SPDX-FileCopyrightText: 2022 Gary Wang -// -// SPDX-License-Identifier: MIT - -#include "settingsdialog.h" - -#include "settings.h" -#include "shortcutedit.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -SettingsDialog::SettingsDialog(QWidget *parent) - : QDialog(parent) - , m_stayOnTop(new QCheckBox) - , m_useBuiltInCloseAnimation(new QCheckBox) - , m_useLightCheckerboard(new QCheckBox) - , m_loopGallery(new QCheckBox) - , m_doubleClickBehavior(new QComboBox) - , m_mouseWheelBehavior(new QComboBox) - , m_initWindowSizeBehavior(new QComboBox) - , m_hiDpiRoundingPolicyBehavior(new QComboBox) -{ - this->setWindowTitle(tr("Settings")); - - 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 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 > _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 > _mw_options { - { Settings::MouseWheelBehavior::Zoom, tr("Zoom in and out") }, - { Settings::MouseWheelBehavior::Switch, tr("View next or previous item") } - }; - - static QList< QPair > _iws_options { - { Settings::WindowSizeBehavior::Auto, tr("Auto size") }, - { Settings::WindowSizeBehavior::Maximized, tr("Maximized") }, - { Settings::WindowSizeBehavior::Windowed, tr("Windowed") } - }; - - static QList< QPair > _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 & dcOption : _dc_options) { - dcbDropDown.append(dcOption.second); - } - - QStringList mwbDropDown; - for (const QPair & mwOption : _mw_options) { - mwbDropDown.append(mwOption.second); - } - - QStringList iwsbDropDown; - for (const QPair & iwsOption : _iws_options) { - iwsbDropDown.append(iwsOption.second); - } - - QStringList hidpiDropDown; - for (const QPair & 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("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_doubleClickBehavior->setModel(new QStringListModel(dcbDropDown)); - Settings::DoubleClickBehavior dcb = Settings::instance()->doubleClickBehavior(); - m_doubleClickBehavior->setCurrentIndex(static_cast(dcb)); - m_mouseWheelBehavior->setModel(new QStringListModel(mwbDropDown)); - Settings::MouseWheelBehavior mwb = Settings::instance()->mouseWheelBehavior(); - m_mouseWheelBehavior->setCurrentIndex(static_cast(mwb)); - m_initWindowSizeBehavior->setModel(new QStringListModel(iwsbDropDown)); - Settings::WindowSizeBehavior iwsb = Settings::instance()->initWindowSizeBehavior(); - m_initWindowSizeBehavior->setCurrentIndex(static_cast(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; - } - } - -#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) -# define QCHECKBOX_CHECKSTATECHANGED QCheckBox::checkStateChanged -# define QT_CHECKSTATE Qt::CheckState -#else -# define QCHECKBOX_CHECKSTATECHANGED QCheckBox::stateChanged -# 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){ - Settings::instance()->setUseLightCheckerboard(state == Qt::Checked); - }); - - connect(m_loopGallery, &QCHECKBOX_CHECKSTATECHANGED, this, [ = ](QT_CHECKSTATE state){ - Settings::instance()->setLoopGallery(state == Qt::Checked); - }); - - connect(m_doubleClickBehavior, QOverload::of(&QComboBox::currentIndexChanged), this, [ = ](int index){ - Settings::instance()->setDoubleClickBehavior(_dc_options.at(index).first); - }); - - connect(m_mouseWheelBehavior, QOverload::of(&QComboBox::currentIndexChanged), this, [ = ](int index){ - Settings::instance()->setMouseWheelBehavior(_mw_options.at(index).first); - }); - - connect(m_initWindowSizeBehavior, QOverload::of(&QComboBox::currentIndexChanged), this, [ = ](int index){ - Settings::instance()->setInitWindowSizeBehavior(_iws_options.at(index).first); - }); - - connect(m_hiDpiRoundingPolicyBehavior, QOverload::of(&QComboBox::currentIndexChanged), this, [ = ](int index){ - Settings::instance()->setHiDpiScaleFactorBehavior(_hidpi_options.at(index).first); - }); - - adjustSize(); - setWindowFlag(Qt::WindowContextHelpButtonHint, false); -} - -SettingsDialog::~SettingsDialog() -{ - -} +// SPDX-FileCopyrightText: 2022 Gary Wang +// +// SPDX-License-Identifier: MIT + +#include "settingsdialog.h" + +#include "settings.h" +#include "shortcutedit.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +SettingsDialog::SettingsDialog(QWidget *parent) + : QDialog(parent) + , m_stayOnTop(new QCheckBox) + , m_useBuiltInCloseAnimation(new QCheckBox) + , m_useLightCheckerboard(new QCheckBox) + , m_loopGallery(new QCheckBox) + , m_doubleClickBehavior(new QComboBox) + , m_mouseWheelBehavior(new QComboBox) + , m_initWindowSizeBehavior(new QComboBox) + , m_hiDpiRoundingPolicyBehavior(new QComboBox) +{ + this->setWindowTitle(tr("Settings")); + + 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 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 > _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 > _mw_options { + { Settings::MouseWheelBehavior::Zoom, tr("Zoom in and out") }, + { Settings::MouseWheelBehavior::Switch, tr("View next or previous item") } + }; + + static QList< QPair > _iws_options { + { Settings::WindowSizeBehavior::Auto, tr("Auto size") }, + { Settings::WindowSizeBehavior::Maximized, tr("Maximized") }, + { Settings::WindowSizeBehavior::Windowed, tr("Windowed") } + }; + + static QList< QPair > _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 & dcOption : _dc_options) { + dcbDropDown.append(dcOption.second); + } + + QStringList mwbDropDown; + for (const QPair & mwOption : _mw_options) { + mwbDropDown.append(mwOption.second); + } + + QStringList iwsbDropDown; + for (const QPair & iwsOption : _iws_options) { + iwsbDropDown.append(iwsOption.second); + } + + QStringList hidpiDropDown; + for (const QPair & 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("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_doubleClickBehavior->setModel(new QStringListModel(dcbDropDown)); + Settings::DoubleClickBehavior dcb = Settings::instance()->doubleClickBehavior(); + m_doubleClickBehavior->setCurrentIndex(static_cast(dcb)); + m_mouseWheelBehavior->setModel(new QStringListModel(mwbDropDown)); + Settings::MouseWheelBehavior mwb = Settings::instance()->mouseWheelBehavior(); + m_mouseWheelBehavior->setCurrentIndex(static_cast(mwb)); + m_initWindowSizeBehavior->setModel(new QStringListModel(iwsbDropDown)); + Settings::WindowSizeBehavior iwsb = Settings::instance()->initWindowSizeBehavior(); + m_initWindowSizeBehavior->setCurrentIndex(static_cast(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; + } + } + +#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) +# define QCHECKBOX_CHECKSTATECHANGED QCheckBox::checkStateChanged +# define QT_CHECKSTATE Qt::CheckState +#else +# define QCHECKBOX_CHECKSTATECHANGED QCheckBox::stateChanged +# 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){ + Settings::instance()->setUseLightCheckerboard(state == Qt::Checked); + }); + + connect(m_loopGallery, &QCHECKBOX_CHECKSTATECHANGED, this, [ = ](QT_CHECKSTATE state){ + Settings::instance()->setLoopGallery(state == Qt::Checked); + }); + + connect(m_doubleClickBehavior, QOverload::of(&QComboBox::currentIndexChanged), this, [ = ](int index){ + Settings::instance()->setDoubleClickBehavior(_dc_options.at(index).first); + }); + + connect(m_mouseWheelBehavior, QOverload::of(&QComboBox::currentIndexChanged), this, [ = ](int index){ + Settings::instance()->setMouseWheelBehavior(_mw_options.at(index).first); + }); + + connect(m_initWindowSizeBehavior, QOverload::of(&QComboBox::currentIndexChanged), this, [ = ](int index){ + Settings::instance()->setInitWindowSizeBehavior(_iws_options.at(index).first); + }); + + connect(m_hiDpiRoundingPolicyBehavior, QOverload::of(&QComboBox::currentIndexChanged), this, [ = ](int index){ + Settings::instance()->setHiDpiScaleFactorBehavior(_hidpi_options.at(index).first); + }); + + adjustSize(); + setWindowFlag(Qt::WindowContextHelpButtonHint, false); +} + +SettingsDialog::~SettingsDialog() +{ + +} diff --git a/app/settingsdialog.h b/app/settingsdialog.h index 9c3fc56..f11c650 100644 --- a/app/settingsdialog.h +++ b/app/settingsdialog.h @@ -1,35 +1,35 @@ -// SPDX-FileCopyrightText: 2022 Gary Wang -// -// SPDX-License-Identifier: MIT - -#ifndef SETTINGSDIALOG_H -#define SETTINGSDIALOG_H - -#include -#include - -class QCheckBox; -class QComboBox; -class SettingsDialog : public QDialog -{ - Q_OBJECT -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; - QComboBox * m_doubleClickBehavior = nullptr; - QComboBox * m_mouseWheelBehavior = nullptr; - QComboBox * m_initWindowSizeBehavior = nullptr; - QComboBox * m_hiDpiRoundingPolicyBehavior = nullptr; -}; - -#endif // SETTINGSDIALOG_H +// SPDX-FileCopyrightText: 2022 Gary Wang +// +// SPDX-License-Identifier: MIT + +#ifndef SETTINGSDIALOG_H +#define SETTINGSDIALOG_H + +#include +#include + +class QCheckBox; +class QComboBox; +class SettingsDialog : public QDialog +{ + Q_OBJECT +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; + QComboBox * m_doubleClickBehavior = nullptr; + QComboBox * m_mouseWheelBehavior = nullptr; + QComboBox * m_initWindowSizeBehavior = nullptr; + QComboBox * m_hiDpiRoundingPolicyBehavior = nullptr; +}; + +#endif // SETTINGSDIALOG_H diff --git a/app/toolbutton.cpp b/app/toolbutton.cpp index 5a98e78..db3778b 100644 --- a/app/toolbutton.cpp +++ b/app/toolbutton.cpp @@ -1,38 +1,38 @@ -// SPDX-FileCopyrightText: 2022 Gary Wang -// -// SPDX-License-Identifier: MIT - -#include "toolbutton.h" - -#include "actionmanager.h" -#include "opacityhelper.h" - -#include -#include -#include - -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); -} +// SPDX-FileCopyrightText: 2022 Gary Wang +// +// SPDX-License-Identifier: MIT + +#include "toolbutton.h" + +#include "actionmanager.h" +#include "opacityhelper.h" + +#include +#include +#include + +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); +} diff --git a/app/toolbutton.h b/app/toolbutton.h index 8c62f67..e53b70f 100644 --- a/app/toolbutton.h +++ b/app/toolbutton.h @@ -1,25 +1,25 @@ -// SPDX-FileCopyrightText: 2022 Gary Wang -// -// SPDX-License-Identifier: MIT - -#ifndef TOOLBUTTON_H -#define TOOLBUTTON_H - -#include - -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 +// SPDX-FileCopyrightText: 2022 Gary Wang +// +// SPDX-License-Identifier: MIT + +#ifndef TOOLBUTTON_H +#define TOOLBUTTON_H + +#include + +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 diff --git a/dist/MacOSXBundleInfo.plist.in b/dist/MacOSXBundleInfo.plist.in index 5afc6a3..aa1e8f8 100644 --- a/dist/MacOSXBundleInfo.plist.in +++ b/dist/MacOSXBundleInfo.plist.in @@ -1,136 +1,136 @@ - - - - - CFBundleDevelopmentRegion - English - CFBundleExecutable - ${MACOSX_BUNDLE_EXECUTABLE_NAME} - CFBundleGetInfoString - ${MACOSX_BUNDLE_INFO_STRING} - CFBundleIconFile - ${MACOSX_BUNDLE_ICON_FILE} - CFBundleIdentifier - ${MACOSX_BUNDLE_GUI_IDENTIFIER} - CFBundleInfoDictionaryVersion - 6.0 - CFBundleLongVersionString - ${MACOSX_BUNDLE_LONG_VERSION_STRING} - CFBundleName - ${MACOSX_BUNDLE_BUNDLE_NAME} - CFBundlePackageType - APPL - CFBundleShortVersionString - ${MACOSX_BUNDLE_SHORT_VERSION_STRING} - CFBundleSignature - ???? - CFBundleVersion - ${MACOSX_BUNDLE_BUNDLE_VERSION} - CSResourcesFileMapped - - NSHumanReadableCopyright - ${MACOSX_BUNDLE_COPYRIGHT} - - CFBundleLocalizations - - en - ca - de - es - fr - id - it - ja - ko - nb_NO - nl - pa_PK - ru - si - ta - tr - uk - zh_CN - - CFBundleDocumentTypes - - - - CFBundleTypeName - JPEG Image - CFBundleTypeExtensions - - jpg - jpeg - jfif - - CFBundleTypeMIMETypes - - image/jpeg - - CFBundleTypeRole - Viewer - - - - CFBundleTypeName - PNG Image - CFBundleTypeExtensions - - png - - CFBundleTypeMIMETypes - - image/png - - CFBundleTypeRole - Viewer - - - - CFBundleTypeName - WebP Image - CFBundleTypeExtensions - - webp - - CFBundleTypeMIMETypes - - image/webp - - CFBundleTypeRole - Viewer - - - - CFBundleTypeName - GIF Image - CFBundleTypeExtensions - - gif - - CFBundleTypeMIMETypes - - image/gif - - CFBundleTypeRole - Viewer - - - - CFBundleTypeName - SVG Image - CFBundleTypeExtensions - - svg - - CFBundleTypeMIMETypes - - image/svg+xml - - CFBundleTypeRole - Viewer - - - - + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + ${MACOSX_BUNDLE_EXECUTABLE_NAME} + CFBundleGetInfoString + ${MACOSX_BUNDLE_INFO_STRING} + CFBundleIconFile + ${MACOSX_BUNDLE_ICON_FILE} + CFBundleIdentifier + ${MACOSX_BUNDLE_GUI_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleLongVersionString + ${MACOSX_BUNDLE_LONG_VERSION_STRING} + CFBundleName + ${MACOSX_BUNDLE_BUNDLE_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + ${MACOSX_BUNDLE_SHORT_VERSION_STRING} + CFBundleSignature + ???? + CFBundleVersion + ${MACOSX_BUNDLE_BUNDLE_VERSION} + CSResourcesFileMapped + + NSHumanReadableCopyright + ${MACOSX_BUNDLE_COPYRIGHT} + + CFBundleLocalizations + + en + ca + de + es + fr + id + it + ja + ko + nb_NO + nl + pa_PK + ru + si + ta + tr + uk + zh_CN + + CFBundleDocumentTypes + + + + CFBundleTypeName + JPEG Image + CFBundleTypeExtensions + + jpg + jpeg + jfif + + CFBundleTypeMIMETypes + + image/jpeg + + CFBundleTypeRole + Viewer + + + + CFBundleTypeName + PNG Image + CFBundleTypeExtensions + + png + + CFBundleTypeMIMETypes + + image/png + + CFBundleTypeRole + Viewer + + + + CFBundleTypeName + WebP Image + CFBundleTypeExtensions + + webp + + CFBundleTypeMIMETypes + + image/webp + + CFBundleTypeRole + Viewer + + + + CFBundleTypeName + GIF Image + CFBundleTypeExtensions + + gif + + CFBundleTypeMIMETypes + + image/gif + + CFBundleTypeRole + Viewer + + + + CFBundleTypeName + SVG Image + CFBundleTypeExtensions + + svg + + CFBundleTypeMIMETypes + + image/svg+xml + + CFBundleTypeRole + Viewer + + + + diff --git a/pineapple-pictures.pro b/pineapple-pictures.pro index d088f0b..ae31466 100644 --- a/pineapple-pictures.pro +++ b/pineapple-pictures.pro @@ -1,101 +1,101 @@ -# SPDX-FileCopyrightText: 2025 Gary Wang -# -# 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 convert -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 -} +# SPDX-FileCopyrightText: 2025 Gary Wang +# +# 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 convert -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 +}