Compare commits

...
19 Commits
Author SHA1 Message Date
yyc12345 e01718928c feat: remove checkerboard icon and change bottom toolbar 2026-07-25 12:23:00 +08:00
yyc12345 f5dbf44ec7 refactor: add possible optimization in graphics view 2026-07-25 12:15:17 +08:00
yyc12345 f55f8c731d refactor: use extensible design for fetching playlist from single file 2026-07-25 12:06:11 +08:00
yyc12345 4ed491c78a refactor: remove closeWindow because no one need it 2026-07-24 15:17:05 +08:00
yyc12345 b54cde2cbf refactor: remove "auto" in window-size behavior
- "auto" is not essential for our hard fork, so i simply remove it.
- remove all useless code after removing it.
- enlarge the min size of window because previous may trigger bad layout when showing navigation view.
2026-07-24 15:14:24 +08:00
yyc12345 b1d12cc406 refactor: remove double-click-window behavior
- remove double-click-window behavior entirely
- remove the icon of copy image action
2026-07-24 15:07:30 +08:00
yyc12345 aa13355966 refactor: use UI file for main window
- main window now use UI file for layout.
- remove all manual computed widget position code. use full Qt layout implementation instead.
- remove useless functions in main window
- change style changed slot as private
2026-07-24 14:57:41 +08:00
yyc12345 bd35a8c3e0 feat: use submenu in right click menu to reduce the height of menu panel 2026-07-15 21:59:30 +08:00
yyc12345 81413b51da feat: support explorer sort and hidden on Window
now playlist can use Windows Explorer's sort and hidden file display options to build it own for better browse experience on Windows
2026-07-14 13:22:23 +08:00
yyc12345 a01d640c27 refactor: remove frameless window
remove frameless window and merge it into mainwindow. because we do not need frameless and transparent window, so most of its functions are removed in previous commits.
2026-07-13 15:59:39 +08:00
yyc12345 16800df138 refactor: remove useless function in main window 2026-07-13 15:50:30 +08:00
yyc12345 7e8fd4ea21 refactor: use class member menu instead of runtime created menu 2026-07-13 15:47:17 +08:00
yyc12345 04fa491cd8 refactor: fully refactor playlist manager
- refactor playlist manager. remove useless qt model design.
- update action updator function.
2026-07-13 15:40:59 +08:00
yyc12345 dbf8c09368 feat: update action manager
- add and delete some actions in action manager.
- rename "fit to view" to "fit to screen".
- update main window right click menu and toolbar.
- use special state update slot in action manager instead of function located in main window.
2026-07-12 21:00:11 +08:00
yyc12345 369a17ed7d fix: fix typo about checkerboard 2026-07-12 19:54:37 +08:00
yyc12345 b19a4f8723 doc: add help for checkboard shift modifier 2026-07-09 13:06:14 +08:00
yyc12345 a8e5464e9f fix: remove useless mouse tracking setting 2026-07-08 20:18:52 +08:00
yyc12345 46f46480cb feat: change application style
- change default dark theme to light
- disable dark mode on windows 10 because Qt's windows 11 theme has bad support on windows 10 (icon display error)
- level required minimum qt version up to 6.5 for color schema.
2026-07-08 20:15:22 +08:00
yyc12345 c06f900ac5 refactor: use ui file to replace manual layout in metadata dialog 2026-07-07 12:16:36 +08:00
36 changed files with 1686 additions and 1087 deletions
+15 -3
View File
@@ -20,7 +20,7 @@ set (CMAKE_AUTOUIC ON)
find_package(QT NAMES Qt6 REQUIRED COMPONENTS Core)
set (QT_MINIMUM_VERSION "6.4")
set (QT_MINIMUM_VERSION "6.5")
find_package(Qt${QT_VERSION_MAJOR} ${QT_MINIMUM_VERSION} REQUIRED
COMPONENTS Widgets Svg SvgWidgets LinguistTools
@@ -39,7 +39,6 @@ endif ()
set (PPIC_CPP_FILES
app/main.cpp
app/framelesswindow.cpp
app/mainwindow.cpp
app/actionmanager.cpp
app/graphicsview.cpp
@@ -54,10 +53,12 @@ set (PPIC_CPP_FILES
app/exiv2wrapper.cpp
app/playlistmanager.cpp
app/fileopeneventhandler.cpp
app/playlistscout.cpp
app/playlistscouts/qtscout.cpp
app/playlistscouts/explorerscout.cpp
)
set (PPIC_HEADER_FILES
app/framelesswindow.h
app/mainwindow.h
app/actionmanager.h
app/graphicsview.h
@@ -72,10 +73,16 @@ set (PPIC_HEADER_FILES
app/exiv2wrapper.h
app/playlistmanager.h
app/fileopeneventhandler.h
app/playlistscout.h
app/playlistscouts/qtscout.h
app/playlistscouts/explorerscout.h
)
set (PPIC_UI_FILES
app/settingsdialog.ui
app/aboutdialog.ui
app/metadatadialog.ui
app/mainwindow.ui
)
set (PPIC_QRC_FILES
@@ -117,6 +124,11 @@ else()
qt_add_translations(${EXE_NAME} ${ADD_TRANSLATIONS_ADDITIONAL_ARGS} TS_FILES ${PPIC_TS_FILES} QM_FILES_OUTPUT_VARIABLE PPIC_QM_FILES)
endif()
# YYC MARK:
# This is essential, otherwise Qt UIC generated UI header file
# can not find our header files if there are custom widgets inside UI file.
target_include_directories(${EXE_NAME} PRIVATE app/)
target_sources(${EXE_NAME} PRIVATE ${PPIC_QM_FILES})
target_link_libraries (${EXE_NAME} Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Svg Qt${QT_VERSION_MAJOR}::SvgWidgets)
+4 -2
View File
@@ -18,7 +18,7 @@ static QString loadEmbeddedHtml(QString&& path) {
}
AboutDialog::AboutDialog(QWidget *parent)
: QDialog(parent), ui(new Ui::AboutDialog)
: QDialog(parent), ui(new QT_PREPEND_NAMESPACE(Ui::AboutDialog))
{
ui->setupUi(this);
setWindowFlag(Qt::WindowContextHelpButtonHint, false);
@@ -35,7 +35,9 @@ AboutDialog::AboutDialog(QWidget *parent)
.arg(tr("None of the operations in this application will alter the pictures on disk."))
.arg(tr("Context Menu Help"))
.arg(tr("Keep transformation", "The same meaning of MainWindow's Keep transformation."))
.arg(tr("Avoid resetting the zoom/rotation/flip state that was applied to the image view when switching between images."));
.arg(tr("Avoid resetting the zoom/rotation/flip state that was applied to the image view when switching between images."))
.arg(tr("Checkerboard", "The same meaning of MainWindow's Checkerboard."))
.arg(tr("Checkerboard light and dark theme can be adjusted in the configuration window, or easily switch it by holding Shift and clicking the button."));
const QString APP_NAME_ARG = qApp->applicationDisplayName();
const QString APP_VERSION_ARG =
-1
View File
@@ -7,7 +7,6 @@
#include <QDialog>
QT_BEGIN_NAMESPACE
namespace Ui {
class AboutDialog;
+116 -63
View File
@@ -4,7 +4,11 @@
#include "actionmanager.h"
#include "settings.h"
#include "mainwindow.h"
#include "playlistmanager.h"
#include "graphicsview.h"
#include "graphicsscene.h"
#include <QGuiApplication>
#include <QSvgRenderer>
@@ -25,6 +29,8 @@ void ActionManager::setupAction(MainWindow *mainWindow)
{
// region: Action and Its Icon Creation
// Declare some convenient macros and lambdas.
#define ICON_NAME(name)\
QStringLiteral(":/icons/" #name ".svg")
@@ -39,40 +45,51 @@ void ActionManager::setupAction(MainWindow *mainWindow)
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);
/// This macro for register normal action.
#define CREATE_NEW_ACTION(w, a) create_action(w, &a, QString(), ACTION_NAME(a))
/// This macro for register Qt theme action.
#define CREATE_NEW_THEMEICON_ACTION(w, a, i) create_action(w, &a, QLatin1String(STRIFY(i)), ACTION_NAME(a), true)
/// This macro for register action with our custom icon.
#define CREATE_NEW_ICON_ACTION(w, a, i) create_action(w, &a, ICON_NAME(i), ACTION_NAME(a), false)
// TODO: We may replace all Qt theme icons to our custom icon for visual consistency.
// TODO: Some icons are invalid.
CREATE_NEW_ACTION(mainWindow, actionCopyPixmap);
CREATE_NEW_ACTION(mainWindow, actionCopyFilePath);
CREATE_NEW_ICON_ACTION(mainWindow, actionPrevPicture, go-previous);
CREATE_NEW_ICON_ACTION(mainWindow, actionNextPicture, go-next);
#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_ICON_ACTION(mainWindow, actionActualSize, zoom-original);
CREATE_NEW_ICON_ACTION(mainWindow, actionFitToScreen, app-icon);
CREATE_NEW_ICON_ACTION(mainWindow, actionFitByWidth, app-icon);
CREATE_NEW_ICON_ACTION(mainWindow, actionFitByHeight, app-icon);
CREATE_NEW_ICON_ACTION(mainWindow, actionZoomIn, zoom-in);
CREATE_NEW_ICON_ACTION(mainWindow, actionZoomOut, zoom-out);
CREATE_NEW_ICON_ACTION(mainWindow, actionFlipHorizontal, app-icon);
CREATE_NEW_ICON_ACTION(mainWindow, actionFlipVertical, app-icon);
CREATE_NEW_ICON_ACTION(mainWindow, actionRotateClockwise, object-rotate-right);
CREATE_NEW_ICON_ACTION(mainWindow, actionRotateCounterClockwise, app-icon);
CREATE_NEW_ACTION(mainWindow, actionToggleAvoidResetTransform);
CREATE_NEW_ACTION(mainWindow, actionToggleCheckerboard);
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, actionToggleAvoidResetTransform);
CREATE_NEW_ACTION(mainWindow, actionSettings);
CREATE_NEW_THEMEICON_ACTION(mainWindow, actionHelp, system-help);
CREATE_NEW_THEMEICON_ACTION(mainWindow, actionLocateInFileManager, system-file-manager);
CREATE_NEW_THEMEICON_ACTION(mainWindow, actionProperties, document-properties);
CREATE_NEW_ACTION(mainWindow, actionQuitApp);
#undef CREATE_NEW_ACTION
CREATE_NEW_ACTION(mainWindow, actionSettings);
CREATE_NEW_THEMEICON_ACTION(mainWindow, actionHelp, system-help);
// Remove convenient macros
#undef CREATE_NEW_ICON_ACTION
#undef CREATE_NEW_THEMEICON_ACTION
#undef CREATE_NEW_ACTION
#undef STRIFY
#undef ACTION_NAME
@@ -80,34 +97,40 @@ void ActionManager::setupAction(MainWindow *mainWindow)
// endregion
// region: Setup Checkable Action
actionToggleAvoidResetTransform->setCheckable(true);
actionToggleCheckerboard->setCheckable(true);
actionTogglePauseAnimation->setCheckable(true);
// endregion
// region: Action Translatable Text Setup
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));
actionCopyPixmap->setText(QCoreApplication::translate("MainWindow", "Copy P&ixmap", nullptr));
actionCopyFilePath->setText(QCoreApplication::translate("MainWindow", "Copy &File Path", nullptr));
actionPrevPicture->setText(QCoreApplication::translate("MainWindow", "Previous image", nullptr));
actionNextPicture->setText(QCoreApplication::translate("MainWindow", "Next image", nullptr));
actionActualSize->setText(QCoreApplication::translate("MainWindow", "Actual size", nullptr));
actionFitToScreen->setText(QCoreApplication::translate("MainWindow", "Fit to screen", nullptr));
actionFitByWidth->setText(QCoreApplication::translate("MainWindow", "Fit by width", nullptr));
actionFitByHeight->setText(QCoreApplication::translate("MainWindow", "Fit by height", nullptr));
actionZoomIn->setText(QCoreApplication::translate("MainWindow", "Zoom in", nullptr));
actionZoomOut->setText(QCoreApplication::translate("MainWindow", "Zoom out", nullptr));
actionFlipHorizontal->setText(QCoreApplication::translate("MainWindow", "Flip &Horizontally", nullptr));
actionFlipVertical->setText(QCoreApplication::translate("MainWindow", "Flip &Vertically", nullptr));
actionRotateClockwise->setText(QCoreApplication::translate("MainWindow", "Rotate right", nullptr));
actionRotateCounterClockwise->setText(QCoreApplication::translate("MainWindow", "Rotate left", nullptr));
actionToggleAvoidResetTransform->setText(QCoreApplication::translate("MainWindow", "Keep transformation", "The 'transformation' means the flip/rotation status that currently applied to the image view"));
actionToggleCheckerboard->setText(QCoreApplication::translate("MainWindow", "Toggle Checkerboard", nullptr));
actionTogglePauseAnimation->setText(QCoreApplication::translate("MainWindow", "Pause/Resume Animation", nullptr));
actionAnimationNextFrame->setText(QCoreApplication::translate("MainWindow", "Animation Go to Next Frame", nullptr));
actionHorizontalFlip->setText(QCoreApplication::translate("MainWindow", "Flip &Horizontally", nullptr));
actionFitInView->setText(QCoreApplication::translate("MainWindow", "Fit to view", nullptr));
actionFitByWidth->setText(QCoreApplication::translate("MainWindow", "Fit to width", nullptr));
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));
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(
@@ -119,8 +142,9 @@ void ActionManager::setupAction(MainWindow *mainWindow)
actionLocateInFileManager->setText(QCoreApplication::translate("MainWindow", "Show in directory", nullptr));
#endif // Q_OS_WIN
actionProperties->setText(QCoreApplication::translate("MainWindow", "Properties", nullptr));
actionQuitApp->setText(QCoreApplication::translate("MainWindow", "Quit", nullptr));
actionSettings->setText(QCoreApplication::translate("MainWindow", "Configure...", nullptr));
actionHelp->setText(QCoreApplication::translate("MainWindow", "Help", nullptr));
// endregion
QMetaObject::connectSlotsByName(mainWindow);
@@ -128,34 +152,63 @@ void ActionManager::setupAction(MainWindow *mainWindow)
void ActionManager::setupShortcuts()
{
actionOpen->setShortcut(QKeySequence::Open);
actionCopyPixmap->setShortcut(QKeySequence::Copy);
actionPrevPicture->setShortcuts({
QKeySequence(Qt::Key_Left),
QKeySequence(Qt::Key_PageUp),
});
actionNextPicture->setShortcuts({
QKeySequence(Qt::Key_Right),
QKeySequence(Qt::Key_PageDown),
});
actionActualSize->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_0));
actionZoomIn->setShortcut(QKeySequence::ZoomIn);
actionZoomOut->setShortcut(QKeySequence::ZoomOut);
actionPrevPicture->setShortcuts({
QKeySequence(Qt::Key_PageUp),
QKeySequence(Qt::Key_Left),
});
actionNextPicture->setShortcuts({
QKeySequence(Qt::Key_PageDown),
QKeySequence(Qt::Key_Right),
});
actionHorizontalFlip->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_R));
actionCopyPixmap->setShortcut(QKeySequence::Copy);
actionPaste->setShortcut(QKeySequence::Paste);
actionTrash->setShortcut(QKeySequence::Delete);
actionHelp->setShortcut(QKeySequence::HelpContents);
actionSettings->setShortcut(QKeySequence::Preferences);
actionProperties->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_I));
actionQuitApp->setShortcuts({
QKeySequence(Qt::Key_Space),
QKeySequence(Qt::Key_Escape)
});
actionSettings->setShortcut(QKeySequence::Preferences);
actionHelp->setShortcut(QKeySequence::HelpContents);
}
void ActionManager::enablePrevNextPictureAction(bool enablePrevPictureAction, bool enableNextPictureAction)
void ActionManager::updateActionState(PlaylistManager* pm, GraphicsView* gv)
{
actionPrevPicture->setEnabled(enablePrevPictureAction);
actionNextPicture->setEnabled(enableNextPictureAction);
const auto* gs = gv->scene();
const auto* settings = Settings::instance();
// Update copy actions
bool canCopy = pm->currentIndex().has_value();
actionCopyPixmap->setEnabled(canCopy);
actionCopyFilePath->setEnabled(canCopy);
// Update Prev/Next Picture State
const int galleryFileCount = pm->totalCount();
const bool loopGallery = settings->loopGallery();
actionPrevPicture->setEnabled(galleryFileCount > 1 && (loopGallery || !pm->isFirstIndex()));
actionNextPicture->setEnabled(galleryFileCount > 1 && (loopGallery || !pm->isLastIndex()));
// Update Avoid Reset Transform
actionToggleAvoidResetTransform->setChecked(gv->isAvoidResetTransform());
// Update Checkerboard
actionToggleCheckerboard->setChecked(gv->isCheckerboard());
// Update Animation Graphics Action
bool canPauseAnimation = gs->canPauseAnimation();
actionTogglePauseAnimation->setEnabled(canPauseAnimation);
if (canPauseAnimation) {
actionTogglePauseAnimation->setChecked(gs->isPauseAnimation());
} else {
actionTogglePauseAnimation->setChecked(false);
}
bool canSkipAnimation = gs->canSkipAnimationFrame();
actionAnimationNextFrame->setEnabled(canSkipAnimation);
// Update trash, located in explorer and properties.
bool hasLoadedFile = pm->currentIndex().has_value();
actionTrash->setEnabled(hasLoadedFile);
actionLocateInFileManager->setEnabled(hasLoadedFile);
actionProperties->setEnabled(hasLoadedFile);
}
+24 -20
View File
@@ -8,6 +8,8 @@
#include <QAction>
class MainWindow;
class PlaylistManager;
class GraphicsView;
/// Action Manager is separated from Main Window
/// to manage all actions specifically.
@@ -19,38 +21,40 @@ public:
void setupAction(MainWindow * mainWindow);
void setupShortcuts();
void enablePrevNextPictureAction(bool enablePrevPictureAction, bool enableNextPictureAction);
public slots:
void updateActionState(PlaylistManager* pm, GraphicsView* gv);
public:
QAction *actionOpen;
QAction *actionActualSize;
QAction *actionToggleMaximize;
QAction *actionZoomIn;
QAction *actionZoomOut;
QAction *actionToggleCheckerboard;
QAction *actionRotateClockwise;
QAction *actionRotateCounterClockwise;
QAction *actionCopyPixmap;
QAction *actionCopyFilePath;
QAction *actionPrevPicture;
QAction *actionNextPicture;
QAction *actionActualSize;
QAction *actionFitToScreen;
QAction *actionFitByWidth;
QAction *actionFitByHeight;
QAction *actionZoomIn;
QAction *actionZoomOut;
QAction *actionFlipHorizontal;
QAction *actionFlipVertical;
QAction *actionRotateClockwise;
QAction *actionRotateCounterClockwise;
QAction *actionToggleAvoidResetTransform;
QAction *actionToggleCheckerboard;
QAction *actionTogglePauseAnimation;
QAction *actionAnimationNextFrame;
QAction *actionHorizontalFlip;
QAction *actionFitInView;
QAction *actionFitByWidth;
QAction *actionCopyPixmap;
QAction *actionCopyFilePath;
QAction *actionPaste;
QAction *actionTrash;
QAction *actionToggleAvoidResetTransform;
QAction *actionSettings;
QAction *actionHelp;
QAction *actionLocateInFileManager;
QAction *actionProperties;
QAction *actionQuitApp;
QAction *actionSettings;
QAction *actionHelp;
};
#endif // ACTIONMANAGER_H
+21 -25
View File
@@ -8,42 +8,38 @@
#include <QVBoxLayout>
#include <QDebug>
BottomButtonGroup::BottomButtonGroup(const std::vector<QAction *> &actionList, QWidget *parent)
BottomButtonGroup::BottomButtonGroup(QWidget *parent)
: QGroupBox (parent)
{
QHBoxLayout * mainLayout = new QHBoxLayout(this);
mainLayout->setSizeConstraint(QLayout::SetFixedSize);
this->setLayout(mainLayout);
this->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
this->setStyleSheet("BottomButtonGroup {"
"border: 1px solid gray;"
"border-top-left-radius: 10px;"
"border-top-right-radius: 10px;"
"border-style: none;"
"background-color:rgba(0,0,0,120)"
"}"
"QToolButton {"
"background:transparent;"
"}"
"QToolButton:!focus {"
"border-style: none;"
"}");
this->setStyleSheet(R"#(
BottomButtonGroup {
border-style: none;
background: transparent;
}
QToolButton {
background: transparent;
border-style: none;
}
)#");
}
auto newActionBtn = [this](QAction * action) -> QToolButton * {
void BottomButtonGroup::addActions(const std::vector<QAction *> & actionList) {
for (QAction * action : actionList) {
// Create button
QToolButton * btn = new QToolButton(this);
btn->setDefaultAction(action);
btn->setIconSize(QSize(32, 32));
btn->setFixedSize(40, 40);
return btn;
};
for (QAction * action : actionList) {
addButton(newActionBtn(action));
// Add button
layout()->addWidget(btn);
}
}
void BottomButtonGroup::addButton(QAbstractButton *button)
{
layout()->addWidget(button);
updateGeometry();
}
void BottomButtonGroup::addAction(QAction* action) {
addActions({action});
}
+3 -2
View File
@@ -14,9 +14,10 @@ class BottomButtonGroup : public QGroupBox
{
Q_OBJECT
public:
explicit BottomButtonGroup(const std::vector<QAction *> & actionList, QWidget *parent = nullptr);
explicit BottomButtonGroup(QWidget *parent = nullptr);
void addButton(QAbstractButton *button);
void addActions(const std::vector<QAction *> & actionList);
void addAction(QAction* action);
};
#endif // BOTTOMBUTTONGROUP_H
-36
View File
@@ -1,36 +0,0 @@
// SPDX-FileCopyrightText: 2022 Gary Wang <wzc782970009@gmail.com>
// SPDX-FileCopyrightText: 2023 Tad Young <yyc12321@outlook.com>
//
// SPDX-License-Identifier: MIT
#include "framelesswindow.h"
#include <QMouseEvent>
#include <QHoverEvent>
#include <QApplication>
#include <QVBoxLayout>
#include <QWindow>
FramelessWindow::FramelessWindow(QWidget *parent)
: QWidget(parent)
, m_centralLayout(new QVBoxLayout(this))
{
this->setWindowFlags(Qt::Window | Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::WindowMinMaxButtonsHint | Qt::WindowCloseButtonHint);
// YYC MARK:
// 72 px is the height of Windows Photo Viewer's bottom line.
// TODO: This value may be declared as a constant.
m_centralLayout->setContentsMargins(QMargins(0, 0, 0, 72));
}
void FramelessWindow::setCentralWidget(QWidget *widget)
{
if (m_centralWidget) {
m_centralLayout->removeWidget(m_centralWidget);
m_centralWidget->deleteLater();
}
m_centralLayout->addWidget(widget);
m_centralWidget = widget;
}
-27
View File
@@ -1,27 +0,0 @@
// SPDX-FileCopyrightText: 2022 Gary Wang <wzc782970009@gmail.com>
//
// SPDX-License-Identifier: MIT
#ifndef FRAMELESSWINDOW_H
#define FRAMELESSWINDOW_H
#include <QWidget>
QT_BEGIN_NAMESPACE
class QVBoxLayout;
QT_END_NAMESPACE
class FramelessWindow : public QWidget
{
Q_OBJECT
public:
explicit FramelessWindow(QWidget *parent = nullptr);
void setCentralWidget(QWidget * widget);
private:
QVBoxLayout * m_centralLayout = nullptr;
QWidget * m_centralWidget = nullptr; // just a pointer, doesn't take the ownership.
};
#endif // FRAMELESSWINDOW_H
+85 -13
View File
@@ -6,6 +6,9 @@
#include "settings.h"
#include <QStyleHints>
#include <QOperatingSystemVersion>
#include <QGuiApplication>
#include <QGraphicsSceneMouseEvent>
#include <QMimeData>
#include <QDebug>
@@ -16,6 +19,33 @@
#include <QMovie>
#include <QPainter>
static const QColor& ColorSchemaToTextForeground(Qt::ColorScheme colorScheme) {
// YYC MARK:
// If we are using Windows 10 and lower system or Qt which do not support "Windows 11"
// theme (lower than Qt 6.7), we should only support light mode.
// Because Qt's dark-mode-aware "Windows 11" theme is only well-supported on Windows 11.
#if defined(Q_OS_WIN)
#if QT_VERSION < QT_VERSION_CHECK(6, 7, 0)
colorScheme = Qt::ColorScheme::Light;
#else
if (QOperatingSystemVersion::current() < QOperatingSystemVersion::Windows11) {
colorScheme = Qt::ColorScheme::Light;
}
#endif // QT_VERSION < QT_VERSION_CHECK(6, 7, 0)
#endif // defined(Q_OS_WIN)
switch (colorScheme) {
case Qt::ColorScheme::Dark:
static QColor WHITE = QColor("White");
return WHITE;
case Qt::ColorScheme::Light:
case Qt::ColorScheme::Unknown:
default:
static QColor BLACK = QColor("Black");
return BLACK;
}
}
class PGraphicsPixmapItem : public QGraphicsPixmapItem
{
public:
@@ -99,7 +129,8 @@ private:
GraphicsScene::GraphicsScene(QObject *parent)
: QGraphicsScene(parent)
{
showText(tr("Drag image here"));
showText(tr("Drag image here", "The same meaning of MainWindow's hint."));
connect(qGuiApp->styleHints(), &QStyleHints::colorSchemeChanged, this, &GraphicsScene::updateStyle);
}
GraphicsScene::~GraphicsScene()
@@ -121,7 +152,7 @@ void GraphicsScene::showText(const QString &text)
{
this->clear();
QGraphicsTextItem * textItem = this->addText(text);
textItem->setDefaultTextColor(QColor("White"));
textItem->setDefaultTextColor(ColorSchemaToTextForeground(qGuiApp->styleHints()->colorScheme()));
m_theThing = textItem;
this->setSceneRect(m_theThing->boundingRect());
}
@@ -169,27 +200,57 @@ bool GraphicsScene::trySetTransformationMode(Qt::TransformationMode mode, float
return false;
}
/// Fetch graphics item as our custom animated item.
/// Return nullptr means this item is not animated item.
static PGraphicsMovieItem* fetchAnimatedItem(QGraphicsItem *item) {
return qgraphicsitem_cast<PGraphicsMovieItem *>(item);
}
bool GraphicsScene::canPauseAnimation() const {
return fetchAnimatedItem(m_theThing) != nullptr;
}
bool GraphicsScene::isPauseAnimation() const {
auto* animatedItem = fetchAnimatedItem(m_theThing);
if (animatedItem != nullptr) {
return animatedItem->movie()->state() == QMovie::Paused;
} else {
return false;
}
}
bool GraphicsScene::togglePauseAnimation()
{
PGraphicsMovieItem * animatedItem = qgraphicsitem_cast<PGraphicsMovieItem *>(m_theThing);
if (animatedItem) {
auto* animatedItem = fetchAnimatedItem(m_theThing);
if (animatedItem != nullptr) {
animatedItem->movie()->setPaused(animatedItem->movie()->state() != QMovie::Paused);
return true;
} else {
return false;
}
return false;
}
bool GraphicsScene::canSkipAnimationFrame() const {
return isPauseAnimation();
}
bool GraphicsScene::skipAnimationFrame(int delta)
{
PGraphicsMovieItem * animatedItem = qgraphicsitem_cast<PGraphicsMovieItem *>(m_theThing);
if (animatedItem) {
const int frameCount = animatedItem->movie()->frameCount();
const int currentFrame = animatedItem->movie()->currentFrameNumber();
const int targetFrame = (currentFrame + delta) % frameCount;
animatedItem->movie()->setPaused(true);
return animatedItem->movie()->jumpToFrame(targetFrame);
auto* animatedItem = fetchAnimatedItem(m_theThing);
// Only paused animated item can skip frame
if (animatedItem == nullptr) {
return false;
}
return false;
if (animatedItem->movie()->state() != QMovie::Paused) {
return false;
}
// Perform skip
const int frameCount = animatedItem->movie()->frameCount();
const int currentFrame = animatedItem->movie()->currentFrameNumber();
const int targetFrame = (currentFrame + delta) % frameCount;
return animatedItem->movie()->jumpToFrame(targetFrame);
}
QPixmap GraphicsScene::renderToPixmap()
@@ -206,3 +267,14 @@ QPixmap GraphicsScene::renderToPixmap()
return pixmap;
}
void GraphicsScene::updateStyle(Qt::ColorScheme colorScheme) {
Q_UNUSED(colorScheme)
QGraphicsItem * item = this->m_theThing;
if (item == nullptr) return;
QGraphicsTextItem * textItem = qgraphicsitem_cast<QGraphicsTextItem*>(item);
if (textItem == nullptr) return;
textItem->setDefaultTextColor(ColorSchemaToTextForeground(colorScheme));
}
+15
View File
@@ -21,11 +21,26 @@ public:
bool trySetTransformationMode(Qt::TransformationMode mode, float scaleHint);
/// Check whether current thing is animation.
/// Only animation thing can be paused.
bool canPauseAnimation() const;
/// Check whether we are pasuing an animation graphics.
bool isPauseAnimation() const;
/// Toggle animation graphics pause state.
/// Return true for successfully toggle, otherwise false.
bool togglePauseAnimation();
/// Check whether we can skip animation frame.
/// Only paused animation graphics can skip frame.
bool canSkipAnimationFrame() const;
/// Skip paused animation graphics with given frame.
/// Return true for successfully skip, otherwise false.
bool skipAnimationFrame(int delta = 1);
QPixmap renderToPixmap();
private slots:
void updateStyle(Qt::ColorScheme colorScheme);
private:
QGraphicsItem * m_theThing = nullptr;
};
+66 -13
View File
@@ -7,6 +7,8 @@
#include "graphicsscene.h"
#include "settings.h"
#include <QStyleHints>
#include <QOperatingSystemVersion>
#include <QDebug>
#include <QColorSpace>
#include <QMouseEvent>
@@ -23,11 +25,12 @@ GraphicsView::GraphicsView(QWidget *parent)
setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setResizeAnchor(QGraphicsView::AnchorUnderMouse);
setTransformationAnchor(QGraphicsView::AnchorUnderMouse);
setStyleSheet("background-color: rgba(0, 0, 0, 220);"
"border-radius: 3px;");
setAcceptDrops(false);
setCheckerboardEnabled(false);
updateStyle(qGuiApp->styleHints()->colorScheme());
connect(qGuiApp->styleHints(), &QStyleHints::colorSchemeChanged, this, &GraphicsView::updateStyle);
connect(horizontalScrollBar(), &QScrollBar::valueChanged, this, &GraphicsView::viewportRectChanged);
connect(verticalScrollBar(), &QScrollBar::valueChanged, this, &GraphicsView::viewportRectChanged);
}
@@ -137,7 +140,7 @@ void GraphicsView::resetTransform()
void GraphicsView::zoomView(qreal scaleFactor)
{
m_enableFitInView = false;
m_enableFitToScreen = false;
scale(scaleFactor, scaleFactor);
applyTransformationModeByScaleFactor();
emit navigatorViewRequired(!isThingSmallerThanWindowWith(transform()), transform());
@@ -221,6 +224,19 @@ void GraphicsView::fitByOrientation(Qt::Orientation ori, bool scaleDownOnly)
QRectF originalScene = sceneRect();
QTransform currentTransform = transform();
// TODO:
// I guess following code may be optimized by matrix arithmetic operation.
// There is an AI given solution. But I didn't use it.
// Because I want to keep the same behavior of upstream.
// // Horizontal fit: Let the position of top center of viewport, (cx, 0), be inversely mapped to scene.
// // And compensate the behavior of `centerOn` which put given point to the center of viewport.
// QTransform inv = currentTransform.inverted();
// QPointF viewCx = viewport()->rect().center().x();
// QPointF viewCy = viewport()->rect().center().y();
// // The target is viewport top center. Its y=0, so `centerOn` required scene point is (viewCx, -viewCy).
// centerOn(inv.map(QPointF(viewCx, -viewCy)));
if (ori == Qt::Horizontal) {
// For horizontal fit (fit by width), position at top (for tall images)
// Find the scene point that corresponds to the top-center of the transformed image
@@ -273,7 +289,7 @@ void GraphicsView::fitByOrientation(Qt::Orientation ori, bool scaleDownOnly)
centerOn(sceneLeftCenter);
}
m_enableFitInView = false;
m_enableFitToScreen = false;
applyTransformationModeByScaleFactor();
emit navigatorViewRequired(!isThingSmallerThanWindowWith(transform()), transform());
@@ -296,7 +312,7 @@ void GraphicsView::displayScene()
emit navigatorViewRequired(false, transform());
}
m_enableFitInView = true;
m_enableFitToScreen = true;
m_firstUserMediaLoaded = true;
}
@@ -310,12 +326,12 @@ bool GraphicsView::isSceneBiggerThanView() const
}
// Automately do fit in view when viewport(window) smaller than image original size.
void GraphicsView::setEnableAutoFitInView(bool enable)
void GraphicsView::setEnableAutoFitToScreen(bool enable)
{
m_enableFitInView = enable;
m_enableFitToScreen = enable;
}
bool GraphicsView::avoidResetTransform() const
bool GraphicsView::isAvoidResetTransform() const
{
return m_avoidResetTransform;
}
@@ -325,6 +341,15 @@ void GraphicsView::setAvoidResetTransform(bool avoidReset)
m_avoidResetTransform = avoidReset;
}
bool GraphicsView::isCheckerboard() const {
return m_checkerboardEnabled;
}
void GraphicsView::toggleCheckerboard(bool invertCheckerboardColor)
{
setCheckerboardEnabled(!m_checkerboardEnabled, invertCheckerboardColor);
}
inline double zeroOrOne(double number)
{
return qFuzzyIsNull(number) ? 0 : (number > 0 ? 1 : -1);
@@ -339,9 +364,37 @@ QTransform GraphicsView::resetScale(const QTransform & orig)
orig.dx(), orig.dy());
}
void GraphicsView::toggleCheckerboard(bool invertCheckerboardColor)
{
setCheckerboardEnabled(!m_checkerboardEnabled, invertCheckerboardColor);
void GraphicsView::updateStyle(Qt::ColorScheme colorScheme) {
// YYC MARK:
// If we are using Windows 10 and lower system or Qt which do not support "Windows 11"
// theme (lower than Qt 6.7), we should only support light mode.
// Because Qt's dark-mode-aware "Windows 11" theme is only well-supported on Windows 11.
#if defined(Q_OS_WIN)
#if QT_VERSION < QT_VERSION_CHECK(6, 7, 0)
colorScheme = Qt::ColorScheme::Light;
#else
if (QOperatingSystemVersion::current() < QOperatingSystemVersion::Windows11) {
colorScheme = Qt::ColorScheme::Light;
}
#endif // QT_VERSION < QT_VERSION_CHECK(6, 7, 0)
#endif // defined(Q_OS_WIN)
switch (colorScheme) {
case Qt::ColorScheme::Dark:
setStyleSheet(R"#(
border-style: none;
background-color: black;
)#");
break;
case Qt::ColorScheme::Light:
case Qt::ColorScheme::Unknown:
default:
setStyleSheet(R"#(
border-style: none;
background-color: white;
)#");
break;
}
}
void GraphicsView::mousePressEvent(QMouseEvent *event)
@@ -387,11 +440,11 @@ void GraphicsView::wheelEvent(QWheelEvent *event)
void GraphicsView::resizeEvent(QResizeEvent *event)
{
if (m_enableFitInView) {
if (m_enableFitToScreen) {
bool originalSizeSmallerThanWindow = isThingSmallerThanWindowWith(resetScale(transform()));
if (originalSizeSmallerThanWindow && scaleFactor() >= 1) {
// no longer need to do fitInView()
// but we leave the m_enableFitInView value unchanged in case
// but we leave the m_enableToScreen value unchanged in case
// user resize down the window again.
} else if (originalSizeSmallerThanWindow && scaleFactor() < 1) {
resetScale();
+8 -5
View File
@@ -38,19 +38,22 @@ public:
void displayScene();
bool isSceneBiggerThanView() const;
void setEnableAutoFitInView(bool enable = true);
void setEnableAutoFitToScreen(bool enable = true);
bool avoidResetTransform() const;
bool isAvoidResetTransform() const;
void setAvoidResetTransform(bool avoidReset);
bool isCheckerboard() const;
void toggleCheckerboard(bool invertCheckerboardColor = false);
static QTransform resetScale(const QTransform & orig);
signals:
void navigatorViewRequired(bool required, QTransform transform);
void viewportRectChanged();
public slots:
void toggleCheckerboard(bool invertCheckerboardColor = false);
private slots:
void updateStyle(Qt::ColorScheme colorScheme);
private:
void mousePressEvent(QMouseEvent * event) override;
@@ -69,7 +72,7 @@ private:
// Consider switch to 3 state for "no fit", "always fit" and "fit when view is smaller"?
// ... or even more? e.g. "fit/snap width" things...
// Currently it's "no fit" when it's false and "fit when view is smaller" when it's true.
bool m_enableFitInView = false;
bool m_enableFitToScreen = false;
bool m_avoidResetTransform = false;
bool m_checkerboardEnabled = false;
bool m_useLightCheckerboard = false;
+5 -12
View File
@@ -4,7 +4,6 @@
#include "mainwindow.h"
#include "playlistmanager.h"
#include "settings.h"
#ifdef Q_OS_MACOS
@@ -68,21 +67,19 @@ int main(int argc, char *argv[])
a.installEventFilter(fileOpenEventHandler);
a.connect(fileOpenEventHandler, &FileOpenEventHandler::fileOpen, [&w](const QUrl & url){
if (w.isHidden()) {
w.setWindowOpacity(1);
w.showNormal();
} else {
w.activateWindow();
}
w.showUrls({url});
w.showFiles({url});
w.initWindowSize();
});
// Handle dock icon clicks to show hidden window
a.connect(&a, &QApplication::applicationStateChanged, [&w](Qt::ApplicationState state) {
if (state == Qt::ApplicationActive && w.isHidden()) {
w.showUrls({});
w.galleryCurrent(true, true);
w.setWindowOpacity(1);
w.showFiles({});
w.galleryCurrent(false);
w.showNormal();
w.raise();
w.activateWindow();
@@ -90,12 +87,8 @@ int main(int argc, char *argv[])
});
#endif // Q_OS_MACOS
QStringList urlStrList = parser.positionalArguments();
QList<QUrl> && urlList = PlaylistManager::convertToUrlList(urlStrList);
if (!urlList.isEmpty()) {
w.showUrls(urlList);
}
QStringList fileList = parser.positionalArguments();
w.showFiles(fileList);
w.initWindowSize();
+246 -457
View File
@@ -3,6 +3,7 @@
// SPDX-License-Identifier: MIT
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "settings.h"
#include "bottombuttongroup.h"
@@ -46,76 +47,124 @@
using namespace Qt::Literals::StringLiterals;
MainWindow::MainWindow(QWidget *parent)
: FramelessWindow(parent)
: QWidget(parent)
, ui(new QT_PREPEND_NAMESPACE(Ui::MainWindow))
, m_am(new ActionManager)
, m_pm(new PlaylistManager(this))
, m_fileSystemWatcher(new QFileSystemWatcher(this))
{
this->setAttribute(Qt::WA_TranslucentBackground, true);
ui->setupUi(this);
this->setWindowFlags(Qt::Window | Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::WindowMinMaxButtonsHint | Qt::WindowCloseButtonHint);
// YYC MARK:
// Blumis set original value is 350 x 330.
// It is too small for modern device,
// so I level it up to standard VGA resolution 640 x 480.
this->setMinimumSize(640, 480);
// so I level it up to SVGA resolution 800 x 600.
this->setMinimumSize(800, 600);
this->setWindowIcon(QIcon(u":/icons/app-icon.svg"_s));
this->setMouseTracking(true);
this->setAcceptDrops(true);
m_pm->setAutoLoadFilterSuffixes(supportedImageFormats());
// Initialize playlist manager
m_pm->setAllowedSuffixes(supportedImageFormats());
// Initialize graphics scene
GraphicsScene * scene = new GraphicsScene(this);
m_gv = new GraphicsView(this);
m_gv->setScene(scene);
this->setCentralWidget(m_gv);
m_nav = new NavigatorView(this);
// YYC MARK:
// Blumia set original value is 220 x 160.
// So if we set its height to 72 for respecting the height of Windows Photo Viewer,
// the width should be set to 99 for keeping aspect ratio.
m_nav->setFixedSize(99, 72);
m_nav->setScene(scene);
m_nav->setMainView(m_gv);
m_nav->fitInView(m_nav->sceneRect(), Qt::KeepAspectRatio);
connect(m_gv, &GraphicsView::navigatorViewRequired,
// Setup graphics view (main photo view)
ui->m_gv->setScene(scene);
// Setup navigator view (right-bottom corner thumbnail view)
ui->m_nav->setScene(scene);
ui->m_nav->setMainView(ui->m_gv);
ui->m_nav->fitInView(ui->m_nav->sceneRect(), Qt::KeepAspectRatio);
// Binding graphics and navigator views signals
connect(ui->m_gv, &GraphicsView::navigatorViewRequired,
this, [this](bool required, const QTransform & tf){
m_nav->setTransform(GraphicsView::resetScale(tf));
m_nav->fitInView(m_nav->sceneRect(), Qt::KeepAspectRatio);
m_nav->setVisible(required);
m_nav->updateMainViewportRegion();
ui->m_nav->setTransform(GraphicsView::resetScale(tf));
ui->m_nav->fitInView(ui->m_nav->sceneRect(), Qt::KeepAspectRatio);
ui->m_nav->setVisible(required);
ui->m_navPH->setVisible(required);
ui->m_nav->updateMainViewportRegion();
});
connect(ui->m_gv, &GraphicsView::viewportRectChanged,
ui->m_nav, &NavigatorView::updateMainViewportRegion);
connect(m_gv, &GraphicsView::viewportRectChanged,
m_nav, &NavigatorView::updateMainViewportRegion);
// Setup action manager
m_am->setupAction(this);
m_bottomButtonGroup = new BottomButtonGroup({
// Setup bottom button group
ui->m_bottomButtonGroup->addActions({
m_am->actionActualSize,
m_am->actionToggleMaximize,
m_am->actionFitToScreen,
m_am->actionZoomIn,
m_am->actionZoomOut,
m_am->actionPrevPicture,
m_am->actionProperties,
m_am->actionNextPicture,
m_am->actionToggleCheckerboard,
m_am->actionRotateClockwise
}, this);
m_am->actionRotateCounterClockwise,
m_am->actionRotateClockwise,
m_am->actionFlipHorizontal,
m_am->actionFlipVertical,
});
// Setup right click menus
{
ui->m_subMenuCopy->addAction(m_am->actionCopyPixmap);
ui->m_subMenuCopy->addAction(m_am->actionCopyFilePath);
}
{
ui->m_subMenuTransform->addAction(m_am->actionActualSize);
ui->m_subMenuTransform->addAction(m_am->actionFitToScreen);
ui->m_subMenuTransform->addAction(m_am->actionFitByWidth);
ui->m_subMenuTransform->addAction(m_am->actionFitByHeight);
ui->m_subMenuTransform->addAction(m_am->actionZoomIn);
ui->m_subMenuTransform->addAction(m_am->actionZoomOut);
connect(m_pm, &PlaylistManager::totalCountChanged, this, &MainWindow::updateGalleryButtonsVisibility);
ui->m_subMenuTransform->addSeparator();
connect(m_pm->model(), &PlaylistModel::modelReset, this, std::bind(&MainWindow::galleryCurrent, this, false, false));
connect(m_pm, &PlaylistManager::currentIndexChanged, this, std::bind(&MainWindow::galleryCurrent, this, true, false));
ui->m_subMenuTransform->addAction(m_am->actionFlipHorizontal);
ui->m_subMenuTransform->addAction(m_am->actionFlipVertical);
ui->m_subMenuTransform->addAction(m_am->actionRotateClockwise);
ui->m_subMenuTransform->addAction(m_am->actionRotateCounterClockwise);
}
{
ui->m_menu->addMenu(ui->m_subMenuCopy);
ui->m_menu->addSeparator();
ui->m_menu->addAction(m_am->actionPrevPicture);
ui->m_menu->addAction(m_am->actionNextPicture);
ui->m_menu->addSeparator();
ui->m_menu->addMenu(ui->m_subMenuTransform);
ui->m_menu->addSeparator();
ui->m_menu->addAction(m_am->actionToggleAvoidResetTransform);
ui->m_menu->addAction(m_am->actionToggleCheckerboard);
ui->m_menu->addAction(m_am->actionTogglePauseAnimation);
ui->m_menu->addAction(m_am->actionAnimationNextFrame);
ui->m_menu->addSeparator();
ui->m_menu->addAction(m_am->actionTrash);
ui->m_menu->addAction(m_am->actionLocateInFileManager);
ui->m_menu->addAction(m_am->actionProperties);
ui->m_menu->addSeparator();
ui->m_menu->addAction(m_am->actionSettings);
ui->m_menu->addAction(m_am->actionHelp);
}
// Connect playlist manager and file watcher signals for photo browsering.
connect(m_pm, &PlaylistManager::playlistChanged, this, std::bind(&MainWindow::updateActionState, this));
connect(m_pm, &PlaylistManager::playlistChanged, this, std::bind(&MainWindow::galleryCurrent, this, false));
connect(m_pm, &PlaylistManager::currentIndexChanged, this, std::bind(&MainWindow::galleryCurrent, this, false));
connect(m_fileSystemWatcher, &QFileSystemWatcher::fileChanged, this, [this](){
QTimer::singleShot(500, this, std::bind(&MainWindow::galleryCurrent, this, false, true));
QTimer::singleShot(500, this, std::bind(&MainWindow::galleryCurrent, this, true));
});
QShortcut * fullscreenShorucut = new QShortcut(QKeySequence(QKeySequence::FullScreen), this);
connect(fullscreenShorucut, &QShortcut::activated,
this, &MainWindow::toggleFullscreen);
// Change window location
centerWindow();
QTimer::singleShot(0, this, [this](){
@@ -126,89 +175,37 @@ MainWindow::MainWindow(QWidget *parent)
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::showUrls(const QList<QUrl> &urls)
void MainWindow::showFiles(const QStringList &files)
{
if (!urls.isEmpty()) {
const QUrl & firstUrl = urls.first();
if (urls.count() == 1) {
const QString lowerCaseUrlPath(firstUrl.path().toLower());
if (lowerCaseUrlPath.endsWith(".m3u8") || lowerCaseUrlPath.endsWith(".m3u")) {
m_pm->loadM3U8Playlist(firstUrl);
galleryCurrent(true, true);
return;
}
}
m_gv->showFileFromPath(firstUrl.toLocalFile());
m_pm->loadPlaylist(urls);
} else {
m_gv->showText(tr("File url list is empty"));
m_pm->setPlaylist(urls);
return;
}
m_pm->loadPlaylist(files);
ui->m_nav->fitInView(ui->m_nav->sceneRect(), Qt::KeepAspectRatio);
}
m_nav->fitInView(m_nav->sceneRect(), Qt::KeepAspectRatio);
void MainWindow::showFiles(const QList<QUrl>& urls)
{
m_pm->loadPlaylist(urls);
ui->m_nav->fitInView(ui->m_nav->sceneRect(), Qt::KeepAspectRatio);
}
void MainWindow::initWindowSize()
{
switch (Settings::instance()->initWindowSizeBehavior()) {
case Settings::WindowSizeBehavior::Auto:
adjustWindowSizeBySceneRect();
break;
case Settings::WindowSizeBehavior::Maximized:
showMaximized();
break;
case Settings::WindowSizeBehavior::Windowed:
default:
showNormal();
break;
default:
adjustWindowSizeBySceneRect();
break;
}
}
void MainWindow::adjustWindowSizeBySceneRect()
{
if (m_pm->totalCount() < 1) return;
QSize sceneSize = m_gv->sceneRect().toRect().size();
QSize sceneSizeWithMargins = sceneSize + QSize(130, 125);
if (m_gv->scaleFactor() < 1 || size().expandedTo(sceneSizeWithMargins) != size()) {
// if it scaled down by the resize policy:
QSize screenSize = qApp->screenAt(QCursor::pos())->availableSize();
if (screenSize.expandedTo(sceneSize) == screenSize) {
// we can show the picture by increase the window size.
QSize finalSize = (screenSize.expandedTo(sceneSizeWithMargins) == screenSize) ?
sceneSizeWithMargins : screenSize;
// We have a very reasonable sizeHint() value ;P
this->resize(finalSize.expandedTo(this->sizeHint()));
// We're sure the window can display the whole thing with 1:1 scale.
// The old window size may cause fitInView call from resize() and the
// above resize() call won't reset the scale back to 1:1, so we
// just call resetScale() here to ensure the thing is no longer scaled.
m_gv->resetScale();
centerWindow();
} else {
// toggle maximum
showMaximized();
}
}
}
// can be empty if it is NOT from a local file.
QUrl MainWindow::currentImageFileUrl() const
{
return m_pm->urlByIndex(m_pm->curIndex());
}
void MainWindow::clearGallery()
{
m_pm->setPlaylist({});
m_pm->clearPlaylist();
}
void MainWindow::galleryPrev()
@@ -216,10 +213,9 @@ void MainWindow::galleryPrev()
const bool loopGallery = Settings::instance()->loopGallery();
if (!loopGallery && m_pm->isFirstIndex()) return;
QModelIndex index = m_pm->previousIndex();
if (index.isValid()) {
m_pm->setCurrentIndex(index);
m_gv->showFileFromPath(m_pm->localFileByIndex(index));
auto index = m_pm->previousIndex();
if (index.has_value()) {
m_pm->setCurrentIndex(*index);
}
}
@@ -228,31 +224,29 @@ void MainWindow::galleryNext()
const bool loopGallery = Settings::instance()->loopGallery();
if (!loopGallery && m_pm->isLastIndex()) return;
QModelIndex index = m_pm->nextIndex();
if (index.isValid()) {
m_pm->setCurrentIndex(index);
m_gv->showFileFromPath(m_pm->localFileByIndex(index));
auto index = m_pm->nextIndex();
if (index.has_value()) {
m_pm->setCurrentIndex(*index);
}
}
// Only use this to update minor information.
void MainWindow::galleryCurrent(bool showLoadImageHintWhenEmpty, bool reloadImage)
void MainWindow::galleryCurrent(bool fromFileWatcher)
{
QModelIndex index = m_pm->curIndex();
bool shouldResetfileWatcher = true;
if (index.isValid()) {
const QString & localFilePath(m_pm->localFileByIndex(index));
if (reloadImage) m_gv->showFileFromPath(localFilePath);
shouldResetfileWatcher = !updateFileWatcher(localFilePath);
setWindowTitle(m_pm->urlByIndex(index).fileName());
} else if (showLoadImageHintWhenEmpty && m_pm->totalCount() <= 0) {
m_gv->showText(QCoreApplication::translate("GraphicsScene", "Drag image here"));
auto index = m_pm->currentIndex();
if (index.has_value()) {
QString localFilePath = m_pm->localFileByIndex(*index);
ui->m_gv->showFileFromPath(localFilePath);
if (!fromFileWatcher) {
updateFileWatcher(localFilePath);
setWindowTitle(m_pm->urlByIndex(*index).fileName());
}
} else {
ui->m_gv->showText(tr("Drag image here"));
setWindowTitle(QString());
updateFileWatcher();
}
updateGalleryButtonsVisibility();
if (shouldResetfileWatcher) updateFileWatcher();
updateActionState();
}
QStringList MainWindow::supportedImageFormats()
@@ -268,53 +262,6 @@ QStringList MainWindow::supportedImageFormats()
return formatFilters;
}
void MainWindow::showEvent(QShowEvent *event)
{
updateWidgetsPosition();
return FramelessWindow::showEvent(event);
}
void MainWindow::enterEvent(QEnterEvent *event)
{
return FramelessWindow::enterEvent(event);
}
void MainWindow::leaveEvent(QEvent *event)
{
return FramelessWindow::leaveEvent(event);
}
void MainWindow::mouseDoubleClickEvent(QMouseEvent *event)
{
// The forward/back mouse button can also used to trigger a mouse double-click event
// Since we use that for gallery navigation so we ignore these two buttons.
if (event->buttons() & Qt::ForwardButton || event->buttons() & Qt::BackButton) {
return;
}
switch (Settings::instance()->doubleClickBehavior()) {
case Settings::DoubleClickBehavior::Close:
quitAppAction();
event->accept();
break;
case Settings::DoubleClickBehavior::Maximize:
toggleMaximize();
event->accept();
break;
case Settings::DoubleClickBehavior::FullScreen:
toggleFullscreen();
event->accept();
break;
case Settings::DoubleClickBehavior::Ignore:
break;
}
// blumia: don't call parent constructor here, seems it will cause mouse move
// event get called even if we set event->accept();
// return QMainWindow::mouseDoubleClickEvent(event);
}
void MainWindow::wheelEvent(QWheelEvent *event)
{
QPoint numDegrees = event->angleDelta() / 8;
@@ -347,94 +294,26 @@ void MainWindow::wheelEvent(QWheelEvent *event)
}
event->accept();
} else {
FramelessWindow::wheelEvent(event);
QWidget::wheelEvent(event);
}
}
void MainWindow::resizeEvent(QResizeEvent *event)
{
updateWidgetsPosition();
return FramelessWindow::resizeEvent(event);
}
void MainWindow::contextMenuEvent(QContextMenuEvent *event)
{
QMenu * menu = new QMenu;
QMenu * copyMenu = new QMenu(tr("&Copy"));
QUrl currentFileUrl = currentImageFileUrl();
ui->m_menu->exec(mapToGlobal(event->pos()));
QAction * copyPixmap = m_am->actionCopyPixmap;
QAction * copyFilePath = m_am->actionCopyFilePath;
copyMenu->setIcon(QIcon::fromTheme(u"edit-copy"_s));
copyMenu->addAction(copyPixmap);
if (currentFileUrl.isValid()) {
copyMenu->addAction(copyFilePath);
}
QAction * paste = m_am->actionPaste;
QAction * trash = m_am->actionTrash;
QAction * avoidResetTransform = m_am->actionToggleAvoidResetTransform;
avoidResetTransform->setCheckable(true);
avoidResetTransform->setChecked(m_gv->avoidResetTransform());
QAction * toggleSettings = m_am->actionSettings;
QAction * helpAction = m_am->actionHelp;
QAction * propertiesAction = m_am->actionProperties;
#if 0
menu->addAction(m_am->actionOpen);
#endif // 0
if (copyMenu->actions().count() == 1) {
menu->addActions(copyMenu->actions());
} else {
menu->addMenu(copyMenu);
}
if (canPaste()) {
menu->addAction(paste);
}
menu->addSeparator();
menu->addAction(m_am->actionHorizontalFlip);
#if 0
menu->addAction(m_am->actionFitInView);
menu->addAction(m_am->actionFitByWidth);
#endif // 0
menu->addSeparator();
menu->addAction(avoidResetTransform);
menu->addSeparator();
menu->addAction(toggleSettings);
menu->addAction(helpAction);
if (currentFileUrl.isValid()) {
menu->addSeparator();
if (currentFileUrl.isLocalFile()) {
menu->addAction(trash);
menu->addAction(m_am->actionLocateInFileManager);
}
menu->addAction(propertiesAction);
}
menu->exec(mapToGlobal(event->pos()));
menu->deleteLater();
copyMenu->deleteLater();
return FramelessWindow::contextMenuEvent(event);
return QWidget::contextMenuEvent(event);
}
void MainWindow::dragEnterEvent(QDragEnterEvent *event)
{
if (event->mimeData()->hasUrls() || event->mimeData()->hasImage() || event->mimeData()->hasText()) {
if (event->mimeData()->hasUrls()) {
event->acceptProposedAction();
} else {
event->ignore();
}
return FramelessWindow::dragEnterEvent(event);
return QWidget::dragEnterEvent(event);
}
void MainWindow::dragMoveEvent(QDragMoveEvent *event)
@@ -447,26 +326,10 @@ void MainWindow::dropEvent(QDropEvent *event)
event->acceptProposedAction();
const QMimeData * mimeData = event->mimeData();
if (mimeData->hasUrls()) {
const QList<QUrl> &urls = mimeData->urls();
if (urls.isEmpty()) {
m_gv->showText(tr("File url list is empty"));
} else {
showUrls(urls);
}
} else if (mimeData->hasImage()) {
QImage img = qvariant_cast<QImage>(mimeData->imageData());
QPixmap pixmap = QPixmap::fromImage(img);
if (pixmap.isNull()) {
m_gv->showText(tr("Image data is invalid"));
} else {
m_gv->showImage(pixmap);
}
} else if (mimeData->hasText()) {
m_gv->showText(mimeData->text());
showFiles(mimeData->urls());
} else {
m_gv->showText(tr("Not supported mimedata: %1").arg(mimeData->formats().first()));
ui->m_gv->showText(tr("Not supported mimedata: %1").arg(mimeData->formats().first()));
}
}
@@ -482,43 +345,9 @@ void MainWindow::centerWindow()
);
}
void MainWindow::closeWindow()
{
}
void MainWindow::updateWidgetsPosition()
{
m_bottomButtonGroup->move((width() - m_bottomButtonGroup->width()) / 2,
(72 - m_bottomButtonGroup->height()) / 2 + (height() - 72));
m_nav->move(width() - m_nav->width(), height() - m_nav->height());
}
void MainWindow::toggleAvoidResetTransform()
{
m_gv->setAvoidResetTransform(!m_gv->avoidResetTransform());
}
bool MainWindow::canPaste() const
{
const QMimeData * clipboardData = QApplication::clipboard()->mimeData();
if (clipboardData->hasImage()) {
return true;
} else if (clipboardData->hasText()) {
QString clipboardText(clipboardData->text());
if (clipboardText.startsWith("PICTURE:")) {
QString maybeFilename(clipboardText.mid(8));
if (QFile::exists(maybeFilename)) {
return true;
}
}
}
return false;
}
void MainWindow::quitAppAction(bool force)
{
closeWindow();
ui->m_gv->setAvoidResetTransform(!ui->m_gv->isAvoidResetTransform());
}
void MainWindow::toggleFullscreen()
@@ -544,140 +373,21 @@ QSize MainWindow::sizeHint() const
return QSize(710, 530);
}
void MainWindow::on_actionOpen_triggered()
{
QStringList picturesLocations = QStandardPaths::standardLocations(QStandardPaths::PicturesLocation);
QUrl pictureUrl = picturesLocations.isEmpty() ? QUrl::fromLocalFile(picturesLocations.first())
: QUrl::fromLocalFile(QDir::homePath());
QList<QUrl> urls(QFileDialog::getOpenFileUrls(this, QString(), pictureUrl));
if (!urls.isEmpty()) {
showUrls(urls);
}
}
void MainWindow::on_actionActualSize_triggered()
{
m_gv->resetScale();
m_gv->setEnableAutoFitInView(false);
}
void MainWindow::on_actionToggleMaximize_triggered()
{
toggleMaximize();
}
void MainWindow::on_actionZoomIn_triggered()
{
if (m_gv->scaleFactor() < 1000) {
m_gv->zoomView(1.25);
}
}
void MainWindow::on_actionZoomOut_triggered()
{
m_gv->zoomView(0.8);
}
void MainWindow::on_actionHorizontalFlip_triggered()
{
m_gv->flipView();
}
void MainWindow::on_actionFitInView_triggered()
{
m_gv->fitInView(m_nav->sceneRect(), Qt::KeepAspectRatio);
m_gv->setEnableAutoFitInView(m_gv->scaleFactor() <= 1);
}
void MainWindow::on_actionFitByWidth_triggered()
{
m_gv->fitByOrientation();
}
// region: Action Trigger
void MainWindow::on_actionCopyPixmap_triggered()
{
QClipboard *cb = QApplication::clipboard();
cb->setPixmap(m_gv->scene()->renderToPixmap());
cb->setPixmap(ui->m_gv->scene()->renderToPixmap());
}
void MainWindow::on_actionCopyFilePath_triggered()
{
QUrl currentFileUrl(currentImageFileUrl());
if (currentFileUrl.isValid()) {
QClipboard *cb = QApplication::clipboard();
cb->setText(currentFileUrl.toLocalFile());
}
}
auto index = m_pm->currentIndex();
if (!index.has_value()) return;
void MainWindow::on_actionPaste_triggered()
{
QImage clipboardImage;
QUrl clipboardFileUrl;
const QMimeData * clipboardData = QApplication::clipboard()->mimeData();
if (clipboardData->hasImage()) {
QVariant imageVariant(clipboardData->imageData());
if (imageVariant.isValid()) {
clipboardImage = qvariant_cast<QImage>(imageVariant);
}
} else if (clipboardData->hasText()) {
QString clipboardText(clipboardData->text());
if (clipboardText.startsWith("PICTURE:")) {
QString maybeFilename(clipboardText.mid(8));
if (QFile::exists(maybeFilename)) {
clipboardFileUrl = QUrl::fromLocalFile(maybeFilename);
}
}
}
if (!clipboardImage.isNull()) {
setWindowTitle(tr("Image From Clipboard"));
m_gv->showImage(clipboardImage);
clearGallery();
} else if (clipboardFileUrl.isValid()) {
m_gv->showFileFromPath(clipboardFileUrl.toLocalFile());
m_pm->loadPlaylist(clipboardFileUrl);
}
}
void MainWindow::on_actionTrash_triggered()
{
QModelIndex index = m_pm->curIndex();
if (!m_pm->urlByIndex(index).isLocalFile()) return;
QFile file(m_pm->localFileByIndex(index));
QFileInfo fileInfo(file.fileName());
QMessageBox::StandardButton result = QMessageBox::question(this, tr("Move to Trash"),
tr("Are you sure you want to move \"%1\" to recycle bin?").arg(fileInfo.fileName()));
if (result == QMessageBox::Yes) {
bool succ = file.moveToTrash();
if (!succ) {
QMessageBox::warning(this, tr("Failed to move file to trash"),
tr("Move to trash failed, it might caused by file permission issue, file system limitation, or platform limitation."));
} else {
m_pm->removeAt(index);
galleryCurrent(true, true);
}
}
}
void MainWindow::on_actionToggleCheckerboard_triggered()
{
// TODO: is that okay to do this since we plan to support custom shortcuts?
m_gv->toggleCheckerboard(QGuiApplication::queryKeyboardModifiers().testFlag(Qt::ShiftModifier));
}
void MainWindow::on_actionRotateClockwise_triggered()
{
m_gv->rotateView();
m_gv->displayScene();
}
void MainWindow::on_actionRotateCounterClockwise_triggered()
{
m_gv->rotateView(false);
m_gv->displayScene();
QClipboard *cb = QApplication::clipboard();
cb->setText(m_pm->localFileByIndex(*index));
}
void MainWindow::on_actionPrevPicture_triggered()
@@ -690,14 +400,60 @@ void MainWindow::on_actionNextPicture_triggered()
galleryNext();
}
void MainWindow::on_actionTogglePauseAnimation_triggered()
void MainWindow::on_actionActualSize_triggered()
{
m_gv->scene()->togglePauseAnimation();
ui->m_gv->resetScale();
ui->m_gv->setEnableAutoFitToScreen(false);
}
void MainWindow::on_actionAnimationNextFrame_triggered()
void MainWindow::on_actionFitToScreen_triggered()
{
m_gv->scene()->skipAnimationFrame(1);
ui->m_gv->fitInView(ui->m_nav->sceneRect(), Qt::KeepAspectRatio);
ui->m_gv->setEnableAutoFitToScreen(ui->m_gv->scaleFactor() <= 1);
}
void MainWindow::on_actionFitByWidth_triggered()
{
ui->m_gv->fitByOrientation(Qt::Orientation::Horizontal);
}
void MainWindow::on_actionFitByHeight_triggered()
{
ui->m_gv->fitByOrientation(Qt::Orientation::Vertical);
}
void MainWindow::on_actionZoomIn_triggered()
{
if (ui->m_gv->scaleFactor() < 1000) {
ui->m_gv->zoomView(1.25);
}
}
void MainWindow::on_actionZoomOut_triggered()
{
ui->m_gv->zoomView(0.8);
}
void MainWindow::on_actionFlipHorizontal_triggered()
{
ui->m_gv->flipView(true);
}
void MainWindow::on_actionFlipVertical_triggered()
{
ui->m_gv->flipView(false);
}
void MainWindow::on_actionRotateClockwise_triggered()
{
ui->m_gv->rotateView(true);
ui->m_gv->displayScene();
}
void MainWindow::on_actionRotateCounterClockwise_triggered()
{
ui->m_gv->rotateView(false);
ui->m_gv->displayScene();
}
void MainWindow::on_actionToggleAvoidResetTransform_triggered()
@@ -705,37 +461,48 @@ void MainWindow::on_actionToggleAvoidResetTransform_triggered()
toggleAvoidResetTransform();
}
void MainWindow::on_actionSettings_triggered()
void MainWindow::on_actionToggleCheckerboard_triggered()
{
SettingsDialog * sd = new SettingsDialog(this);
sd->exec();
sd->deleteLater();
ui->m_gv->toggleCheckerboard(QGuiApplication::queryKeyboardModifiers().testFlag(Qt::ShiftModifier));
}
void MainWindow::on_actionHelp_triggered()
void MainWindow::on_actionTogglePauseAnimation_triggered()
{
AboutDialog * ad = new AboutDialog(this);
ad->exec();
ad->deleteLater();
ui->m_gv->scene()->togglePauseAnimation();
}
void MainWindow::on_actionProperties_triggered()
void MainWindow::on_actionAnimationNextFrame_triggered()
{
QUrl currentFileUrl = currentImageFileUrl();
if (!currentFileUrl.isValid()) return;
ui->m_gv->scene()->skipAnimationFrame(1);
}
MetadataModel * md = new MetadataModel();
md->setFile(currentFileUrl.toLocalFile());
void MainWindow::on_actionTrash_triggered()
{
auto index = m_pm->currentIndex();
if (!index.has_value()) return;
if (!m_pm->urlByIndex(*index).isLocalFile()) return;
MetadataDialog * ad = new MetadataDialog(this);
ad->setMetadataModel(md);
ad->exec();
ad->deleteLater();
QFile file(m_pm->localFileByIndex(*index));
QFileInfo fileInfo(file.fileName());
QMessageBox::StandardButton result = QMessageBox::question(this, tr("Move to Trash"),
tr("Are you sure you want to move \"%1\" to recycle bin?").arg(fileInfo.fileName()));
if (result == QMessageBox::Yes) {
bool succ = file.moveToTrash();
if (!succ) {
QMessageBox::warning(this, tr("Failed to move file to trash"),
tr("Move to trash failed, it might caused by file permission issue, file system limitation, or platform limitation."));
} else {
m_pm->removeFromPlaylist(*index);
}
}
}
void MainWindow::on_actionLocateInFileManager_triggered()
{
QUrl currentFileUrl = currentImageFileUrl();
auto index = m_pm->currentIndex();
if (!index.has_value()) return;
QUrl currentFileUrl = m_pm->urlByIndex(*index);
if (!currentFileUrl.isValid()) return;
QFileInfo fileInfo(currentFileUrl.toLocalFile());
@@ -768,11 +535,38 @@ void MainWindow::on_actionLocateInFileManager_triggered()
#endif // Q_OS_WIN
}
void MainWindow::on_actionQuitApp_triggered()
void MainWindow::on_actionProperties_triggered()
{
quitAppAction(false);
auto index = m_pm->currentIndex();
if (!index.has_value()) return;
QUrl currentFileUrl = m_pm->urlByIndex(*index);
if (!currentFileUrl.isValid()) return;
MetadataModel * md = new MetadataModel();
md->setFile(currentFileUrl.toLocalFile());
MetadataDialog * ad = new MetadataDialog(this);
ad->setMetadataModel(md);
ad->exec();
ad->deleteLater();
}
void MainWindow::on_actionSettings_triggered()
{
SettingsDialog * sd = new SettingsDialog(this);
sd->exec();
sd->deleteLater();
}
void MainWindow::on_actionHelp_triggered()
{
AboutDialog * ad = new AboutDialog(this);
ad->exec();
ad->deleteLater();
}
// endregion
bool MainWindow::updateFileWatcher(const QString &basePath)
{
m_fileSystemWatcher->removePaths(m_fileSystemWatcher->files());
@@ -780,11 +574,6 @@ bool MainWindow::updateFileWatcher(const QString &basePath)
return false;
}
void MainWindow::updateGalleryButtonsVisibility()
{
const int galleryFileCount = m_pm->totalCount();
const bool loopGallery = Settings::instance()->loopGallery();
m_am->enablePrevNextPictureAction(
galleryFileCount > 1 && (loopGallery || !m_pm->isFirstIndex()),
galleryFileCount > 1 && (loopGallery || !m_pm->isLastIndex()));
void MainWindow::updateActionState() {
m_am->updateActionState(m_pm, ui->m_gv);
}
+31 -47
View File
@@ -5,23 +5,19 @@
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include "framelesswindow.h"
#include <QParallelAnimationGroup>
#include <QPropertyAnimation>
#include <QPushButton>
#include <QWidget>
QT_BEGIN_NAMESPACE
class QGraphicsView;
class QFileSystemWatcher;
namespace Ui {
class MainWindow;
}
QT_END_NAMESPACE
class ActionManager;
class PlaylistManager;
class GraphicsView;
class NavigatorView;
class BottomButtonGroup;
class MainWindow : public FramelessWindow
class MainWindow : public QWidget
{
Q_OBJECT
@@ -29,36 +25,26 @@ public:
explicit MainWindow(QWidget *parent = nullptr);
~MainWindow() override;
void showUrls(const QList<QUrl> &urls);
void showFiles(const QStringList& files);
void showFiles(const QList<QUrl>& urls);
void initWindowSize();
void adjustWindowSizeBySceneRect();
QUrl currentImageFileUrl() const;
void clearGallery();
void galleryPrev();
void galleryNext();
void galleryCurrent(bool showLoadImageHintWhenEmpty, bool reloadImage);
void galleryCurrent(bool fromFileWatcher);
static QStringList supportedImageFormats();
protected slots:
void showEvent(QShowEvent *event) override;
void enterEvent(QEnterEvent *event) override;
void leaveEvent(QEvent *event) override;
void 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 toggleAvoidResetTransform();
bool canPaste() const;
void quitAppAction(bool force = false);
void toggleFullscreen();
void toggleMaximize();
@@ -66,48 +52,46 @@ 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_actionCopyPixmap_triggered();
void on_actionCopyFilePath_triggered();
void on_actionPrevPicture_triggered();
void on_actionNextPicture_triggered();
void on_actionActualSize_triggered();
void on_actionFitToScreen_triggered();
void on_actionFitByWidth_triggered();
void on_actionFitByHeight_triggered();
void on_actionZoomIn_triggered();
void on_actionZoomOut_triggered();
void on_actionFlipHorizontal_triggered();
void on_actionFlipVertical_triggered();
void on_actionRotateClockwise_triggered();
void on_actionRotateCounterClockwise_triggered();
void on_actionToggleAvoidResetTransform_triggered();
void on_actionToggleCheckerboard_triggered();
void on_actionTogglePauseAnimation_triggered();
void on_actionAnimationNextFrame_triggered();
void on_actionHorizontalFlip_triggered();
void on_actionFitInView_triggered();
void on_actionFitByWidth_triggered();
void on_actionCopyPixmap_triggered();
void on_actionCopyFilePath_triggered();
void on_actionPaste_triggered();
void on_actionTrash_triggered();
void on_actionToggleAvoidResetTransform_triggered();
void on_actionLocateInFileManager_triggered();
void on_actionProperties_triggered();
void on_actionSettings_triggered();
void on_actionHelp_triggered();
void on_actionProperties_triggered();
void on_actionLocateInFileManager_triggered();
void on_actionQuitApp_triggered();
private:
bool updateFileWatcher(const QString & basePath = QString());
void updateGalleryButtonsVisibility();
void updateActionState();
private:
QT_PREPEND_NAMESPACE(Ui::MainWindow) *ui;
ActionManager *m_am;
PlaylistManager *m_pm;
GraphicsView *m_gv;
NavigatorView *m_nav;
QFileSystemWatcher *m_fileSystemWatcher;
BottomButtonGroup *m_bottomButtonGroup;
};
#endif // MAINWINDOW_H
+172
View File
@@ -0,0 +1,172 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QWidget" name="MainWindow">
<property name="windowModality">
<enum>Qt::NonModal</enum>
</property>
<property name="minimumSize">
<size>
<width>640</width>
<height>480</height>
</size>
</property>
<property name="acceptDrops">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="m_centralLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="GraphicsView" name="m_gv">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="verticalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
</widget>
</item>
<item>
<widget class="QWidget" name="bottomBar" native="true">
<property name="minimumSize">
<size>
<width>0</width>
<height>72</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>72</height>
</size>
</property>
<layout class="QHBoxLayout" name="bottomBarLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QWidget" name="m_navPH" native="true">
<property name="minimumSize">
<size>
<width>99</width>
<height>72</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>99</width>
<height>72</height>
</size>
</property>
</widget>
</item>
<item>
<spacer name="leftSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</spacer>
</item>
<item>
<widget class="BottomButtonGroup" name="m_bottomButtonGroup"/>
</item>
<item>
<spacer name="rightSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</spacer>
</item>
<item>
<widget class="NavigatorView" name="m_nav">
<property name="minimumSize">
<size>
<width>99</width>
<height>72</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>99</width>
<height>72</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="verticalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
<widget class="QMenu" name="m_subMenuCopy">
<property name="title">
<string>&amp;Copy</string>
</property>
<property name="icon">
<iconset theme="edit-copy"/>
</property>
</widget>
<widget class="QMenu" name="m_subMenuTransform">
<property name="title">
<string>&amp;Transform</string>
</property>
</widget>
<widget class="QMenu" name="m_menu"/>
</widget>
<customwidgets>
<customwidget>
<class>GraphicsView</class>
<extends>QGraphicsView</extends>
<header>graphicsview.h</header>
</customwidget>
<customwidget>
<class>NavigatorView</class>
<extends>QGraphicsView</extends>
<header>navigatorview.h</header>
</customwidget>
<customwidget>
<class>BottomButtonGroup</class>
<extends>QGroupBox</extends>
<header>bottombuttongroup.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>
+18 -15
View File
@@ -3,12 +3,12 @@
// SPDX-License-Identifier: MIT
#include "metadatadialog.h"
#include "ui_metadatadialog.h"
#include <QDialogButtonBox>
#include <QPainter>
#include <QStyledItemDelegate>
#include <QTreeView>
#include <QVBoxLayout>
#include <QHeaderView>
#include "metadatamodel.h"
@@ -73,30 +73,33 @@ protected:
};
MetadataDialog::MetadataDialog(QWidget *parent)
: QDialog(parent)
, m_treeView(new PropertyTreeView(this))
: QDialog(parent), ui(new QT_PREPEND_NAMESPACE(Ui::MetadataDialog))
{
ui->setupUi(this);
setWindowFlag(Qt::WindowContextHelpButtonHint, false);
// YYC MARK:
// Because we use custom QTreeView and we do not want to expose it to public domain.
// So I have to use this way as the compromise.
// Create our custom QTreeView and insert them
m_treeView = new PropertyTreeView(this);
auto* layout = ui->verticalLayout;
const int index = layout->indexOf(ui->m_buttonBox);
layout->insertWidget(index, m_treeView);
// Setup our custom QTreeView
m_treeView->setRootIsDecorated(false);
m_treeView->setIndentation(0);
m_treeView->setItemDelegate(new PropertyTreeItemDelegate(m_treeView));
m_treeView->header()->resizeSection(0, sizeHint().width() / 2);
setWindowTitle(tr("Image Metadata"));
QDialogButtonBox * buttonBox = new QDialogButtonBox(QDialogButtonBox::Close);
setLayout(new QVBoxLayout);
layout()->addWidget(m_treeView);
layout()->addWidget(buttonBox);
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::close);
setWindowFlag(Qt::WindowContextHelpButtonHint, false);
connect(ui->m_buttonBox, &QDialogButtonBox::rejected, this, &QDialog::close);
}
MetadataDialog::~MetadataDialog()
{
delete ui;
}
void MetadataDialog::setMetadataModel(MetadataModel * model)
+7
View File
@@ -11,6 +11,12 @@ QT_BEGIN_NAMESPACE
class QTreeView;
QT_END_NAMESPACE
QT_BEGIN_NAMESPACE
namespace Ui {
class MetadataDialog;
}
QT_END_NAMESPACE
class MetadataModel;
class MetadataDialog : public QDialog
{
@@ -25,6 +31,7 @@ public:
private:
QTreeView * m_treeView = nullptr;
QT_PREPEND_NAMESPACE(Ui::MetadataDialog) *ui;
};
#endif // METADATADIALOG_H
+23
View File
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MetadataDialog</class>
<widget class="QDialog" name="MetadataDialog">
<property name="windowModality">
<enum>Qt::NonModal</enum>
</property>
<property name="windowTitle">
<string>Image Metadata</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QDialogButtonBox" name="m_buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Close</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
+38 -2
View File
@@ -6,6 +6,8 @@
#include "graphicsview.h"
#include <QStyleHints>
#include <QOperatingSystemVersion>
#include <QMouseEvent>
#include <QDebug>
#include <QTimer>
@@ -16,8 +18,9 @@ NavigatorView::NavigatorView(QWidget *parent)
{
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setStyleSheet("background-color: rgba(0, 0, 0, 120);"
"border-radius: 3px;");
updateStyle(qGuiApp->styleHints()->colorScheme());
connect(qGuiApp->styleHints(), &QStyleHints::colorSchemeChanged, this, &NavigatorView::updateStyle);
}
// doesn't take or manage its ownership
@@ -38,6 +41,39 @@ void NavigatorView::updateMainViewportRegion()
});
}
void NavigatorView::updateStyle(Qt::ColorScheme colorScheme) {
// YYC MARK:
// If we are using Windows 10 and lower system or Qt which do not support "Windows 11"
// theme (lower than Qt 6.7), we should only support light mode.
// Because Qt's dark-mode-aware "Windows 11" theme is only well-supported on Windows 11.
#if defined(Q_OS_WIN)
#if QT_VERSION < QT_VERSION_CHECK(6, 7, 0)
colorScheme = Qt::ColorScheme::Light;
#else
if (QOperatingSystemVersion::current() < QOperatingSystemVersion::Windows11) {
colorScheme = Qt::ColorScheme::Light;
}
#endif // QT_VERSION < QT_VERSION_CHECK(6, 7, 0)
#endif // defined(Q_OS_WIN)
switch (colorScheme) {
case Qt::ColorScheme::Dark:
setStyleSheet(R"#(
border-style: none;
background-color: rgba(255, 255, 255, 70);
)#");
break;
case Qt::ColorScheme::Light:
case Qt::ColorScheme::Unknown:
default:
setStyleSheet(R"#(
border-style: none;
background-color: rgba(0, 0, 0, 70);
)#");
break;
}
}
void NavigatorView::mousePressEvent(QMouseEvent *event)
{
m_mouseDown = true;
+3
View File
@@ -19,6 +19,9 @@ public:
public slots:
void updateMainViewportRegion();
private slots:
void updateStyle(Qt::ColorScheme colorScheme);
private:
void mousePressEvent(QMouseEvent * event) override;
void mouseMoveEvent(QMouseEvent * event) override;
+213 -201
View File
@@ -3,158 +3,20 @@
// SPDX-License-Identifier: MIT
#include "playlistmanager.h"
#include "playlistscout.h"
#include <QCollator>
#include <QDir>
#include <QFileInfo>
#include <QUrl>
PlaylistModel::PlaylistModel(QObject *parent)
: QAbstractListModel(parent)
{
}
PlaylistModel::~PlaylistModel()
= default;
void PlaylistModel::setPlaylist(const QList<QUrl> &urls)
{
beginResetModel();
m_playlist = urls;
endResetModel();
}
QModelIndex PlaylistModel::loadPlaylist(const QList<QUrl> & urls)
{
if (urls.isEmpty()) return {};
if (urls.count() == 1) {
return loadPlaylist(urls.constFirst());
} else {
setPlaylist(urls);
return index(0);
}
}
QModelIndex PlaylistModel::loadPlaylist(const QUrl &url)
{
QFileInfo info(url.toLocalFile());
QDir dir(info.path());
QString && currentFileName = info.fileName();
if (dir.path() == m_currentDir) {
int idx = indexOf(url);
return idx == -1 ? appendToPlaylist(url) : index(idx);
}
QStringList entryList = dir.entryList(
m_autoLoadSuffixes,
QDir::Files | QDir::NoSymLinks, QDir::NoSort);
QCollator collator;
collator.setNumericMode(true);
std::sort(entryList.begin(), entryList.end(), collator);
QList<QUrl> playlist;
int idx = -1;
for (int i = 0; i < entryList.count(); i++) {
const QString & fileName = entryList.at(i);
const QString & oneEntry = dir.absoluteFilePath(fileName);
const QUrl & url = QUrl::fromLocalFile(oneEntry);
playlist.append(url);
if (fileName == currentFileName) {
idx = i;
}
}
if (idx == -1) {
idx = playlist.count();
playlist.append(url);
}
m_currentDir = dir.path();
setPlaylist(playlist);
return index(idx);
}
QModelIndex PlaylistModel::appendToPlaylist(const QUrl &url)
{
const int lastIndex = rowCount();
beginInsertRows(QModelIndex(), lastIndex, lastIndex);
m_playlist.append(url);
endInsertRows();
return index(lastIndex);
}
bool PlaylistModel::removeAt(int index)
{
if (index < 0 || index >= rowCount()) return false;
beginRemoveRows(QModelIndex(), index, index);
m_playlist.removeAt(index);
endRemoveRows();
return true;
}
int PlaylistModel::indexOf(const QUrl &url) const
{
return m_playlist.indexOf(url);
}
QUrl PlaylistModel::urlByIndex(int index) const
{
return m_playlist.value(index);
}
QStringList PlaylistModel::autoLoadFilterSuffixes() const
{
return m_autoLoadSuffixes;
}
QHash<int, QByteArray> PlaylistModel::roleNames() const
{
QHash<int, QByteArray> result = QAbstractListModel::roleNames();
result.insert(UrlRole, "url");
return result;
}
int PlaylistModel::rowCount(const QModelIndex &parent) const
{
return m_playlist.count();
}
QVariant PlaylistModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid()) return {};
switch (role) {
case Qt::DisplayRole:
return m_playlist.at(index.row()).fileName();
case UrlRole:
return m_playlist.at(index.row());
}
return {};
}
PlaylistManager::PlaylistManager(QObject *parent)
: QObject(parent)
, m_currentIndex(std::nullopt)
, m_playlist()
, m_allowedSuffixes()
{
connect(&m_model, &PlaylistModel::rowsRemoved, this,
[this](const QModelIndex &, int, int) {
if (m_model.rowCount() <= m_currentIndex) {
setProperty("currentIndex", m_currentIndex - 1);
}
});
auto onRowCountChanged = [this](){
emit totalCountChanged(m_model.rowCount());
};
connect(&m_model, &PlaylistModel::rowsInserted, this, onRowCountChanged);
connect(&m_model, &PlaylistModel::rowsRemoved, this, onRowCountChanged);
connect(&m_model, &PlaylistModel::modelReset, this, onRowCountChanged);
}
PlaylistManager::~PlaylistManager()
@@ -162,35 +24,86 @@ PlaylistManager::~PlaylistManager()
}
PlaylistModel *PlaylistManager::model()
{
return &m_model;
// region: Allowed Suffixes Operations
const QStringList& PlaylistManager::allowedSuffixes() const {
return m_allowedSuffixes;
}
void PlaylistManager::setPlaylist(const QList<QUrl> &urls)
{
m_model.setPlaylist(urls);
void PlaylistManager::setAllowedSuffixes(const QStringList& nameFilters) {
m_allowedSuffixes = nameFilters;
emit allowedSuffixesChanged(m_allowedSuffixes);
}
QModelIndex PlaylistManager::loadPlaylist(const QList<QUrl> &urls)
{
QModelIndex idx = m_model.loadPlaylist(urls);
setProperty("currentIndex", idx.row());
return idx;
// endregion
// region: Playlist Loader and Reset
bool PlaylistManager::loadPlaylist(const QStringList& files) {
// Convert string list to url list first
QList<QUrl> urls;
for (const QString & file : std::as_const(files)) {
urls.append(urlFromString(file));
}
return loadPlaylist(urls);
}
QModelIndex PlaylistManager::loadPlaylist(const QUrl &url)
{
QModelIndex idx = m_model.loadPlaylist(url);
setProperty("currentIndex", idx.row());
return idx;
bool PlaylistManager::loadPlaylist(const QList<QUrl>& urls) {
// Check its count, and check M3U8 playlist for single file case.
if (urls.isEmpty()) {
clearPlaylist();
return true;
} else if (urls.count() == 1) {
const QUrl& firstUrl = urls.first();
const QString lowerCaseUrlPath(firstUrl.path().toLower());
if (lowerCaseUrlPath.endsWith(".m3u8") || lowerCaseUrlPath.endsWith(".m3u")) {
return loadM3U8Playlist(firstUrl);
} else {
return loadSingleFilePlaylist(firstUrl);
}
} else {
return loadMultipleFilesPlaylist(std::move(urls));
}
}
QModelIndex PlaylistManager::loadM3U8Playlist(const QUrl &url)
{
bool PlaylistManager::loadSingleFilePlaylist(const QUrl& url) {
// Check URL at first
if (!checkUrl(url))
return false;
// Scout playlist by given file
auto scoutResult = PlaylistScout::scout(url, m_allowedSuffixes);
if (!scoutResult.has_value()) {
return false;
}
// Setup playlist with index
setPlaylist(std::move(scoutResult->playlist), scoutResult->indexHint);
return true;
}
bool PlaylistManager::loadMultipleFilesPlaylist(const QList<QUrl>&& urls) {
// Check all of them are valid
for (const auto& url : std::as_const(urls)) {
if (!checkUrl(url)) {
return false;
}
}
// Set playlist and return
setPlaylist(std::move(urls));
return true;
}
bool PlaylistManager::loadM3U8Playlist(const QUrl& url) {
// Check URL at first
if (!checkUrl(url))
return false;
QList<QUrl> urls;
QFile file(url.toLocalFile());
if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
QList<QUrl> urls;
while (!file.atEnd()) {
QString line = file.readLine();
if (line.startsWith('#')) {
@@ -198,86 +111,185 @@ QModelIndex PlaylistManager::loadM3U8Playlist(const QUrl &url)
}
QFileInfo fileInfo(file);
QUrl item = QUrl::fromUserInput(line, fileInfo.absolutePath());
if (!checkUrl(item)) {
return false;
}
urls.append(item);
}
return loadPlaylist(urls);
} else {
return {};
return false;
}
setPlaylist(std::move(urls));
return true;
}
void PlaylistManager::setPlaylist(const QList<QUrl>&& urls, std::optional<qsizetype> indexHint) {
// Setup playlist
m_playlist = std::move(urls);
// Set index with hint
if (indexHint.has_value()) {
m_currentIndex = *indexHint;
}
sanitizeIndex();
// Raise signal
emit playlistChanged(totalCount());
}
void PlaylistManager::clearPlaylist() {
m_playlist.clear();
sanitizeIndex();
emit playlistChanged(this->totalCount());
}
// endregion
// region: Playlist Operations
void PlaylistManager::appendToPlaylist(const QString &file)
{
return appendToPlaylist(urlFromString(file));
}
void PlaylistManager::appendToPlaylist(const QUrl &url)
{
m_playlist.append(url);
sanitizeIndex();
emit playlistChanged(totalCount());
}
bool PlaylistManager::insertIntoPlaylist(qsizetype index, const QString &file)
{
return insertIntoPlaylist(index, urlFromString(file));
}
bool PlaylistManager::insertIntoPlaylist(qsizetype index, const QUrl &url)
{
if (index < 0 || index > totalCount()) {
return false;
} else {
m_playlist.insert(index, url);
sanitizeIndex();
emit playlistChanged(totalCount());
return true;
}
}
int PlaylistManager::totalCount() const
bool PlaylistManager::removeFromPlaylist(qsizetype index)
{
return m_model.rowCount();
if (index < 0 || index >= totalCount()) {
return false;
} else {
m_playlist.removeAt(index);
sanitizeIndex();
emit playlistChanged(totalCount());
return true;
}
}
QModelIndex PlaylistManager::previousIndex() const
{
int count = totalCount();
if (count == 0) return {};
// endregion
return m_model.index(isFirstIndex() ? count - 1 : m_currentIndex - 1);
// region: Index Operations
qsizetype PlaylistManager::totalCount() const
{
return m_playlist.count();
}
QModelIndex PlaylistManager::nextIndex() const
std::optional<qsizetype> PlaylistManager::previousIndex() const
{
int count = totalCount();
if (count == 0) return {};
return m_model.index(isLastIndex() ? 0 : m_currentIndex + 1);
if (!m_currentIndex.has_value() || totalCount() <= 0) return std::nullopt;
return (isFirstIndex() ? totalCount() - 1 : *m_currentIndex - 1);
}
QModelIndex PlaylistManager::curIndex() const
std::optional<qsizetype> PlaylistManager::nextIndex() const
{
return m_model.index(m_currentIndex);
if (!m_currentIndex.has_value() || totalCount() <= 0) return std::nullopt;
return (isLastIndex() ? 0 : *m_currentIndex + 1);
}
bool PlaylistManager::isFirstIndex() const
{
return m_currentIndex == 0;
return m_currentIndex.has_value() && *m_currentIndex == 0;
}
bool PlaylistManager::isLastIndex() const
{
return m_currentIndex + 1 == totalCount();
return m_currentIndex.has_value() && *m_currentIndex + 1 == totalCount();
}
void PlaylistManager::setCurrentIndex(const QModelIndex &index)
std::optional<qsizetype> PlaylistManager::currentIndex() const
{
if (index.isValid() && index.row() >= 0 && index.row() < totalCount()) {
setProperty("currentIndex", index.row());
return m_currentIndex;
}
bool PlaylistManager::setCurrentIndex(qsizetype index)
{
if (!m_currentIndex.has_value()) {
return false;
}
if (index < 0 || index >= totalCount()) {
return false;
}
if (*m_currentIndex == index) {
return true;
}
m_currentIndex = index;
emit currentIndexChanged(index);
return true;
}
QUrl PlaylistManager::urlByIndex(const QModelIndex &index)
QUrl PlaylistManager::urlByIndex(qsizetype index) const
{
return m_model.urlByIndex(index.row());
return m_playlist.value(index);
}
QString PlaylistManager::localFileByIndex(const QModelIndex &index)
QString PlaylistManager::localFileByIndex(qsizetype index) const
{
return urlByIndex(index).toLocalFile();
}
bool PlaylistManager::removeAt(const QModelIndex &index)
{
return m_model.removeAt(index.row());
}
// endregion
void PlaylistManager::setAutoLoadFilterSuffixes(const QStringList &nameFilters)
{
m_model.setProperty("autoLoadFilterSuffixes", nameFilters);
}
// region: Misc
QList<QUrl> PlaylistManager::convertToUrlList(const QStringList &files)
void PlaylistManager::sanitizeIndex()
{
QList<QUrl> urlList;
for (const QString & str : std::as_const(files)) {
QUrl url = QUrl::fromLocalFile(str);
if (url.isValid()) {
urlList.append(url);
auto cnt = totalCount();
if (cnt == 0) {
// If playlist is empty, current index should be invalid.
m_currentIndex = std::nullopt;
} else {
if (m_currentIndex.has_value()) {
// Clamp current index in playlist range.
// If it is already in range, not change was done.
auto currentIndex = *m_currentIndex;
if (currentIndex < 0) {
m_currentIndex = 0;
} else if (currentIndex >= cnt) {
m_currentIndex = cnt - 1;
} else {
; // No change.
}
} else {
// If there is no set value for current index,
// set it at the beginning of playlist.
m_currentIndex = 0;
}
}
return urlList;
}
bool PlaylistManager::checkUrl(const QUrl& url)
{
return url.isValid() && url.isLocalFile();
}
QUrl PlaylistManager::urlFromString(const QString& file)
{
return QUrl::fromLocalFile(file);
}
// endregion
+107 -65
View File
@@ -5,84 +5,126 @@
#pragma once
#include <QUrl>
#include <QAbstractListModel>
class PlaylistModel : public QAbstractListModel
{
Q_OBJECT
public:
enum PlaylistRole {
UrlRole = Qt::UserRole
};
Q_ENUM(PlaylistRole)
Q_PROPERTY(QStringList autoLoadFilterSuffixes MEMBER m_autoLoadSuffixes NOTIFY autoLoadFilterSuffixesChanged)
explicit PlaylistModel(QObject *parent = nullptr);
~PlaylistModel() override;
void setPlaylist(const QList<QUrl> & urls);
QModelIndex loadPlaylist(const QList<QUrl> & urls);
QModelIndex loadPlaylist(const QUrl & url);
QModelIndex appendToPlaylist(const QUrl & url);
bool removeAt(int index);
int indexOf(const QUrl & url) const;
QUrl urlByIndex(int index) const;
QStringList autoLoadFilterSuffixes() const;
QHash<int, QByteArray> roleNames() const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
signals:
void autoLoadFilterSuffixesChanged(QStringList suffixes);
private:
// model data
QList<QUrl> m_playlist;
// properties
QStringList m_autoLoadSuffixes = {};
// internal
QString m_currentDir;
};
#include <QObject>
#include <optional>
/// The manager of playlist.
class PlaylistManager : public QObject
{
Q_OBJECT
public:
Q_PROPERTY(int currentIndex MEMBER m_currentIndex NOTIFY currentIndexChanged)
Q_PROPERTY(QStringList autoLoadFilterSuffixes WRITE setAutoLoadFilterSuffixes)
Q_PROPERTY(PlaylistModel * model READ model CONSTANT)
explicit PlaylistManager(QObject *parent = nullptr);
/// Constructs a PlaylistManager with an optional parent QObject.
explicit PlaylistManager(QObject* parent = nullptr);
/// Destructor.
~PlaylistManager();
PlaylistModel * model();
/// Get the allowed file suffixes of this playlist.
/// @return The current list of allowed suffixes.
const QStringList& allowedSuffixes() const;
/// Set the allowed file suffixes of this playlist.
/// @remark This only works for "load the whole directory by one file" case.
/// @param[in] nameFilters The new list of allowed suffixes.
void setAllowedSuffixes(const QStringList& nameFilters);
void setPlaylist(const QList<QUrl> & url);
Q_INVOKABLE QModelIndex loadPlaylist(const QList<QUrl> & urls);
Q_INVOKABLE QModelIndex loadPlaylist(const QUrl & url);
Q_INVOKABLE QModelIndex loadM3U8Playlist(const QUrl & url);
/// Load user specified files as playlist.
/// @remark The wrapper for another overload.
/// @return True for successful load, otherwise false.
bool loadPlaylist(const QStringList& files);
/// Load user specified URLs as playlist.
/// @remark This function automatically dispatch these files into correct sub-load functions.
/// @return True for successful load, otherwise false.
bool loadPlaylist(const QList<QUrl>& urls);
/// Clear the entire playlist and reset current index.
void clearPlaylist();
int totalCount() const;
QModelIndex previousIndex() const;
QModelIndex nextIndex() const;
QModelIndex curIndex() const;
/// Append a local file path to the playlist.
void appendToPlaylist(const QString& file);
/// Append a URL to the playlist.
void appendToPlaylist(const QUrl& url);
/// Insert a local file path at the given index.
/// @return True if inserted successfully, false if index is out of range.
bool insertIntoPlaylist(qsizetype index, const QString& file);
/// Insert a URL at the given index.
/// @return True if inserted successfully, false if index is out of range.
bool insertIntoPlaylist(qsizetype index, const QUrl& url);
/// Remove the entry at the given index.
/// @return True if removed successfully, false if index is out of range.
bool removeFromPlaylist(qsizetype index);
/// Returns the total number of items in the playlist.
/// @return Total count of entries in the playlist.
qsizetype totalCount() const;
/// Try fetching previous index.
/// @details This previous index is gallery-loop-aware.
/// @return std::nullopt if playlist is empty, otherwise the previous index considering gallery loop.
std::optional<qsizetype> previousIndex() const;
/// Try fetching next index.
/// @details This next index is gallery-loop-aware.
/// @return std::nullopt if playlist is empty, otherwise the next index considering gallery loop.
std::optional<qsizetype> nextIndex() const;
/// Check whether current index is the first index.
/// @return False if current index is not the first index or playlist is empty, otherwise true.
bool isFirstIndex() const;
/// Check whether current index is the last index.
/// @return False if current index is not the last index or playlist is empty, otherwise true.
bool isLastIndex() const;
void setCurrentIndex(const QModelIndex & index);
QUrl urlByIndex(const QModelIndex & index);
QString localFileByIndex(const QModelIndex & index);
bool removeAt(const QModelIndex & index);
void setAutoLoadFilterSuffixes(const QStringList &nameFilters);
static QList<QUrl> convertToUrlList(const QStringList & files);
/// Returns the current index.
/// @return std::nullopt if the playlist is empty, otherwise the current index.
std::optional<qsizetype> currentIndex() const;
/// Set the current index.
/// @remark Index only can be set when playlist is not empty.
/// @return True if successful set, including "not changed" set, otherwise false.
/// This return value only indicates whether we can set index and given index is valid for setting.
/// If "no changed" set occurs, this function returns true, but doesn't raise current index changed signal.
bool setCurrentIndex(qsizetype index);
/// Returns the URL at the given index.
/// @return The URL if index is valid.
/// This class guarantee that this QUrl is pointing to a local file.
QUrl urlByIndex(qsizetype index) const;
/// Returns the local file path at the given index.
/// @return The local file path if index is valid.
/// This class guarantee that local file path always is valid.
QString localFileByIndex(qsizetype index) const;
signals:
void currentIndexChanged(int index);
void totalCountChanged(int count);
void allowedSuffixesChanged(const QStringList& suffixes);
/// Raised when index was changed bu setter.
/// The argument holds the new current index.
/// Please note that this signal will NOT be raised by the whole playlist change caused index change.
void currentIndexChanged(qsizetype index);
/// The whole playlist was changed, involving loading, editing and deleting some or full entries.
/// The argument holds the total count of new play list.
void playlistChanged(qsizetype count);
private:
int m_currentIndex = -1;
PlaylistModel m_model;
/// The fundamental playlist setter.
/// @details All playlist load functions will call this function to set playlist at last.
/// @param[in] urls ALl urls in this list should be checked by checkUrl() function.
/// @param[in] indexHint Try to set current index to this index if possible.
void setPlaylist(const QList<QUrl>&& urls, std::optional<qsizetype> indexHint = std::nullopt);
/// Load playlist from a single file.
/// @details This function scans the same directory for siblings matching allowed suffixes, sorted naturally.
/// @return True if the playlist was loaded successfully, false otherwise.
bool loadSingleFilePlaylist(const QUrl& url);
/// Load playlist from multiple URLs directly.
/// @return True if all URLs are valid, false otherwise.
bool loadMultipleFilesPlaylist(const QList<QUrl>&& urls);
/// Load playlist from an M3U/M3U8 playlist file.
/// @return True if loaded successfully, false otherwise.
bool loadM3U8Playlist(const QUrl& url);
/// Make sure that current index is valid.
/// @details This function usually is used by those functions manipulating playlist.
void sanitizeIndex();
/// Check whether given QUrl is proper to be put into playlist.
/// @details Only the url pointing to local file can be put into playlist.
static bool checkUrl(const QUrl& url);
/// Convert a string to local file into a QUrl.
/// @param[in] file The string pointing to local file.
static QUrl urlFromString(const QString& file);
std::optional<qsizetype> m_currentIndex;
QList<QUrl> m_playlist;
QStringList m_allowedSuffixes;
};
+25
View File
@@ -0,0 +1,25 @@
// SPDX-FileCopyrightText: 2026 yyc12345 <yyc12321@outlook.com>
//
// SPDX-License-Identifier: MIT
#include "playlistscout.h"
#include "playlistscouts/qtscout.h"
#include "playlistscouts/explorerscout.h"
std::optional<PlaylistScout::ScoutResult> PlaylistScout::scout(
const QUrl &url, const QStringList &allowedSuffixes) {
#ifdef Q_OS_WIN
// In Windows, Qt approach can not aware Windows Explorer custom sort order.
// So we need to try using Win32 functions for fetching it at first.
if (auto rv = PlaylistScout::Explorer::scout(url, allowedSuffixes); rv.has_value()) {
return rv;
}
#endif // Q_OS_WIN
// TODO: We may add macOS Finder, KDE Dolphin and etc supports in future.
// Qt way is the last fallback.
return PlaylistScout::Qt::scout(url, allowedSuffixes);
}
+31
View File
@@ -0,0 +1,31 @@
// SPDX-FileCopyrightText: 2026 yyc12345 <yyc12321@outlook.com>
//
// SPDX-License-Identifier: MIT
#pragma once
#include <QUrl>
#include <QStringList>
#include <QList>
#include <optional>
namespace PlaylistScout {
/// The result of scout
struct ScoutResult {
/// The final fetched playlist (it can be empty).
/// Each item must be a QUrl pointing to a local file.
QList<QUrl> playlist;
/// The index of given file located in playlist.
/// Nullopt if it is not presented in playlist.
std::optional<qsizetype> indexHint;
};
/// @brief Scout available image files in given file's parent dir with given allowed suffixes.
/// @param[in] url The path to file whose parent dir is our target dir for finding image files.
/// @param[in] allowedSuffixes
/// Each items in this list is a wildcard pattern for matching allowed file name, like \c *.png .
/// The fetched item will be preserved if its name matching any of them.
/// If this list is empty, there is no restrictions for finding files.
/// @return Scout result if success, otherwise nullopt.
std::optional<ScoutResult> scout(const QUrl &url, const QStringList &allowedSuffixes);
}
+332
View File
@@ -0,0 +1,332 @@
// SPDX-FileCopyrightText: 2026 yyc12345 <yyc12321@outlook.com>
//
// SPDX-License-Identifier: MIT
#include "explorerscout.h"
#ifdef Q_OS_WIN
#include <QDir>
#include <QDebug>
#include <QFileInfo>
#include <QRegularExpression>
#include <optional>
#include <exdisp.h> // IShellWindows, CLSID_ShellWindows
#include <servprov.h> // IServiceProvider
#include <shlguid.h> // SID_STopLevelBrowser
#include <shlobj.h>
// YYC MARK:
// We use custom RAII wrappers instead of existing COM smart pointers because the
// project may be built with MinGW, which does not have good compatibility with
// CComPtr and Microsoft's WRL ComPtr (<wrl/client.h>).
// Furthermore, PIDLs are not COM objects (they are freed via ILFree, not Release),
// so no standard smart pointer covers them regardless of compiler.
/// RAII wrapper for COM initialization (CoInitialize / CoUninitialize).
/// Must be declared before any COM objects (e.g., ComPtr, UniquePidl) so that
/// CoUninitialize runs after all COM objects are released during scope exit.
class ComGuard {
public:
ComGuard() : m_hr(CoInitialize(nullptr)) {}
~ComGuard() { if (SUCCEEDED(m_hr)) CoUninitialize(); }
ComGuard(const ComGuard&) = delete;
ComGuard& operator=(const ComGuard&) = delete;
bool IsInitialized() const { return SUCCEEDED(m_hr); }
private:
HRESULT m_hr;
};
/// RAII wrapper for COM interface pointers.
template <typename T>
class ComPtr {
public:
ComPtr() = default;
ComPtr(std::nullptr_t) {}
~ComPtr() { Reset(); }
ComPtr(const ComPtr&) = delete;
ComPtr& operator=(const ComPtr&) = delete;
ComPtr(ComPtr&& other) noexcept : m_ptr(other.m_ptr) { other.m_ptr = nullptr; }
ComPtr& operator=(ComPtr&& other) noexcept {
if (this != &other) {
Reset();
m_ptr = other.m_ptr;
other.m_ptr = nullptr;
}
return *this;
}
T* operator->() const { return m_ptr; }
T* Get() const { return m_ptr; }
T** GetAddressOf() { return &m_ptr; }
explicit operator bool() const { return m_ptr != nullptr; }
void Reset() { if (m_ptr) { m_ptr->Release(); m_ptr = nullptr; } }
private:
T* m_ptr = nullptr;
};
/// RAII wrapper for PIDL (Item ID List).
class UniquePidl {
public:
UniquePidl() = default;
explicit UniquePidl(LPITEMIDLIST pidl) : m_pidl(pidl) {}
~UniquePidl() { Reset(); }
UniquePidl(const UniquePidl&) = delete;
UniquePidl& operator=(const UniquePidl&) = delete;
UniquePidl(UniquePidl&& other) noexcept : m_pidl(other.m_pidl) { other.m_pidl = nullptr; }
UniquePidl& operator=(UniquePidl&& other) noexcept {
if (this != &other) {
Reset();
m_pidl = other.m_pidl;
other.m_pidl = nullptr;
}
return *this;
}
LPCITEMIDLIST Get() const { return m_pidl; }
LPITEMIDLIST* GetAddressOf() { return &m_pidl; }
explicit operator bool() const { return m_pidl != nullptr; }
void Reset() { if (m_pidl) { ILFree(m_pidl); m_pidl = nullptr; } }
private:
LPITEMIDLIST m_pidl = nullptr;
};
/// Outputs a diagnostic warning for debugging Windows Explorer integration failures.
static void ReportError(const QString& detail)
{
qWarning("Windows Explorer file list fetch error: %s",
qPrintable(detail));
}
/// Normalizes a Windows directory path for case-insensitive comparison:
/// converts to native separators, lowercases, and ensures a trailing backslash.
static QString NormalizePath(const QString& path)
{
QString result = QDir::toNativeSeparators(path).toLower();
if (!result.endsWith(QChar('\\'))) {
result += QChar('\\');
}
return result;
}
/// Checks whether the Explorer window represented by the given IDispatch is
/// displaying the target folder. Returns its IFolderView2 on match, or null.
static ComPtr<IFolderView2> TryMatchWindow(IDispatch* dispatch, const QString& normalizedTarget)
{
// Navigate: IDispatch -> IServiceProvider -> IShellBrowser.
ComPtr<IServiceProvider> serviceProvider;
if (FAILED(dispatch->QueryInterface(IID_PPV_ARGS(serviceProvider.GetAddressOf())))) {
return nullptr;
}
ComPtr<IShellBrowser> shellBrowser;
if (FAILED(serviceProvider->QueryService(SID_STopLevelBrowser,
IID_PPV_ARGS(shellBrowser.GetAddressOf())))) {
return nullptr;
}
// IShellBrowser -> active IShellView.
ComPtr<IShellView> shellView;
if (FAILED(shellBrowser->QueryActiveShellView(shellView.GetAddressOf()))) {
return nullptr;
}
// Retrieve the folder's path via IPersistIDList.
ComPtr<IPersistIDList> persistIdList;
if (FAILED(shellView->QueryInterface(IID_PPV_ARGS(persistIdList.GetAddressOf())))) {
return nullptr;
}
UniquePidl folderPidl;
if (FAILED(persistIdList->GetIDList(folderPidl.GetAddressOf()))) {
return nullptr;
}
PWSTR pathStr = nullptr;
if (FAILED(SHGetNameFromIDList(folderPidl.Get(), SIGDN_DESKTOPABSOLUTEPARSING, &pathStr))) {
return nullptr;
}
QString windowPath = NormalizePath(QString::fromWCharArray(pathStr));
CoTaskMemFree(pathStr);
if (windowPath != normalizedTarget) {
return nullptr;
}
// Path matches — query IFolderView2 from the ShellView.
ComPtr<IFolderView2> folderView2;
if (FAILED(shellView->QueryInterface(IID_PPV_ARGS(folderView2.GetAddressOf())))) {
return nullptr;
}
return folderView2;
}
/// Tries to find a live Explorer window that is currently displaying the given
/// folder path. Returns the IFolderView2 from that window, or an empty ComPtr.
///
/// Explorer's sort order is stored only in the running Explorer process's memory.
/// Creating a new shell view via IShellFolder::CreateViewObject yields a default
/// sort (always by name), so we must find the actual live IFolderView2 instead.
///
/// Reference:
/// - https://www.stephenli.ca/getting-windows-explorers-current-directory-sort-order
/// - https://github.com/d2phap/ImageGlass/blob/75a296287f338edcf409eee3917531b25ff5d927/v9/Components/ImageGlass.Base/FileSystem/FileFinder.cs
/// - NuGet Package: D2Phap.EggShell
static ComPtr<IFolderView2> FindLiveFolderView(const QString& targetFolderPath)
{
// Create the Shell windows collection (all open Explorer windows).
ComPtr<IShellWindows> shellWindows;
if (FAILED(CoCreateInstance(CLSID_ShellWindows, nullptr, CLSCTX_ALL,
IID_PPV_ARGS(shellWindows.GetAddressOf())))) {
ReportError("Failed to create IShellWindows instance");
return nullptr;
}
QString normalizedTarget = NormalizePath(targetFolderPath);
// 1. Try the Desktop window first — it may contain the file the user opened.
{
VARIANT varLoc;
VariantInit(&varLoc);
varLoc.vt = VT_I4;
varLoc.lVal = 0;
VARIANT varLocRoot;
VariantInit(&varLocRoot);
long desktopHwnd = 0;
ComPtr<IDispatch> desktopDispatch;
HRESULT findHr = shellWindows->FindWindowSW(
&varLoc, &varLocRoot, SWC_DESKTOP,
&desktopHwnd, SWFO_NEEDDISPATCH,
desktopDispatch.GetAddressOf());
VariantClear(&varLoc);
VariantClear(&varLocRoot);
if (SUCCEEDED(findHr) && desktopDispatch) {
ComPtr<IFolderView2> result = TryMatchWindow(desktopDispatch.Get(), normalizedTarget);
if (result) return result;
}
}
// 2. Iterate regular Explorer windows.
LONG count = 0;
shellWindows->get_Count(&count);
for (LONG i = 0; i < count; i++) {
VARIANT varIndex;
VariantInit(&varIndex);
varIndex.vt = VT_I4;
varIndex.lVal = i;
ComPtr<IDispatch> dispatch;
HRESULT itemHr = shellWindows->Item(varIndex, dispatch.GetAddressOf());
VariantClear(&varIndex);
if (FAILED(itemHr) || !dispatch) {
continue;
}
ComPtr<IFolderView2> result = TryMatchWindow(dispatch.Get(), normalizedTarget);
if (result) return result;
}
return nullptr;
}
std::optional<PlaylistScout::ScoutResult> PlaylistScout::Explorer::scout(
const QUrl &url, const QStringList &allowedSuffixes) {
PlaylistScout::ScoutResult rv;
QFileInfo info(url.toLocalFile());
QString folderPath = info.path();
QString currentFileName = info.fileName();
// Pre-compile wildcard suffix filters into regular expressions for performance.
QList<QRegularExpression> nameRegexps;
for (const QString &pattern : allowedSuffixes) {
nameRegexps.append(QRegularExpression(
QRegularExpression::wildcardToRegularExpression(pattern),
QRegularExpression::CaseInsensitiveOption));
}
// Initialize COM. The ComGuard must be declared before any COM objects
// below so that CoUninitialize runs after all of them are released.
ComGuard comGuard;
if (!comGuard.IsInitialized()) {
ReportError("COM initialization failed");
return std::nullopt;
}
// Find a live Explorer window that is currently displaying the target folder.
ComPtr<IFolderView2> folderView2 = FindLiveFolderView(folderPath);
if (!folderView2) {
ReportError("No open Explorer window matches the folder: " + folderPath);
return std::nullopt;
}
// Get items already sorted in Explorer's view order, as an IShellItemArray.
// SVGIO_ALLVIEW returns all items; SVGIO_FLAG_VIEWORDER respects the view's sort.
ComPtr<IShellItemArray> itemArray;
if (FAILED(folderView2->Items(SVGIO_ALLVIEW | SVGIO_FLAG_VIEWORDER,
IID_PPV_ARGS(itemArray.GetAddressOf())))) {
ReportError("IFolderView2::Items(SVGIO_ALLVIEW | SVGIO_FLAG_VIEWORDER) failed");
return std::nullopt;
}
DWORD itemCount = 0;
itemArray->GetCount(&itemCount);
// Enumerate sorted items, filter by pattern, and build the playlist.
for (DWORD i = 0; i < itemCount; i++) {
ComPtr<IShellItem> shellItem;
if (FAILED(itemArray->GetItemAt(i, shellItem.GetAddressOf()))) {
continue;
}
// Get the full parsing path of the item.
PWSTR pathStr = nullptr;
if (FAILED(shellItem->GetDisplayName(SIGDN_DESKTOPABSOLUTEPARSING, &pathStr)) || !pathStr) {
continue;
}
QString filePath = QString::fromWCharArray(pathStr);
CoTaskMemFree(pathStr);
// Skip directories — the view may include folders alongside files.
QFileInfo fileInfo(filePath);
if (!fileInfo.isFile()) {
continue;
}
QString fileName = fileInfo.fileName();
// Match against compiled wildcard patterns.
bool matches = nameRegexps.isEmpty();
for (const QRegularExpression &re : std::as_const(nameRegexps)) {
if (re.match(fileName).hasMatch()) {
matches = true;
break;
}
}
if (!matches) {
continue;
}
rv.playlist.append(QUrl::fromLocalFile(filePath));
if (fileName == currentFileName) {
rv.indexHint = rv.playlist.size() - 1;
}
}
return rv;
}
#endif // Q_OS_WIN
+16
View File
@@ -0,0 +1,16 @@
// SPDX-FileCopyrightText: 2026 yyc12345 <yyc12321@outlook.com>
//
// SPDX-License-Identifier: MIT
#pragma once
#include "../playlistscout.h"
#include <QtSystemDetection>
#ifdef Q_OS_WIN
namespace PlaylistScout::Explorer {
std::optional<ScoutResult> scout(const QUrl &url, const QStringList &allowedSuffixes);
}
#endif // Q_OS_WIN
+40
View File
@@ -0,0 +1,40 @@
// SPDX-FileCopyrightText: 2025 Gary Wang <git@blumia.net>
// SPDX-FileCopyrightText: 2026 yyc12345 <yyc12321@outlook.com>
//
// SPDX-License-Identifier: MIT
#include "qtscout.h"
#include <QFileInfo>
#include <QDir>
#include <QCollator>
std::optional<PlaylistScout::ScoutResult> PlaylistScout::Qt::scout(
const QUrl &url, const QStringList &allowedSuffixes) {
PlaylistScout::ScoutResult rv;
QFileInfo info(url.toLocalFile());
QDir dir(info.path());
QString currentFileName = info.fileName();
QStringList entryList = dir.entryList(
allowedSuffixes, QDir::Files | QDir::NoSymLinks, QDir::NoSort);
QCollator collator;
collator.setNumericMode(true);
std::sort(entryList.begin(), entryList.end(), collator);
for (qsizetype i = 0; i < entryList.count(); i++) {
const QString &fileName = entryList.at(i);
const QString &oneEntry = dir.absoluteFilePath(fileName);
const QUrl &url = QUrl::fromLocalFile(oneEntry);
rv.playlist.append(url);
if (fileName == currentFileName) {
rv.indexHint = i;
}
}
return rv;
}
+11
View File
@@ -0,0 +1,11 @@
// SPDX-FileCopyrightText: 2026 yyc12345 <yyc12321@outlook.com>
//
// SPDX-License-Identifier: MIT
#pragma once
#include "../playlistscout.h"
namespace PlaylistScout::Qt {
std::optional<ScoutResult> scout(const QUrl &url, const QStringList &allowedSuffixes);
}
-12
View File
@@ -65,12 +65,6 @@ bool Settings::svgTiny12Only() const
#endif // QT_VERSION < QT_VERSION_CHECK(6, 9, 3)
}
Settings::DoubleClickBehavior Settings::doubleClickBehavior() const
{
QString result = m_qsettings->value("double_click_behavior", "").toString();
return QEnumHelper::fromString<DoubleClickBehavior>(result, DoubleClickBehavior::Ignore);
}
Settings::MouseWheelBehavior Settings::mouseWheelBehavior() const
{
QString result = m_qsettings->value("mouse_wheel_behavior", "").toString();
@@ -107,12 +101,6 @@ void Settings::setSvgTiny12Only(bool 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));
-12
View File
@@ -11,23 +11,13 @@ class Settings : public QObject
{
Q_OBJECT
public:
enum DoubleClickBehavior {
Ignore,
Close,
Maximize,
FullScreen,
};
Q_ENUM(DoubleClickBehavior)
enum MouseWheelBehavior {
Zoom,
Switch,
};
Q_ENUM(MouseWheelBehavior)
// TODO: I want to remove Auto behavior in future.
enum WindowSizeBehavior {
Auto,
Maximized,
Windowed,
};
@@ -38,7 +28,6 @@ public:
bool useLightCheckerboard() const;
bool loopGallery() const;
bool svgTiny12Only() const;
DoubleClickBehavior doubleClickBehavior() const;
MouseWheelBehavior mouseWheelBehavior() const;
WindowSizeBehavior initWindowSizeBehavior() const;
Qt::HighDpiScaleFactorRoundingPolicy hiDpiScaleFactorBehavior() const;
@@ -46,7 +35,6 @@ public:
void setUseLightCheckerboard(bool light);
void setLoopGallery(bool on);
void setSvgTiny12Only(bool on);
void setDoubleClickBehavior(DoubleClickBehavior dcb);
void setMouseWheelBehavior(MouseWheelBehavior mwb);
void setInitWindowSizeBehavior(WindowSizeBehavior wsb);
void setHiDpiScaleFactorBehavior(Qt::HighDpiScaleFactorRoundingPolicy hidpi);
+1 -18
View File
@@ -49,15 +49,6 @@ private:
Pairs pairs;
};
static EnumComboBoxTransformer<Settings::DoubleClickBehavior> DC_OPTIONS {
{
{Settings::DoubleClickBehavior::Ignore, QCoreApplication::translate("SettingsDialog", "Do nothing")},
{Settings::DoubleClickBehavior::Close, QCoreApplication::translate("SettingsDialog", "Close the window")},
{Settings::DoubleClickBehavior::Maximize, QCoreApplication::translate("SettingsDialog", "Toggle maximize")},
{Settings::DoubleClickBehavior::FullScreen, QCoreApplication::translate("SettingsDialog", "Toggle fullscreen")}
}
};
static EnumComboBoxTransformer<Settings::MouseWheelBehavior, QString> MW_OPTIONS {
{
{ Settings::MouseWheelBehavior::Zoom, QCoreApplication::translate("SettingsDialog", "Zoom in and out") },
@@ -67,7 +58,6 @@ static EnumComboBoxTransformer<Settings::MouseWheelBehavior, QString> MW_OPTIONS
static EnumComboBoxTransformer<Settings::WindowSizeBehavior, QString> IWS_OPTIONS {
{
{ Settings::WindowSizeBehavior::Auto, QCoreApplication::translate("SettingsDialog", "Auto size") },
{ Settings::WindowSizeBehavior::Maximized, QCoreApplication::translate("SettingsDialog", "Maximized") },
{ Settings::WindowSizeBehavior::Windowed, QCoreApplication::translate("SettingsDialog", "Windowed") }
}
@@ -83,7 +73,7 @@ static EnumComboBoxTransformer<Qt::HighDpiScaleFactorRoundingPolicy, QString> HI
};
SettingsDialog::SettingsDialog(QWidget *parent)
: QDialog(parent), ui(new Ui::SettingsDialog)
: QDialog(parent), ui(new QT_PREPEND_NAMESPACE(Ui::SettingsDialog))
{
ui->setupUi(this);
setWindowFlag(Qt::WindowContextHelpButtonHint, false);
@@ -92,9 +82,6 @@ SettingsDialog::SettingsDialog(QWidget *parent)
ui->m_useLightCheckerboard->setChecked(settings->useLightCheckerboard());
ui->m_loopGallery->setChecked(settings->loopGallery());
ui->m_svgTiny12Only->setChecked(settings->svgTiny12Only());
ui->m_doubleClickBehavior->setModel(DC_OPTIONS.buildComboBoxModel(this));
Settings::DoubleClickBehavior dcb = settings->doubleClickBehavior();
ui->m_doubleClickBehavior->setCurrentIndex(DC_OPTIONS.to_index(dcb));
ui->m_mouseWheelBehavior->setModel(MW_OPTIONS.buildComboBoxModel(this));
Settings::MouseWheelBehavior mwb = settings->mouseWheelBehavior();
ui->m_mouseWheelBehavior->setCurrentIndex(MW_OPTIONS.to_index(mwb));
@@ -125,10 +112,6 @@ SettingsDialog::SettingsDialog(QWidget *parent)
Settings::instance()->setSvgTiny12Only(state == Qt::Checked);
});
connect(ui->m_doubleClickBehavior, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [ = ](int index){
Settings::instance()->setDoubleClickBehavior(DC_OPTIONS.to_value(index));
});
connect(ui->m_mouseWheelBehavior, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [ = ](int index){
Settings::instance()->setMouseWheelBehavior(MW_OPTIONS.to_value(index));
});
+5 -15
View File
@@ -41,43 +41,33 @@
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="m_doubleClickBehaviorLabel">
<property name="text">
<string>Double-click behavior</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="m_doubleClickBehavior"/>
</item>
<item row="4" column="0">
<widget class="QLabel" name="m_mouseWheelBehaviorLabel">
<property name="text">
<string>Mouse wheel behavior</string>
</property>
</widget>
</item>
<item row="4" column="1">
<item row="3" column="1">
<widget class="QComboBox" name="m_mouseWheelBehavior"/>
</item>
<item row="5" column="0">
<item row="4" column="0">
<widget class="QLabel" name="m_initWindowSizeBehaviorLabel">
<property name="text">
<string>Default window size</string>
</property>
</widget>
</item>
<item row="5" column="1">
<item row="4" column="1">
<widget class="QComboBox" name="m_initWindowSizeBehavior"/>
</item>
<item row="6" column="0">
<item row="5" column="0">
<widget class="QLabel" name="m_hiDpiRoundingPolicyBehaviorLabel">
<property name="text">
<string>HiDPI scale factor rounding policy</string>
</property>
</widget>
</item>
<item row="6" column="1">
<item row="5" column="1">
<widget class="QComboBox" name="m_hiDpiRoundingPolicyBehavior"/>
</item>
</layout>
+5
View File
@@ -21,4 +21,9 @@
<br/>
<span>%9</span>
</li>
<li>
<b>%10</b><span>:</span>
<br/>
<span>%11</span>
</li>
</ul>
-21
View File
@@ -1,21 +0,0 @@
<ul>
<li><u>Catalan</u>: Toni Estévez</li>
<li><u>Chinese (Simplified)</u>: Percy Hong</li>
<li><u>Dutch</u>: Heimen Stoffels</li>
<li><u>French</u>: J. Lavoie, K. Herbert, Maxime Leroy</li>
<li><u>German</u>: K. Herbert, J. Lavoie, sal0max</li>
<li><u>Indonesian</u>: liimee, Reza Almanda</li>
<li><u>Italian</u>: albanobattistella</li>
<li><u>Japanese</u>: Black Cat, mmahhi, Percy Hong</li>
<li><u>Korean</u>: VenusGirl</li>
<li><u>Norwegian Bokmål</u>: Allan Nordhøy, ovl-1</li>
<li><u>Punjabi (Pakistan)</u>: bgo-eiu</li>
<li><u>Russian</u>: Sergey Shornikov, Artem, Andrey</li>
<li><u>Sinhala</u>: HelaBasa</li>
<li><u>Slovenian</u>: Andrej Poženel - SDT</li>
<li><u>Spanish</u>: Toni Estévez, Génesis Toxical, William(ѕ)ⁿ, gallegonovato</li>
<li><u>Tamil</u>: தமிழ்நேரம் (TamilNeram)</li>
<li><u>Turkish</u>: E-Akcaer, Oğuz Ersen, Sabri Ünal</li>
<li><u>Ukrainian</u>: Dan, volkov, Сергій</li>
<li><u>Vietnamese</u>: Loc Huynh</li>
</ul>