Compare commits

...
11 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
24 changed files with 788 additions and 382 deletions
+12 -2
View File
@@ -39,7 +39,6 @@ endif ()
set (PPIC_CPP_FILES set (PPIC_CPP_FILES
app/main.cpp app/main.cpp
app/framelesswindow.cpp
app/mainwindow.cpp app/mainwindow.cpp
app/actionmanager.cpp app/actionmanager.cpp
app/graphicsview.cpp app/graphicsview.cpp
@@ -54,10 +53,12 @@ set (PPIC_CPP_FILES
app/exiv2wrapper.cpp app/exiv2wrapper.cpp
app/playlistmanager.cpp app/playlistmanager.cpp
app/fileopeneventhandler.cpp app/fileopeneventhandler.cpp
app/playlistscout.cpp
app/playlistscouts/qtscout.cpp
app/playlistscouts/explorerscout.cpp
) )
set (PPIC_HEADER_FILES set (PPIC_HEADER_FILES
app/framelesswindow.h
app/mainwindow.h app/mainwindow.h
app/actionmanager.h app/actionmanager.h
app/graphicsview.h app/graphicsview.h
@@ -72,12 +73,16 @@ set (PPIC_HEADER_FILES
app/exiv2wrapper.h app/exiv2wrapper.h
app/playlistmanager.h app/playlistmanager.h
app/fileopeneventhandler.h app/fileopeneventhandler.h
app/playlistscout.h
app/playlistscouts/qtscout.h
app/playlistscouts/explorerscout.h
) )
set (PPIC_UI_FILES set (PPIC_UI_FILES
app/settingsdialog.ui app/settingsdialog.ui
app/aboutdialog.ui app/aboutdialog.ui
app/metadatadialog.ui app/metadatadialog.ui
app/mainwindow.ui
) )
set (PPIC_QRC_FILES set (PPIC_QRC_FILES
@@ -119,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) qt_add_translations(${EXE_NAME} ${ADD_TRANSLATIONS_ADDITIONAL_ARGS} TS_FILES ${PPIC_TS_FILES} QM_FILES_OUTPUT_VARIABLE PPIC_QM_FILES)
endif() 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_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) target_link_libraries (${EXE_NAME} Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Svg Qt${QT_VERSION_MAJOR}::SvgWidgets)
+2 -2
View File
@@ -55,7 +55,7 @@ void ActionManager::setupAction(MainWindow *mainWindow)
// TODO: We may replace all Qt theme icons to our custom icon for visual consistency. // TODO: We may replace all Qt theme icons to our custom icon for visual consistency.
// TODO: Some icons are invalid. // TODO: Some icons are invalid.
CREATE_NEW_THEMEICON_ACTION(mainWindow, actionCopyPixmap, edit-copy); CREATE_NEW_ACTION(mainWindow, actionCopyPixmap);
CREATE_NEW_ACTION(mainWindow, actionCopyFilePath); CREATE_NEW_ACTION(mainWindow, actionCopyFilePath);
CREATE_NEW_ICON_ACTION(mainWindow, actionPrevPicture, go-previous); CREATE_NEW_ICON_ACTION(mainWindow, actionPrevPicture, go-previous);
@@ -74,7 +74,7 @@ void ActionManager::setupAction(MainWindow *mainWindow)
CREATE_NEW_ICON_ACTION(mainWindow, actionRotateCounterClockwise, app-icon); CREATE_NEW_ICON_ACTION(mainWindow, actionRotateCounterClockwise, app-icon);
CREATE_NEW_ACTION(mainWindow, actionToggleAvoidResetTransform); CREATE_NEW_ACTION(mainWindow, actionToggleAvoidResetTransform);
CREATE_NEW_ICON_ACTION(mainWindow, actionToggleCheckerboard, view-background-checkerboard); CREATE_NEW_ACTION(mainWindow, actionToggleCheckerboard);
CREATE_NEW_ACTION(mainWindow, actionTogglePauseAnimation); CREATE_NEW_ACTION(mainWindow, actionTogglePauseAnimation);
CREATE_NEW_ACTION(mainWindow, actionAnimationNextFrame); CREATE_NEW_ACTION(mainWindow, actionAnimationNextFrame);
+7 -1
View File
@@ -8,7 +8,7 @@
#include <QVBoxLayout> #include <QVBoxLayout>
#include <QDebug> #include <QDebug>
BottomButtonGroup::BottomButtonGroup(const std::vector<QAction *> &actionList, QWidget *parent) BottomButtonGroup::BottomButtonGroup(QWidget *parent)
: QGroupBox (parent) : QGroupBox (parent)
{ {
QHBoxLayout * mainLayout = new QHBoxLayout(this); QHBoxLayout * mainLayout = new QHBoxLayout(this);
@@ -25,7 +25,9 @@ QToolButton {
border-style: none; border-style: none;
} }
)#"); )#");
}
void BottomButtonGroup::addActions(const std::vector<QAction *> & actionList) {
for (QAction * action : actionList) { for (QAction * action : actionList) {
// Create button // Create button
QToolButton * btn = new QToolButton(this); QToolButton * btn = new QToolButton(this);
@@ -37,3 +39,7 @@ QToolButton {
} }
updateGeometry(); updateGeometry();
} }
void BottomButtonGroup::addAction(QAction* action) {
addActions({action});
}
+4 -1
View File
@@ -14,7 +14,10 @@ class BottomButtonGroup : public QGroupBox
{ {
Q_OBJECT Q_OBJECT
public: public:
explicit BottomButtonGroup(const std::vector<QAction *> & actionList, QWidget *parent = nullptr); explicit BottomButtonGroup(QWidget *parent = nullptr);
void addActions(const std::vector<QAction *> & actionList);
void addAction(QAction* action);
}; };
#endif // BOTTOMBUTTONGROUP_H #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
+1 -1
View File
@@ -38,7 +38,7 @@ public:
QPixmap renderToPixmap(); QPixmap renderToPixmap();
public slots: private slots:
void updateStyle(Qt::ColorScheme colorScheme); void updateStyle(Qt::ColorScheme colorScheme);
private: private:
+12 -1
View File
@@ -224,7 +224,18 @@ void GraphicsView::fitByOrientation(Qt::Orientation ori, bool scaleDownOnly)
QRectF originalScene = sceneRect(); QRectF originalScene = sceneRect();
QTransform currentTransform = transform(); QTransform currentTransform = transform();
// TODO: May be optimized by matrix arithmetic operation? // 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) { if (ori == Qt::Horizontal) {
// For horizontal fit (fit by width), position at top (for tall images) // For horizontal fit (fit by width), position at top (for tall images)
+1 -1
View File
@@ -52,7 +52,7 @@ signals:
void navigatorViewRequired(bool required, QTransform transform); void navigatorViewRequired(bool required, QTransform transform);
void viewportRectChanged(); void viewportRectChanged();
public slots: private slots:
void updateStyle(Qt::ColorScheme colorScheme); void updateStyle(Qt::ColorScheme colorScheme);
private: private:
+100 -203
View File
@@ -3,6 +3,7 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
#include "mainwindow.h" #include "mainwindow.h"
#include "ui_mainwindow.h"
#include "settings.h" #include "settings.h"
#include "bottombuttongroup.h" #include "bottombuttongroup.h"
@@ -46,51 +47,50 @@
using namespace Qt::Literals::StringLiterals; using namespace Qt::Literals::StringLiterals;
MainWindow::MainWindow(QWidget *parent) MainWindow::MainWindow(QWidget *parent)
: FramelessWindow(parent) : QWidget(parent)
, ui(new QT_PREPEND_NAMESPACE(Ui::MainWindow))
, m_am(new ActionManager) , m_am(new ActionManager)
, m_pm(new PlaylistManager(this)) , m_pm(new PlaylistManager(this))
, m_fileSystemWatcher(new QFileSystemWatcher(this)) , m_fileSystemWatcher(new QFileSystemWatcher(this))
{ {
ui->setupUi(this);
this->setWindowFlags(Qt::Window | Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::WindowMinMaxButtonsHint | Qt::WindowCloseButtonHint);
// YYC MARK: // YYC MARK:
// Blumis set original value is 350 x 330. // Blumis set original value is 350 x 330.
// It is too small for modern device, // It is too small for modern device,
// so I level it up to standard VGA resolution 640 x 480. // so I level it up to SVGA resolution 800 x 600.
this->setMinimumSize(640, 480); this->setMinimumSize(800, 600);
this->setWindowIcon(QIcon(u":/icons/app-icon.svg"_s)); this->setWindowIcon(QIcon(u":/icons/app-icon.svg"_s));
this->setAcceptDrops(true); this->setAcceptDrops(true);
// Initialize playlist manager
m_pm->setAllowedSuffixes(supportedImageFormats()); m_pm->setAllowedSuffixes(supportedImageFormats());
// Initialize graphics scene
GraphicsScene * scene = new GraphicsScene(this); GraphicsScene * scene = new GraphicsScene(this);
// Setup graphics view (main photo view)
m_gv = new GraphicsView(this); ui->m_gv->setScene(scene);
m_gv->setScene(scene); // Setup navigator view (right-bottom corner thumbnail view)
this->setCentralWidget(m_gv); ui->m_nav->setScene(scene);
ui->m_nav->setMainView(ui->m_gv);
m_nav = new NavigatorView(this); ui->m_nav->fitInView(ui->m_nav->sceneRect(), Qt::KeepAspectRatio);
// YYC MARK: // Binding graphics and navigator views signals
// Blumia set original value is 220 x 160. connect(ui->m_gv, &GraphicsView::navigatorViewRequired,
// 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,
this, [this](bool required, const QTransform & tf){ this, [this](bool required, const QTransform & tf){
m_nav->setTransform(GraphicsView::resetScale(tf)); ui->m_nav->setTransform(GraphicsView::resetScale(tf));
m_nav->fitInView(m_nav->sceneRect(), Qt::KeepAspectRatio); ui->m_nav->fitInView(ui->m_nav->sceneRect(), Qt::KeepAspectRatio);
m_nav->setVisible(required); ui->m_nav->setVisible(required);
m_nav->updateMainViewportRegion(); 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, // Setup action manager
m_nav, &NavigatorView::updateMainViewportRegion);
m_am->setupAction(this); m_am->setupAction(this);
// Setup bottom button group
m_bottomButtonGroup = new BottomButtonGroup({ ui->m_bottomButtonGroup->addActions({
m_am->actionActualSize, m_am->actionActualSize,
m_am->actionFitToScreen, m_am->actionFitToScreen,
m_am->actionZoomIn, m_am->actionZoomIn,
@@ -101,55 +101,60 @@ MainWindow::MainWindow(QWidget *parent)
m_am->actionRotateCounterClockwise, m_am->actionRotateCounterClockwise,
m_am->actionRotateClockwise, m_am->actionRotateClockwise,
m_am->actionFlipHorizontal, m_am->actionFlipHorizontal,
m_am->actionToggleCheckerboard, m_am->actionFlipVertical,
}, this); });
// Setup right click menus
m_menu = new QMenu(this);
{ {
m_menu->addAction(m_am->actionCopyPixmap); ui->m_subMenuCopy->addAction(m_am->actionCopyPixmap);
m_menu->addAction(m_am->actionCopyFilePath); 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);
m_menu->addSeparator(); ui->m_subMenuTransform->addSeparator();
m_menu->addAction(m_am->actionPrevPicture); ui->m_subMenuTransform->addAction(m_am->actionFlipHorizontal);
m_menu->addAction(m_am->actionNextPicture); 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);
m_menu->addSeparator(); ui->m_menu->addSeparator();
m_menu->addAction(m_am->actionActualSize); ui->m_menu->addAction(m_am->actionPrevPicture);
m_menu->addAction(m_am->actionFitToScreen); ui->m_menu->addAction(m_am->actionNextPicture);
m_menu->addAction(m_am->actionFitByWidth);
m_menu->addAction(m_am->actionFitByHeight);
m_menu->addAction(m_am->actionZoomIn);
m_menu->addAction(m_am->actionZoomOut);
m_menu->addSeparator(); ui->m_menu->addSeparator();
m_menu->addAction(m_am->actionFlipHorizontal); ui->m_menu->addMenu(ui->m_subMenuTransform);
m_menu->addAction(m_am->actionFlipVertical);
m_menu->addAction(m_am->actionRotateClockwise);
m_menu->addAction(m_am->actionRotateCounterClockwise);
m_menu->addSeparator(); ui->m_menu->addSeparator();
m_menu->addAction(m_am->actionToggleAvoidResetTransform); ui->m_menu->addAction(m_am->actionToggleAvoidResetTransform);
m_menu->addAction(m_am->actionToggleCheckerboard); ui->m_menu->addAction(m_am->actionToggleCheckerboard);
m_menu->addAction(m_am->actionTogglePauseAnimation); ui->m_menu->addAction(m_am->actionTogglePauseAnimation);
m_menu->addAction(m_am->actionAnimationNextFrame); ui->m_menu->addAction(m_am->actionAnimationNextFrame);
m_menu->addSeparator(); ui->m_menu->addSeparator();
m_menu->addAction(m_am->actionTrash); ui->m_menu->addAction(m_am->actionTrash);
m_menu->addAction(m_am->actionLocateInFileManager); ui->m_menu->addAction(m_am->actionLocateInFileManager);
m_menu->addAction(m_am->actionProperties); ui->m_menu->addAction(m_am->actionProperties);
m_menu->addSeparator(); ui->m_menu->addSeparator();
m_menu->addAction(m_am->actionSettings); ui->m_menu->addAction(m_am->actionSettings);
m_menu->addAction(m_am->actionHelp); 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::updateActionState, this));
connect(m_pm, &PlaylistManager::playlistChanged, this, std::bind(&MainWindow::galleryCurrent, this, false)); connect(m_pm, &PlaylistManager::playlistChanged, this, std::bind(&MainWindow::galleryCurrent, this, false));
@@ -159,10 +164,7 @@ MainWindow::MainWindow(QWidget *parent)
QTimer::singleShot(500, this, std::bind(&MainWindow::galleryCurrent, this, true)); QTimer::singleShot(500, this, std::bind(&MainWindow::galleryCurrent, this, true));
}); });
QShortcut * fullscreenShorucut = new QShortcut(QKeySequence(QKeySequence::FullScreen), this); // Change window location
connect(fullscreenShorucut, &QShortcut::activated,
this, &MainWindow::toggleFullscreen);
centerWindow(); centerWindow();
QTimer::singleShot(0, this, [this](){ QTimer::singleShot(0, this, [this](){
@@ -173,66 +175,31 @@ MainWindow::MainWindow(QWidget *parent)
MainWindow::~MainWindow() MainWindow::~MainWindow()
{ {
delete ui;
} }
void MainWindow::showFiles(const QStringList &files) void MainWindow::showFiles(const QStringList &files)
{ {
m_pm->loadPlaylist(files); m_pm->loadPlaylist(files);
m_nav->fitInView(m_nav->sceneRect(), Qt::KeepAspectRatio); ui->m_nav->fitInView(ui->m_nav->sceneRect(), Qt::KeepAspectRatio);
} }
void MainWindow::showFiles(const QList<QUrl>& urls) void MainWindow::showFiles(const QList<QUrl>& urls)
{ {
m_pm->loadPlaylist(urls); m_pm->loadPlaylist(urls);
m_nav->fitInView(m_nav->sceneRect(), Qt::KeepAspectRatio); ui->m_nav->fitInView(ui->m_nav->sceneRect(), Qt::KeepAspectRatio);
} }
void MainWindow::initWindowSize() void MainWindow::initWindowSize()
{ {
switch (Settings::instance()->initWindowSizeBehavior()) { switch (Settings::instance()->initWindowSizeBehavior()) {
case Settings::WindowSizeBehavior::Auto:
adjustWindowSizeBySceneRect();
break;
case Settings::WindowSizeBehavior::Maximized: case Settings::WindowSizeBehavior::Maximized:
showMaximized(); showMaximized();
break; break;
case Settings::WindowSizeBehavior::Windowed: case Settings::WindowSizeBehavior::Windowed:
default:
showNormal(); showNormal();
break; 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();
}
} }
} }
@@ -268,13 +235,13 @@ void MainWindow::galleryCurrent(bool fromFileWatcher)
auto index = m_pm->currentIndex(); auto index = m_pm->currentIndex();
if (index.has_value()) { if (index.has_value()) {
QString localFilePath = m_pm->localFileByIndex(*index); QString localFilePath = m_pm->localFileByIndex(*index);
m_gv->showFileFromPath(localFilePath); ui->m_gv->showFileFromPath(localFilePath);
if (!fromFileWatcher) { if (!fromFileWatcher) {
updateFileWatcher(localFilePath); updateFileWatcher(localFilePath);
setWindowTitle(m_pm->urlByIndex(*index).fileName()); setWindowTitle(m_pm->urlByIndex(*index).fileName());
} }
} else { } else {
m_gv->showText(tr("Drag image here")); ui->m_gv->showText(tr("Drag image here"));
setWindowTitle(QString()); setWindowTitle(QString());
updateFileWatcher(); updateFileWatcher();
} }
@@ -295,53 +262,6 @@ QStringList MainWindow::supportedImageFormats()
return formatFilters; 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) void MainWindow::wheelEvent(QWheelEvent *event)
{ {
QPoint numDegrees = event->angleDelta() / 8; QPoint numDegrees = event->angleDelta() / 8;
@@ -374,22 +294,15 @@ void MainWindow::wheelEvent(QWheelEvent *event)
} }
event->accept(); event->accept();
} else { } else {
FramelessWindow::wheelEvent(event); QWidget::wheelEvent(event);
} }
} }
void MainWindow::resizeEvent(QResizeEvent *event)
{
updateWidgetsPosition();
return FramelessWindow::resizeEvent(event);
}
void MainWindow::contextMenuEvent(QContextMenuEvent *event) void MainWindow::contextMenuEvent(QContextMenuEvent *event)
{ {
m_menu->exec(mapToGlobal(event->pos())); ui->m_menu->exec(mapToGlobal(event->pos()));
return FramelessWindow::contextMenuEvent(event); return QWidget::contextMenuEvent(event);
} }
void MainWindow::dragEnterEvent(QDragEnterEvent *event) void MainWindow::dragEnterEvent(QDragEnterEvent *event)
@@ -400,7 +313,7 @@ void MainWindow::dragEnterEvent(QDragEnterEvent *event)
event->ignore(); event->ignore();
} }
return FramelessWindow::dragEnterEvent(event); return QWidget::dragEnterEvent(event);
} }
void MainWindow::dragMoveEvent(QDragMoveEvent *event) void MainWindow::dragMoveEvent(QDragMoveEvent *event)
@@ -416,7 +329,7 @@ void MainWindow::dropEvent(QDropEvent *event)
if (mimeData->hasUrls()) { if (mimeData->hasUrls()) {
showFiles(mimeData->urls()); showFiles(mimeData->urls());
} else { } 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()));
} }
} }
@@ -432,25 +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() void MainWindow::toggleAvoidResetTransform()
{ {
m_gv->setAvoidResetTransform(!m_gv->isAvoidResetTransform()); ui->m_gv->setAvoidResetTransform(!ui->m_gv->isAvoidResetTransform());
}
void MainWindow::quitAppAction(bool force)
{
closeWindow();
} }
void MainWindow::toggleFullscreen() void MainWindow::toggleFullscreen()
@@ -481,7 +378,7 @@ QSize MainWindow::sizeHint() const
void MainWindow::on_actionCopyPixmap_triggered() void MainWindow::on_actionCopyPixmap_triggered()
{ {
QClipboard *cb = QApplication::clipboard(); QClipboard *cb = QApplication::clipboard();
cb->setPixmap(m_gv->scene()->renderToPixmap()); cb->setPixmap(ui->m_gv->scene()->renderToPixmap());
} }
void MainWindow::on_actionCopyFilePath_triggered() void MainWindow::on_actionCopyFilePath_triggered()
@@ -505,58 +402,58 @@ void MainWindow::on_actionNextPicture_triggered()
void MainWindow::on_actionActualSize_triggered() void MainWindow::on_actionActualSize_triggered()
{ {
m_gv->resetScale(); ui->m_gv->resetScale();
m_gv->setEnableAutoFitToScreen(false); ui->m_gv->setEnableAutoFitToScreen(false);
} }
void MainWindow::on_actionFitToScreen_triggered() void MainWindow::on_actionFitToScreen_triggered()
{ {
m_gv->fitInView(m_nav->sceneRect(), Qt::KeepAspectRatio); ui->m_gv->fitInView(ui->m_nav->sceneRect(), Qt::KeepAspectRatio);
m_gv->setEnableAutoFitToScreen(m_gv->scaleFactor() <= 1); ui->m_gv->setEnableAutoFitToScreen(ui->m_gv->scaleFactor() <= 1);
} }
void MainWindow::on_actionFitByWidth_triggered() void MainWindow::on_actionFitByWidth_triggered()
{ {
m_gv->fitByOrientation(Qt::Orientation::Horizontal); ui->m_gv->fitByOrientation(Qt::Orientation::Horizontal);
} }
void MainWindow::on_actionFitByHeight_triggered() void MainWindow::on_actionFitByHeight_triggered()
{ {
m_gv->fitByOrientation(Qt::Orientation::Vertical); ui->m_gv->fitByOrientation(Qt::Orientation::Vertical);
} }
void MainWindow::on_actionZoomIn_triggered() void MainWindow::on_actionZoomIn_triggered()
{ {
if (m_gv->scaleFactor() < 1000) { if (ui->m_gv->scaleFactor() < 1000) {
m_gv->zoomView(1.25); ui->m_gv->zoomView(1.25);
} }
} }
void MainWindow::on_actionZoomOut_triggered() void MainWindow::on_actionZoomOut_triggered()
{ {
m_gv->zoomView(0.8); ui->m_gv->zoomView(0.8);
} }
void MainWindow::on_actionFlipHorizontal_triggered() void MainWindow::on_actionFlipHorizontal_triggered()
{ {
m_gv->flipView(true); ui->m_gv->flipView(true);
} }
void MainWindow::on_actionFlipVertical_triggered() void MainWindow::on_actionFlipVertical_triggered()
{ {
m_gv->flipView(false); ui->m_gv->flipView(false);
} }
void MainWindow::on_actionRotateClockwise_triggered() void MainWindow::on_actionRotateClockwise_triggered()
{ {
m_gv->rotateView(true); ui->m_gv->rotateView(true);
m_gv->displayScene(); ui->m_gv->displayScene();
} }
void MainWindow::on_actionRotateCounterClockwise_triggered() void MainWindow::on_actionRotateCounterClockwise_triggered()
{ {
m_gv->rotateView(false); ui->m_gv->rotateView(false);
m_gv->displayScene(); ui->m_gv->displayScene();
} }
void MainWindow::on_actionToggleAvoidResetTransform_triggered() void MainWindow::on_actionToggleAvoidResetTransform_triggered()
@@ -566,17 +463,17 @@ void MainWindow::on_actionToggleAvoidResetTransform_triggered()
void MainWindow::on_actionToggleCheckerboard_triggered() void MainWindow::on_actionToggleCheckerboard_triggered()
{ {
m_gv->toggleCheckerboard(QGuiApplication::queryKeyboardModifiers().testFlag(Qt::ShiftModifier)); ui->m_gv->toggleCheckerboard(QGuiApplication::queryKeyboardModifiers().testFlag(Qt::ShiftModifier));
} }
void MainWindow::on_actionTogglePauseAnimation_triggered() void MainWindow::on_actionTogglePauseAnimation_triggered()
{ {
m_gv->scene()->togglePauseAnimation(); ui->m_gv->scene()->togglePauseAnimation();
} }
void MainWindow::on_actionAnimationNextFrame_triggered() void MainWindow::on_actionAnimationNextFrame_triggered()
{ {
m_gv->scene()->skipAnimationFrame(1); ui->m_gv->scene()->skipAnimationFrame(1);
} }
void MainWindow::on_actionTrash_triggered() void MainWindow::on_actionTrash_triggered()
@@ -678,5 +575,5 @@ bool MainWindow::updateFileWatcher(const QString &basePath)
} }
void MainWindow::updateActionState() { void MainWindow::updateActionState() {
m_am->updateActionState(m_pm, m_gv); m_am->updateActionState(m_pm, ui->m_gv);
} }
+8 -25
View File
@@ -5,24 +5,19 @@
#ifndef MAINWINDOW_H #ifndef MAINWINDOW_H
#define MAINWINDOW_H #define MAINWINDOW_H
#include "framelesswindow.h" #include <QWidget>
#include <QParallelAnimationGroup>
#include <QPropertyAnimation>
#include <QPushButton>
QT_BEGIN_NAMESPACE QT_BEGIN_NAMESPACE
class QMenu;
class QGraphicsView;
class QFileSystemWatcher; class QFileSystemWatcher;
namespace Ui {
class MainWindow;
}
QT_END_NAMESPACE QT_END_NAMESPACE
class ActionManager; class ActionManager;
class PlaylistManager; class PlaylistManager;
class GraphicsView;
class NavigatorView; class MainWindow : public QWidget
class BottomButtonGroup;
class MainWindow : public FramelessWindow
{ {
Q_OBJECT Q_OBJECT
@@ -33,7 +28,6 @@ public:
void showFiles(const QStringList& files); void showFiles(const QStringList& files);
void showFiles(const QList<QUrl>& urls); void showFiles(const QList<QUrl>& urls);
void initWindowSize(); void initWindowSize();
void adjustWindowSizeBySceneRect();
void clearGallery(); void clearGallery();
void galleryPrev(); void galleryPrev();
@@ -43,22 +37,14 @@ public:
static QStringList supportedImageFormats(); static QStringList supportedImageFormats();
protected slots: 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 wheelEvent(QWheelEvent *event) override;
void resizeEvent(QResizeEvent *event) override;
void contextMenuEvent(QContextMenuEvent *event) override; void contextMenuEvent(QContextMenuEvent *event) override;
void dragEnterEvent(QDragEnterEvent *event) override; void dragEnterEvent(QDragEnterEvent *event) override;
void dragMoveEvent(QDragMoveEvent *event) override; void dragMoveEvent(QDragMoveEvent *event) override;
void dropEvent(QDropEvent *event) override; void dropEvent(QDropEvent *event) override;
void centerWindow(); void centerWindow();
void closeWindow();
void updateWidgetsPosition();
void toggleAvoidResetTransform(); void toggleAvoidResetTransform();
void quitAppAction(bool force = false);
void toggleFullscreen(); void toggleFullscreen();
void toggleMaximize(); void toggleMaximize();
@@ -101,14 +87,11 @@ private:
void updateActionState(); void updateActionState();
private: private:
QT_PREPEND_NAMESPACE(Ui::MainWindow) *ui;
ActionManager *m_am; ActionManager *m_am;
PlaylistManager *m_pm; PlaylistManager *m_pm;
GraphicsView *m_gv;
NavigatorView *m_nav;
QMenu *m_menu;
QFileSystemWatcher *m_fileSystemWatcher; QFileSystemWatcher *m_fileSystemWatcher;
BottomButtonGroup *m_bottomButtonGroup;
}; };
#endif // MAINWINDOW_H #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>
+2
View File
@@ -18,6 +18,8 @@ public:
public slots: public slots:
void updateMainViewportRegion(); void updateMainViewportRegion();
private slots:
void updateStyle(Qt::ColorScheme colorScheme); void updateStyle(Qt::ColorScheme colorScheme);
private: private:
+6 -25
View File
@@ -3,6 +3,7 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
#include "playlistmanager.h" #include "playlistmanager.h"
#include "playlistscout.h"
#include <QCollator> #include <QCollator>
#include <QDir> #include <QDir>
@@ -71,34 +72,14 @@ bool PlaylistManager::loadSingleFilePlaylist(const QUrl& url) {
if (!checkUrl(url)) if (!checkUrl(url))
return false; return false;
QFileInfo info(url.toLocalFile()); // Scout playlist by given file
QDir dir(info.path()); auto scoutResult = PlaylistScout::scout(url, m_allowedSuffixes);
QString currentFileName = info.fileName(); if (!scoutResult.has_value()) {
QStringList entryList = dir.entryList(
m_allowedSuffixes, QDir::Files | QDir::NoSymLinks, QDir::NoSort);
QCollator collator;
collator.setNumericMode(true);
std::sort(entryList.begin(), entryList.end(), collator);
QList<QUrl> playlist;
std::optional<qsizetype> idx = std::nullopt;
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);
if (!checkUrl(url)) {
return false; return false;
} }
playlist.append(url);
if (fileName == currentFileName) {
idx = i;
}
}
setPlaylist(std::move(playlist), idx); // Setup playlist with index
setPlaylist(std::move(scoutResult->playlist), scoutResult->indexHint);
return true; return true;
} }
+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) #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 Settings::MouseWheelBehavior Settings::mouseWheelBehavior() const
{ {
QString result = m_qsettings->value("mouse_wheel_behavior", "").toString(); QString result = m_qsettings->value("mouse_wheel_behavior", "").toString();
@@ -107,12 +101,6 @@ void Settings::setSvgTiny12Only(bool on)
m_qsettings->sync(); 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) void Settings::setMouseWheelBehavior(MouseWheelBehavior mwb)
{ {
m_qsettings->setValue("mouse_wheel_behavior", QEnumHelper::toString(mwb)); m_qsettings->setValue("mouse_wheel_behavior", QEnumHelper::toString(mwb));
-12
View File
@@ -11,23 +11,13 @@ class Settings : public QObject
{ {
Q_OBJECT Q_OBJECT
public: public:
enum DoubleClickBehavior {
Ignore,
Close,
Maximize,
FullScreen,
};
Q_ENUM(DoubleClickBehavior)
enum MouseWheelBehavior { enum MouseWheelBehavior {
Zoom, Zoom,
Switch, Switch,
}; };
Q_ENUM(MouseWheelBehavior) Q_ENUM(MouseWheelBehavior)
// TODO: I want to remove Auto behavior in future.
enum WindowSizeBehavior { enum WindowSizeBehavior {
Auto,
Maximized, Maximized,
Windowed, Windowed,
}; };
@@ -38,7 +28,6 @@ public:
bool useLightCheckerboard() const; bool useLightCheckerboard() const;
bool loopGallery() const; bool loopGallery() const;
bool svgTiny12Only() const; bool svgTiny12Only() const;
DoubleClickBehavior doubleClickBehavior() const;
MouseWheelBehavior mouseWheelBehavior() const; MouseWheelBehavior mouseWheelBehavior() const;
WindowSizeBehavior initWindowSizeBehavior() const; WindowSizeBehavior initWindowSizeBehavior() const;
Qt::HighDpiScaleFactorRoundingPolicy hiDpiScaleFactorBehavior() const; Qt::HighDpiScaleFactorRoundingPolicy hiDpiScaleFactorBehavior() const;
@@ -46,7 +35,6 @@ public:
void setUseLightCheckerboard(bool light); void setUseLightCheckerboard(bool light);
void setLoopGallery(bool on); void setLoopGallery(bool on);
void setSvgTiny12Only(bool on); void setSvgTiny12Only(bool on);
void setDoubleClickBehavior(DoubleClickBehavior dcb);
void setMouseWheelBehavior(MouseWheelBehavior mwb); void setMouseWheelBehavior(MouseWheelBehavior mwb);
void setInitWindowSizeBehavior(WindowSizeBehavior wsb); void setInitWindowSizeBehavior(WindowSizeBehavior wsb);
void setHiDpiScaleFactorBehavior(Qt::HighDpiScaleFactorRoundingPolicy hidpi); void setHiDpiScaleFactorBehavior(Qt::HighDpiScaleFactorRoundingPolicy hidpi);
-17
View File
@@ -49,15 +49,6 @@ private:
Pairs pairs; 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 { static EnumComboBoxTransformer<Settings::MouseWheelBehavior, QString> MW_OPTIONS {
{ {
{ Settings::MouseWheelBehavior::Zoom, QCoreApplication::translate("SettingsDialog", "Zoom in and out") }, { 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 { static EnumComboBoxTransformer<Settings::WindowSizeBehavior, QString> IWS_OPTIONS {
{ {
{ Settings::WindowSizeBehavior::Auto, QCoreApplication::translate("SettingsDialog", "Auto size") },
{ Settings::WindowSizeBehavior::Maximized, QCoreApplication::translate("SettingsDialog", "Maximized") }, { Settings::WindowSizeBehavior::Maximized, QCoreApplication::translate("SettingsDialog", "Maximized") },
{ Settings::WindowSizeBehavior::Windowed, QCoreApplication::translate("SettingsDialog", "Windowed") } { Settings::WindowSizeBehavior::Windowed, QCoreApplication::translate("SettingsDialog", "Windowed") }
} }
@@ -92,9 +82,6 @@ SettingsDialog::SettingsDialog(QWidget *parent)
ui->m_useLightCheckerboard->setChecked(settings->useLightCheckerboard()); ui->m_useLightCheckerboard->setChecked(settings->useLightCheckerboard());
ui->m_loopGallery->setChecked(settings->loopGallery()); ui->m_loopGallery->setChecked(settings->loopGallery());
ui->m_svgTiny12Only->setChecked(settings->svgTiny12Only()); 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)); ui->m_mouseWheelBehavior->setModel(MW_OPTIONS.buildComboBoxModel(this));
Settings::MouseWheelBehavior mwb = settings->mouseWheelBehavior(); Settings::MouseWheelBehavior mwb = settings->mouseWheelBehavior();
ui->m_mouseWheelBehavior->setCurrentIndex(MW_OPTIONS.to_index(mwb)); ui->m_mouseWheelBehavior->setCurrentIndex(MW_OPTIONS.to_index(mwb));
@@ -125,10 +112,6 @@ SettingsDialog::SettingsDialog(QWidget *parent)
Settings::instance()->setSvgTiny12Only(state == Qt::Checked); 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){ connect(ui->m_mouseWheelBehavior, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [ = ](int index){
Settings::instance()->setMouseWheelBehavior(MW_OPTIONS.to_value(index)); Settings::instance()->setMouseWheelBehavior(MW_OPTIONS.to_value(index));
}); });
+5 -15
View File
@@ -41,43 +41,33 @@
</widget> </widget>
</item> </item>
<item row="3" column="0"> <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"> <widget class="QLabel" name="m_mouseWheelBehaviorLabel">
<property name="text"> <property name="text">
<string>Mouse wheel behavior</string> <string>Mouse wheel behavior</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="1"> <item row="3" column="1">
<widget class="QComboBox" name="m_mouseWheelBehavior"/> <widget class="QComboBox" name="m_mouseWheelBehavior"/>
</item> </item>
<item row="5" column="0"> <item row="4" column="0">
<widget class="QLabel" name="m_initWindowSizeBehaviorLabel"> <widget class="QLabel" name="m_initWindowSizeBehaviorLabel">
<property name="text"> <property name="text">
<string>Default window size</string> <string>Default window size</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="5" column="1"> <item row="4" column="1">
<widget class="QComboBox" name="m_initWindowSizeBehavior"/> <widget class="QComboBox" name="m_initWindowSizeBehavior"/>
</item> </item>
<item row="6" column="0"> <item row="5" column="0">
<widget class="QLabel" name="m_hiDpiRoundingPolicyBehaviorLabel"> <widget class="QLabel" name="m_hiDpiRoundingPolicyBehaviorLabel">
<property name="text"> <property name="text">
<string>HiDPI scale factor rounding policy</string> <string>HiDPI scale factor rounding policy</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="6" column="1"> <item row="5" column="1">
<widget class="QComboBox" name="m_hiDpiRoundingPolicyBehavior"/> <widget class="QComboBox" name="m_hiDpiRoundingPolicyBehavior"/>
</item> </item>
</layout> </layout>