Compare commits
13
Commits
dbf8c09368
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e01718928c | ||
|
|
f5dbf44ec7 | ||
|
|
f55f8c731d | ||
|
|
4ed491c78a | ||
|
|
b54cde2cbf | ||
|
|
b1d12cc406 | ||
|
|
aa13355966 | ||
|
|
bd35a8c3e0 | ||
|
|
81413b51da | ||
|
|
a01d640c27 | ||
|
|
16800df138 | ||
|
|
7e8fd4ea21 | ||
|
|
04fa491cd8 |
+12
-2
@@ -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,12 +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
|
||||
@@ -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)
|
||||
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)
|
||||
|
||||
+14
-8
@@ -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: Some icons are invalid.
|
||||
|
||||
CREATE_NEW_THEMEICON_ACTION(mainWindow, actionCopyPixmap, edit-copy);
|
||||
CREATE_NEW_ACTION(mainWindow, actionCopyPixmap);
|
||||
CREATE_NEW_ACTION(mainWindow, actionCopyFilePath);
|
||||
|
||||
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_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, actionAnimationNextFrame);
|
||||
|
||||
@@ -179,6 +179,11 @@ void ActionManager::updateActionState(PlaylistManager* pm, GraphicsView* gv)
|
||||
const auto* gs = gv->scene();
|
||||
const auto* settings = Settings::instance();
|
||||
|
||||
// Update copy actions
|
||||
bool canCopy = pm->currentIndex().has_value();
|
||||
actionCopyPixmap->setEnabled(canCopy);
|
||||
actionCopyFilePath->setEnabled(canCopy);
|
||||
|
||||
// Update Prev/Next Picture State
|
||||
const int galleryFileCount = pm->totalCount();
|
||||
const bool loopGallery = settings->loopGallery();
|
||||
@@ -186,11 +191,9 @@ void ActionManager::updateActionState(PlaylistManager* pm, GraphicsView* gv)
|
||||
actionNextPicture->setEnabled(galleryFileCount > 1 && (loopGallery || !pm->isLastIndex()));
|
||||
|
||||
// Update Avoid Reset Transform
|
||||
actionToggleAvoidResetTransform->setCheckable(gv->avoidResetTransform());
|
||||
|
||||
actionToggleAvoidResetTransform->setChecked(gv->isAvoidResetTransform());
|
||||
// Update Checkerboard
|
||||
actionToggleCheckerboard->setChecked(gv->checkerboard());
|
||||
|
||||
actionToggleCheckerboard->setChecked(gv->isCheckerboard());
|
||||
// Update Animation Graphics Action
|
||||
bool canPauseAnimation = gs->canPauseAnimation();
|
||||
actionTogglePauseAnimation->setEnabled(canPauseAnimation);
|
||||
@@ -202,7 +205,10 @@ void ActionManager::updateActionState(PlaylistManager* pm, GraphicsView* gv)
|
||||
bool canSkipAnimation = gs->canSkipAnimationFrame();
|
||||
actionAnimationNextFrame->setEnabled(canSkipAnimation);
|
||||
|
||||
// TODO: Add more...
|
||||
|
||||
// Update trash, located in explorer and properties.
|
||||
bool hasLoadedFile = pm->currentIndex().has_value();
|
||||
actionTrash->setEnabled(hasLoadedFile);
|
||||
actionLocateInFileManager->setEnabled(hasLoadedFile);
|
||||
actionProperties->setEnabled(hasLoadedFile);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
#include <QVBoxLayout>
|
||||
#include <QDebug>
|
||||
|
||||
BottomButtonGroup::BottomButtonGroup(const std::vector<QAction *> &actionList, QWidget *parent)
|
||||
BottomButtonGroup::BottomButtonGroup(QWidget *parent)
|
||||
: QGroupBox (parent)
|
||||
{
|
||||
QHBoxLayout * mainLayout = new QHBoxLayout(this);
|
||||
@@ -25,7 +25,9 @@ QToolButton {
|
||||
border-style: none;
|
||||
}
|
||||
)#");
|
||||
}
|
||||
|
||||
void BottomButtonGroup::addActions(const std::vector<QAction *> & actionList) {
|
||||
for (QAction * action : actionList) {
|
||||
// Create button
|
||||
QToolButton * btn = new QToolButton(this);
|
||||
@@ -37,3 +39,7 @@ QToolButton {
|
||||
}
|
||||
updateGeometry();
|
||||
}
|
||||
|
||||
void BottomButtonGroup::addAction(QAction* action) {
|
||||
addActions({action});
|
||||
}
|
||||
|
||||
@@ -14,7 +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 addActions(const std::vector<QAction *> & actionList);
|
||||
void addAction(QAction* action);
|
||||
};
|
||||
|
||||
#endif // BOTTOMBUTTONGROUP_H
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -129,7 +129,7 @@ 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);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ public:
|
||||
|
||||
QPixmap renderToPixmap();
|
||||
|
||||
public slots:
|
||||
private slots:
|
||||
void updateStyle(Qt::ColorScheme colorScheme);
|
||||
|
||||
private:
|
||||
|
||||
+14
-3
@@ -224,7 +224,18 @@ void GraphicsView::fitByOrientation(Qt::Orientation ori, bool scaleDownOnly)
|
||||
QRectF originalScene = sceneRect();
|
||||
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) {
|
||||
// For horizontal fit (fit by width), position at top (for tall images)
|
||||
@@ -320,7 +331,7 @@ void GraphicsView::setEnableAutoFitToScreen(bool enable)
|
||||
m_enableFitToScreen = enable;
|
||||
}
|
||||
|
||||
bool GraphicsView::avoidResetTransform() const
|
||||
bool GraphicsView::isAvoidResetTransform() const
|
||||
{
|
||||
return m_avoidResetTransform;
|
||||
}
|
||||
@@ -330,7 +341,7 @@ void GraphicsView::setAvoidResetTransform(bool avoidReset)
|
||||
m_avoidResetTransform = avoidReset;
|
||||
}
|
||||
|
||||
bool GraphicsView::checkerboard() const {
|
||||
bool GraphicsView::isCheckerboard() const {
|
||||
return m_checkerboardEnabled;
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -40,10 +40,10 @@ public:
|
||||
bool isSceneBiggerThanView() const;
|
||||
void setEnableAutoFitToScreen(bool enable = true);
|
||||
|
||||
bool avoidResetTransform() const;
|
||||
bool isAvoidResetTransform() const;
|
||||
void setAvoidResetTransform(bool avoidReset);
|
||||
|
||||
bool checkerboard() const;
|
||||
bool isCheckerboard() const;
|
||||
void toggleCheckerboard(bool invertCheckerboardColor = false);
|
||||
|
||||
static QTransform resetScale(const QTransform & orig);
|
||||
@@ -52,7 +52,7 @@ signals:
|
||||
void navigatorViewRequired(bool required, QTransform transform);
|
||||
void viewportRectChanged();
|
||||
|
||||
public slots:
|
||||
private slots:
|
||||
void updateStyle(Qt::ColorScheme colorScheme);
|
||||
|
||||
private:
|
||||
|
||||
+5
-12
@@ -4,7 +4,6 @@
|
||||
|
||||
#include "mainwindow.h"
|
||||
|
||||
#include "playlistmanager.h"
|
||||
#include "settings.h"
|
||||
|
||||
#ifdef Q_OS_MACOS
|
||||
@@ -68,21 +67,19 @@ int main(int argc, char *argv[])
|
||||
a.installEventFilter(fileOpenEventHandler);
|
||||
a.connect(fileOpenEventHandler, &FileOpenEventHandler::fileOpen, [&w](const QUrl & url){
|
||||
if (w.isHidden()) {
|
||||
w.setWindowOpacity(1);
|
||||
w.showNormal();
|
||||
} else {
|
||||
w.activateWindow();
|
||||
}
|
||||
w.showUrls({url});
|
||||
w.showFiles({url});
|
||||
w.initWindowSize();
|
||||
});
|
||||
|
||||
// Handle dock icon clicks to show hidden window
|
||||
a.connect(&a, &QApplication::applicationStateChanged, [&w](Qt::ApplicationState state) {
|
||||
if (state == Qt::ApplicationActive && w.isHidden()) {
|
||||
w.showUrls({});
|
||||
w.galleryCurrent(true, true);
|
||||
w.setWindowOpacity(1);
|
||||
w.showFiles({});
|
||||
w.galleryCurrent(false);
|
||||
w.showNormal();
|
||||
w.raise();
|
||||
w.activateWindow();
|
||||
@@ -90,12 +87,8 @@ int main(int argc, char *argv[])
|
||||
});
|
||||
#endif // Q_OS_MACOS
|
||||
|
||||
QStringList urlStrList = parser.positionalArguments();
|
||||
QList<QUrl> && urlList = PlaylistManager::convertToUrlList(urlStrList);
|
||||
|
||||
if (!urlList.isEmpty()) {
|
||||
w.showUrls(urlList);
|
||||
}
|
||||
QStringList fileList = parser.positionalArguments();
|
||||
w.showFiles(fileList);
|
||||
|
||||
w.initWindowSize();
|
||||
|
||||
|
||||
+160
-333
@@ -3,6 +3,7 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "mainwindow.h"
|
||||
#include "ui_mainwindow.h"
|
||||
|
||||
#include "settings.h"
|
||||
#include "bottombuttongroup.h"
|
||||
@@ -46,51 +47,50 @@
|
||||
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))
|
||||
{
|
||||
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->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->actionFitToScreen,
|
||||
m_am->actionZoomIn,
|
||||
@@ -101,22 +101,70 @@ MainWindow::MainWindow(QWidget *parent)
|
||||
m_am->actionRotateCounterClockwise,
|
||||
m_am->actionRotateClockwise,
|
||||
m_am->actionFlipHorizontal,
|
||||
m_am->actionToggleCheckerboard,
|
||||
}, this);
|
||||
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](){
|
||||
@@ -127,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()
|
||||
@@ -217,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,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()
|
||||
@@ -269,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;
|
||||
@@ -348,102 +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)
|
||||
{
|
||||
// TODO: Update this function.
|
||||
QMenu * menu = new QMenu;
|
||||
ui->m_menu->exec(mapToGlobal(event->pos()));
|
||||
|
||||
// TODO: currentImageFileUrl() may be redundant.
|
||||
QMenu * copyMenu = new QMenu(tr("&Copy"));
|
||||
QUrl currentFileUrl = currentImageFileUrl();
|
||||
|
||||
copyMenu->setIcon(QIcon::fromTheme(u"edit-copy"_s));
|
||||
copyMenu->addAction(m_am->actionCopyPixmap);
|
||||
if (currentFileUrl.isValid()) {
|
||||
copyMenu->addAction(m_am->actionCopyFilePath);
|
||||
}
|
||||
if (copyMenu->actions().count() == 1) {
|
||||
menu->addActions(copyMenu->actions());
|
||||
} else {
|
||||
menu->addMenu(copyMenu);
|
||||
}
|
||||
|
||||
menu->addSeparator();
|
||||
|
||||
menu->addAction(m_am->actionPrevPicture);
|
||||
menu->addAction(m_am->actionNextPicture);
|
||||
|
||||
menu->addSeparator();
|
||||
|
||||
menu->addAction(m_am->actionActualSize);
|
||||
menu->addAction(m_am->actionFitToScreen);
|
||||
menu->addAction(m_am->actionFitByWidth);
|
||||
menu->addAction(m_am->actionFitByHeight);
|
||||
menu->addAction(m_am->actionZoomIn);
|
||||
menu->addAction(m_am->actionZoomOut);
|
||||
|
||||
menu->addSeparator();
|
||||
|
||||
menu->addAction(m_am->actionFlipHorizontal);
|
||||
menu->addAction(m_am->actionFlipVertical);
|
||||
menu->addAction(m_am->actionRotateClockwise);
|
||||
menu->addAction(m_am->actionRotateCounterClockwise);
|
||||
|
||||
menu->addSeparator();
|
||||
|
||||
// TODO: Move checked update to action manager.
|
||||
QAction * avoidResetTransform = m_am->actionToggleAvoidResetTransform;
|
||||
avoidResetTransform->setCheckable(true);
|
||||
avoidResetTransform->setChecked(m_gv->avoidResetTransform());
|
||||
menu->addAction(avoidResetTransform);
|
||||
menu->addAction(m_am->actionToggleCheckerboard);
|
||||
QAction * pauseAnimation = m_am->actionTogglePauseAnimation;
|
||||
pauseAnimation->setCheckable(true);
|
||||
pauseAnimation->setChecked(m_gv->scene()->pauseAnimation());
|
||||
menu->addAction(pauseAnimation);
|
||||
menu->addAction(m_am->actionAnimationNextFrame);
|
||||
|
||||
if (currentFileUrl.isValid()) {
|
||||
menu->addSeparator();
|
||||
if (currentFileUrl.isLocalFile()) {
|
||||
menu->addAction(m_am->actionTrash);
|
||||
menu->addAction(m_am->actionLocateInFileManager);
|
||||
}
|
||||
menu->addAction(m_am->actionProperties);
|
||||
}
|
||||
|
||||
menu->addSeparator();
|
||||
|
||||
menu->addAction(m_am->actionSettings);
|
||||
menu->addAction(m_am->actionHelp);
|
||||
|
||||
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)
|
||||
@@ -456,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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -491,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()
|
||||
@@ -558,16 +378,16 @@ QSize MainWindow::sizeHint() const
|
||||
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;
|
||||
|
||||
QClipboard *cb = QApplication::clipboard();
|
||||
cb->setText(m_pm->localFileByIndex(*index));
|
||||
}
|
||||
|
||||
void MainWindow::on_actionPrevPicture_triggered()
|
||||
@@ -582,58 +402,58 @@ void MainWindow::on_actionNextPicture_triggered()
|
||||
|
||||
void MainWindow::on_actionActualSize_triggered()
|
||||
{
|
||||
m_gv->resetScale();
|
||||
m_gv->setEnableAutoFitToScreen(false);
|
||||
ui->m_gv->resetScale();
|
||||
ui->m_gv->setEnableAutoFitToScreen(false);
|
||||
}
|
||||
|
||||
void MainWindow::on_actionFitToScreen_triggered()
|
||||
{
|
||||
m_gv->fitInView(m_nav->sceneRect(), Qt::KeepAspectRatio);
|
||||
m_gv->setEnableAutoFitToScreen(m_gv->scaleFactor() <= 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()
|
||||
{
|
||||
m_gv->fitByOrientation(Qt::Orientation::Horizontal);
|
||||
ui->m_gv->fitByOrientation(Qt::Orientation::Horizontal);
|
||||
}
|
||||
|
||||
void MainWindow::on_actionFitByHeight_triggered()
|
||||
{
|
||||
m_gv->fitByOrientation(Qt::Orientation::Vertical);
|
||||
ui->m_gv->fitByOrientation(Qt::Orientation::Vertical);
|
||||
}
|
||||
|
||||
void MainWindow::on_actionZoomIn_triggered()
|
||||
{
|
||||
if (m_gv->scaleFactor() < 1000) {
|
||||
m_gv->zoomView(1.25);
|
||||
if (ui->m_gv->scaleFactor() < 1000) {
|
||||
ui->m_gv->zoomView(1.25);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::on_actionZoomOut_triggered()
|
||||
{
|
||||
m_gv->zoomView(0.8);
|
||||
ui->m_gv->zoomView(0.8);
|
||||
}
|
||||
|
||||
void MainWindow::on_actionFlipHorizontal_triggered()
|
||||
{
|
||||
m_gv->flipView(true);
|
||||
ui->m_gv->flipView(true);
|
||||
}
|
||||
|
||||
void MainWindow::on_actionFlipVertical_triggered()
|
||||
{
|
||||
m_gv->flipView(false);
|
||||
ui->m_gv->flipView(false);
|
||||
}
|
||||
|
||||
void MainWindow::on_actionRotateClockwise_triggered()
|
||||
{
|
||||
m_gv->rotateView(true);
|
||||
m_gv->displayScene();
|
||||
ui->m_gv->rotateView(true);
|
||||
ui->m_gv->displayScene();
|
||||
}
|
||||
|
||||
void MainWindow::on_actionRotateCounterClockwise_triggered()
|
||||
{
|
||||
m_gv->rotateView(false);
|
||||
m_gv->displayScene();
|
||||
ui->m_gv->rotateView(false);
|
||||
ui->m_gv->displayScene();
|
||||
}
|
||||
|
||||
void MainWindow::on_actionToggleAvoidResetTransform_triggered()
|
||||
@@ -643,17 +463,17 @@ void MainWindow::on_actionToggleAvoidResetTransform_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()
|
||||
{
|
||||
m_gv->scene()->togglePauseAnimation();
|
||||
ui->m_gv->scene()->togglePauseAnimation();
|
||||
}
|
||||
|
||||
void MainWindow::on_actionAnimationNextFrame_triggered()
|
||||
{
|
||||
m_gv->scene()->skipAnimationFrame(1);
|
||||
ui->m_gv->scene()->skipAnimationFrame(1);
|
||||
}
|
||||
|
||||
void MainWindow::on_actionTrash_triggered()
|
||||
@@ -673,15 +493,16 @@ void MainWindow::on_actionTrash_triggered()
|
||||
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);
|
||||
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());
|
||||
@@ -716,7 +537,9 @@ void MainWindow::on_actionLocateInFileManager_triggered()
|
||||
|
||||
void MainWindow::on_actionProperties_triggered()
|
||||
{
|
||||
QUrl currentFileUrl = currentImageFileUrl();
|
||||
auto index = m_pm->currentIndex();
|
||||
if (!index.has_value()) return;
|
||||
QUrl currentFileUrl = m_pm->urlByIndex(*index);
|
||||
if (!currentFileUrl.isValid()) return;
|
||||
|
||||
MetadataModel * md = new MetadataModel();
|
||||
@@ -750,3 +573,7 @@ bool MainWindow::updateFileWatcher(const QString &basePath)
|
||||
if (!basePath.isEmpty()) return m_fileSystemWatcher->addPath(basePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
void MainWindow::updateActionState() {
|
||||
m_am->updateActionState(m_pm, ui->m_gv);
|
||||
}
|
||||
|
||||
+12
-27
@@ -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();
|
||||
|
||||
@@ -98,15 +84,14 @@ private slots:
|
||||
|
||||
private:
|
||||
bool updateFileWatcher(const QString & basePath = QString());
|
||||
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
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>MainWindow</class>
|
||||
<widget class="QWidget" name="MainWindow">
|
||||
<property name="windowModality">
|
||||
<enum>Qt::NonModal</enum>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>640</width>
|
||||
<height>480</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="acceptDrops">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="m_centralLayout">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="GraphicsView" name="m_gv">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::NoFrame</enum>
|
||||
</property>
|
||||
<property name="verticalScrollBarPolicy">
|
||||
<enum>Qt::ScrollBarAlwaysOff</enum>
|
||||
</property>
|
||||
<property name="horizontalScrollBarPolicy">
|
||||
<enum>Qt::ScrollBarAlwaysOff</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="bottomBar" native="true">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>72</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>72</height>
|
||||
</size>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="bottomBarLayout">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QWidget" name="m_navPH" native="true">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>99</width>
|
||||
<height>72</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>99</width>
|
||||
<height>72</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="leftSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="BottomButtonGroup" name="m_bottomButtonGroup"/>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="rightSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="NavigatorView" name="m_nav">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>99</width>
|
||||
<height>72</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>99</width>
|
||||
<height>72</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::NoFrame</enum>
|
||||
</property>
|
||||
<property name="verticalScrollBarPolicy">
|
||||
<enum>Qt::ScrollBarAlwaysOff</enum>
|
||||
</property>
|
||||
<property name="horizontalScrollBarPolicy">
|
||||
<enum>Qt::ScrollBarAlwaysOff</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
<widget class="QMenu" name="m_subMenuCopy">
|
||||
<property name="title">
|
||||
<string>&Copy</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset theme="edit-copy"/>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QMenu" name="m_subMenuTransform">
|
||||
<property name="title">
|
||||
<string>&Transform</string>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QMenu" name="m_menu"/>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>GraphicsView</class>
|
||||
<extends>QGraphicsView</extends>
|
||||
<header>graphicsview.h</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>NavigatorView</class>
|
||||
<extends>QGraphicsView</extends>
|
||||
<header>navigatorview.h</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>BottomButtonGroup</class>
|
||||
<extends>QGroupBox</extends>
|
||||
<header>bottombuttongroup.h</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -18,6 +18,8 @@ public:
|
||||
|
||||
public slots:
|
||||
void updateMainViewportRegion();
|
||||
|
||||
private slots:
|
||||
void updateStyle(Qt::ColorScheme colorScheme);
|
||||
|
||||
private:
|
||||
|
||||
+213
-201
@@ -3,158 +3,20 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "playlistmanager.h"
|
||||
#include "playlistscout.h"
|
||||
|
||||
#include <QCollator>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QUrl>
|
||||
|
||||
PlaylistModel::PlaylistModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
PlaylistModel::~PlaylistModel()
|
||||
= default;
|
||||
|
||||
void PlaylistModel::setPlaylist(const QList<QUrl> &urls)
|
||||
{
|
||||
beginResetModel();
|
||||
m_playlist = urls;
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
QModelIndex PlaylistModel::loadPlaylist(const QList<QUrl> & urls)
|
||||
{
|
||||
if (urls.isEmpty()) return {};
|
||||
if (urls.count() == 1) {
|
||||
return loadPlaylist(urls.constFirst());
|
||||
} else {
|
||||
setPlaylist(urls);
|
||||
return index(0);
|
||||
}
|
||||
}
|
||||
|
||||
QModelIndex PlaylistModel::loadPlaylist(const QUrl &url)
|
||||
{
|
||||
QFileInfo info(url.toLocalFile());
|
||||
QDir dir(info.path());
|
||||
QString && currentFileName = info.fileName();
|
||||
|
||||
if (dir.path() == m_currentDir) {
|
||||
int idx = indexOf(url);
|
||||
return idx == -1 ? appendToPlaylist(url) : index(idx);
|
||||
}
|
||||
|
||||
QStringList entryList = dir.entryList(
|
||||
m_autoLoadSuffixes,
|
||||
QDir::Files | QDir::NoSymLinks, QDir::NoSort);
|
||||
|
||||
QCollator collator;
|
||||
collator.setNumericMode(true);
|
||||
|
||||
std::sort(entryList.begin(), entryList.end(), collator);
|
||||
|
||||
QList<QUrl> playlist;
|
||||
|
||||
int idx = -1;
|
||||
for (int i = 0; i < entryList.count(); i++) {
|
||||
const QString & fileName = entryList.at(i);
|
||||
const QString & oneEntry = dir.absoluteFilePath(fileName);
|
||||
const QUrl & url = QUrl::fromLocalFile(oneEntry);
|
||||
playlist.append(url);
|
||||
if (fileName == currentFileName) {
|
||||
idx = i;
|
||||
}
|
||||
}
|
||||
if (idx == -1) {
|
||||
idx = playlist.count();
|
||||
playlist.append(url);
|
||||
}
|
||||
m_currentDir = dir.path();
|
||||
|
||||
setPlaylist(playlist);
|
||||
|
||||
return index(idx);
|
||||
}
|
||||
|
||||
QModelIndex PlaylistModel::appendToPlaylist(const QUrl &url)
|
||||
{
|
||||
const int lastIndex = rowCount();
|
||||
beginInsertRows(QModelIndex(), lastIndex, lastIndex);
|
||||
m_playlist.append(url);
|
||||
endInsertRows();
|
||||
return index(lastIndex);
|
||||
}
|
||||
|
||||
bool PlaylistModel::removeAt(int index)
|
||||
{
|
||||
if (index < 0 || index >= rowCount()) return false;
|
||||
beginRemoveRows(QModelIndex(), index, index);
|
||||
m_playlist.removeAt(index);
|
||||
endRemoveRows();
|
||||
return true;
|
||||
}
|
||||
|
||||
int PlaylistModel::indexOf(const QUrl &url) const
|
||||
{
|
||||
return m_playlist.indexOf(url);
|
||||
}
|
||||
|
||||
QUrl PlaylistModel::urlByIndex(int index) const
|
||||
{
|
||||
return m_playlist.value(index);
|
||||
}
|
||||
|
||||
QStringList PlaylistModel::autoLoadFilterSuffixes() const
|
||||
{
|
||||
return m_autoLoadSuffixes;
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> PlaylistModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> result = QAbstractListModel::roleNames();
|
||||
result.insert(UrlRole, "url");
|
||||
return result;
|
||||
}
|
||||
|
||||
int PlaylistModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
return m_playlist.count();
|
||||
}
|
||||
|
||||
QVariant PlaylistModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid()) return {};
|
||||
|
||||
switch (role) {
|
||||
case Qt::DisplayRole:
|
||||
return m_playlist.at(index.row()).fileName();
|
||||
case UrlRole:
|
||||
return m_playlist.at(index.row());
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
PlaylistManager::PlaylistManager(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_currentIndex(std::nullopt)
|
||||
, m_playlist()
|
||||
, m_allowedSuffixes()
|
||||
{
|
||||
connect(&m_model, &PlaylistModel::rowsRemoved, this,
|
||||
[this](const QModelIndex &, int, int) {
|
||||
if (m_model.rowCount() <= m_currentIndex) {
|
||||
setProperty("currentIndex", m_currentIndex - 1);
|
||||
}
|
||||
});
|
||||
|
||||
auto onRowCountChanged = [this](){
|
||||
emit totalCountChanged(m_model.rowCount());
|
||||
};
|
||||
|
||||
connect(&m_model, &PlaylistModel::rowsInserted, this, onRowCountChanged);
|
||||
connect(&m_model, &PlaylistModel::rowsRemoved, this, onRowCountChanged);
|
||||
connect(&m_model, &PlaylistModel::modelReset, this, onRowCountChanged);
|
||||
}
|
||||
|
||||
PlaylistManager::~PlaylistManager()
|
||||
@@ -162,35 +24,86 @@ PlaylistManager::~PlaylistManager()
|
||||
|
||||
}
|
||||
|
||||
PlaylistModel *PlaylistManager::model()
|
||||
{
|
||||
return &m_model;
|
||||
// region: Allowed Suffixes Operations
|
||||
|
||||
const QStringList& PlaylistManager::allowedSuffixes() const {
|
||||
return m_allowedSuffixes;
|
||||
}
|
||||
|
||||
void PlaylistManager::setPlaylist(const QList<QUrl> &urls)
|
||||
{
|
||||
m_model.setPlaylist(urls);
|
||||
void PlaylistManager::setAllowedSuffixes(const QStringList& nameFilters) {
|
||||
m_allowedSuffixes = nameFilters;
|
||||
emit allowedSuffixesChanged(m_allowedSuffixes);
|
||||
}
|
||||
|
||||
QModelIndex PlaylistManager::loadPlaylist(const QList<QUrl> &urls)
|
||||
{
|
||||
QModelIndex idx = m_model.loadPlaylist(urls);
|
||||
setProperty("currentIndex", idx.row());
|
||||
return idx;
|
||||
// endregion
|
||||
|
||||
// region: Playlist Loader and Reset
|
||||
|
||||
bool PlaylistManager::loadPlaylist(const QStringList& files) {
|
||||
// Convert string list to url list first
|
||||
QList<QUrl> urls;
|
||||
for (const QString & file : std::as_const(files)) {
|
||||
urls.append(urlFromString(file));
|
||||
}
|
||||
|
||||
return loadPlaylist(urls);
|
||||
}
|
||||
|
||||
QModelIndex PlaylistManager::loadPlaylist(const QUrl &url)
|
||||
{
|
||||
QModelIndex idx = m_model.loadPlaylist(url);
|
||||
setProperty("currentIndex", idx.row());
|
||||
return idx;
|
||||
bool PlaylistManager::loadPlaylist(const QList<QUrl>& urls) {
|
||||
// Check its count, and check M3U8 playlist for single file case.
|
||||
if (urls.isEmpty()) {
|
||||
clearPlaylist();
|
||||
return true;
|
||||
} else if (urls.count() == 1) {
|
||||
const QUrl& firstUrl = urls.first();
|
||||
const QString lowerCaseUrlPath(firstUrl.path().toLower());
|
||||
if (lowerCaseUrlPath.endsWith(".m3u8") || lowerCaseUrlPath.endsWith(".m3u")) {
|
||||
return loadM3U8Playlist(firstUrl);
|
||||
} else {
|
||||
return loadSingleFilePlaylist(firstUrl);
|
||||
}
|
||||
} else {
|
||||
return loadMultipleFilesPlaylist(std::move(urls));
|
||||
}
|
||||
}
|
||||
|
||||
QModelIndex PlaylistManager::loadM3U8Playlist(const QUrl &url)
|
||||
{
|
||||
bool PlaylistManager::loadSingleFilePlaylist(const QUrl& url) {
|
||||
// Check URL at first
|
||||
if (!checkUrl(url))
|
||||
return false;
|
||||
|
||||
// Scout playlist by given file
|
||||
auto scoutResult = PlaylistScout::scout(url, m_allowedSuffixes);
|
||||
if (!scoutResult.has_value()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Setup playlist with index
|
||||
setPlaylist(std::move(scoutResult->playlist), scoutResult->indexHint);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlaylistManager::loadMultipleFilesPlaylist(const QList<QUrl>&& urls) {
|
||||
// Check all of them are valid
|
||||
for (const auto& url : std::as_const(urls)) {
|
||||
if (!checkUrl(url)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Set playlist and return
|
||||
setPlaylist(std::move(urls));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlaylistManager::loadM3U8Playlist(const QUrl& url) {
|
||||
// Check URL at first
|
||||
if (!checkUrl(url))
|
||||
return false;
|
||||
|
||||
QList<QUrl> urls;
|
||||
QFile file(url.toLocalFile());
|
||||
if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
QList<QUrl> urls;
|
||||
while (!file.atEnd()) {
|
||||
QString line = file.readLine();
|
||||
if (line.startsWith('#')) {
|
||||
@@ -198,86 +111,185 @@ QModelIndex PlaylistManager::loadM3U8Playlist(const QUrl &url)
|
||||
}
|
||||
QFileInfo fileInfo(file);
|
||||
QUrl item = QUrl::fromUserInput(line, fileInfo.absolutePath());
|
||||
if (!checkUrl(item)) {
|
||||
return false;
|
||||
}
|
||||
urls.append(item);
|
||||
}
|
||||
return loadPlaylist(urls);
|
||||
} else {
|
||||
return {};
|
||||
return false;
|
||||
}
|
||||
|
||||
setPlaylist(std::move(urls));
|
||||
return true;
|
||||
}
|
||||
|
||||
void PlaylistManager::setPlaylist(const QList<QUrl>&& urls, std::optional<qsizetype> indexHint) {
|
||||
// Setup playlist
|
||||
m_playlist = std::move(urls);
|
||||
|
||||
// Set index with hint
|
||||
if (indexHint.has_value()) {
|
||||
m_currentIndex = *indexHint;
|
||||
}
|
||||
sanitizeIndex();
|
||||
|
||||
// Raise signal
|
||||
emit playlistChanged(totalCount());
|
||||
}
|
||||
|
||||
void PlaylistManager::clearPlaylist() {
|
||||
m_playlist.clear();
|
||||
sanitizeIndex();
|
||||
emit playlistChanged(this->totalCount());
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Playlist Operations
|
||||
|
||||
void PlaylistManager::appendToPlaylist(const QString &file)
|
||||
{
|
||||
return appendToPlaylist(urlFromString(file));
|
||||
}
|
||||
|
||||
void PlaylistManager::appendToPlaylist(const QUrl &url)
|
||||
{
|
||||
m_playlist.append(url);
|
||||
sanitizeIndex();
|
||||
emit playlistChanged(totalCount());
|
||||
}
|
||||
|
||||
bool PlaylistManager::insertIntoPlaylist(qsizetype index, const QString &file)
|
||||
{
|
||||
return insertIntoPlaylist(index, urlFromString(file));
|
||||
}
|
||||
|
||||
bool PlaylistManager::insertIntoPlaylist(qsizetype index, const QUrl &url)
|
||||
{
|
||||
if (index < 0 || index > totalCount()) {
|
||||
return false;
|
||||
} else {
|
||||
m_playlist.insert(index, url);
|
||||
sanitizeIndex();
|
||||
emit playlistChanged(totalCount());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
int PlaylistManager::totalCount() const
|
||||
bool PlaylistManager::removeFromPlaylist(qsizetype index)
|
||||
{
|
||||
return m_model.rowCount();
|
||||
if (index < 0 || index >= totalCount()) {
|
||||
return false;
|
||||
} else {
|
||||
m_playlist.removeAt(index);
|
||||
sanitizeIndex();
|
||||
emit playlistChanged(totalCount());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
QModelIndex PlaylistManager::previousIndex() const
|
||||
{
|
||||
int count = totalCount();
|
||||
if (count == 0) return {};
|
||||
// endregion
|
||||
|
||||
return m_model.index(isFirstIndex() ? count - 1 : m_currentIndex - 1);
|
||||
// region: Index Operations
|
||||
|
||||
qsizetype PlaylistManager::totalCount() const
|
||||
{
|
||||
return m_playlist.count();
|
||||
}
|
||||
|
||||
QModelIndex PlaylistManager::nextIndex() const
|
||||
std::optional<qsizetype> PlaylistManager::previousIndex() const
|
||||
{
|
||||
int count = totalCount();
|
||||
if (count == 0) return {};
|
||||
|
||||
return m_model.index(isLastIndex() ? 0 : m_currentIndex + 1);
|
||||
if (!m_currentIndex.has_value() || totalCount() <= 0) return std::nullopt;
|
||||
return (isFirstIndex() ? totalCount() - 1 : *m_currentIndex - 1);
|
||||
}
|
||||
|
||||
QModelIndex PlaylistManager::curIndex() const
|
||||
std::optional<qsizetype> PlaylistManager::nextIndex() const
|
||||
{
|
||||
return m_model.index(m_currentIndex);
|
||||
if (!m_currentIndex.has_value() || totalCount() <= 0) return std::nullopt;
|
||||
return (isLastIndex() ? 0 : *m_currentIndex + 1);
|
||||
}
|
||||
|
||||
bool PlaylistManager::isFirstIndex() const
|
||||
{
|
||||
return m_currentIndex == 0;
|
||||
return m_currentIndex.has_value() && *m_currentIndex == 0;
|
||||
}
|
||||
|
||||
bool PlaylistManager::isLastIndex() const
|
||||
{
|
||||
return m_currentIndex + 1 == totalCount();
|
||||
return m_currentIndex.has_value() && *m_currentIndex + 1 == totalCount();
|
||||
}
|
||||
|
||||
void PlaylistManager::setCurrentIndex(const QModelIndex &index)
|
||||
std::optional<qsizetype> PlaylistManager::currentIndex() const
|
||||
{
|
||||
if (index.isValid() && index.row() >= 0 && index.row() < totalCount()) {
|
||||
setProperty("currentIndex", index.row());
|
||||
return m_currentIndex;
|
||||
}
|
||||
|
||||
bool PlaylistManager::setCurrentIndex(qsizetype index)
|
||||
{
|
||||
if (!m_currentIndex.has_value()) {
|
||||
return false;
|
||||
}
|
||||
if (index < 0 || index >= totalCount()) {
|
||||
return false;
|
||||
}
|
||||
if (*m_currentIndex == index) {
|
||||
return true;
|
||||
}
|
||||
|
||||
m_currentIndex = index;
|
||||
emit currentIndexChanged(index);
|
||||
return true;
|
||||
}
|
||||
|
||||
QUrl PlaylistManager::urlByIndex(const QModelIndex &index)
|
||||
QUrl PlaylistManager::urlByIndex(qsizetype index) const
|
||||
{
|
||||
return m_model.urlByIndex(index.row());
|
||||
return m_playlist.value(index);
|
||||
}
|
||||
|
||||
QString PlaylistManager::localFileByIndex(const QModelIndex &index)
|
||||
QString PlaylistManager::localFileByIndex(qsizetype index) const
|
||||
{
|
||||
return urlByIndex(index).toLocalFile();
|
||||
}
|
||||
|
||||
bool PlaylistManager::removeAt(const QModelIndex &index)
|
||||
{
|
||||
return m_model.removeAt(index.row());
|
||||
}
|
||||
// endregion
|
||||
|
||||
void PlaylistManager::setAutoLoadFilterSuffixes(const QStringList &nameFilters)
|
||||
{
|
||||
m_model.setProperty("autoLoadFilterSuffixes", nameFilters);
|
||||
}
|
||||
// region: Misc
|
||||
|
||||
QList<QUrl> PlaylistManager::convertToUrlList(const QStringList &files)
|
||||
void PlaylistManager::sanitizeIndex()
|
||||
{
|
||||
QList<QUrl> urlList;
|
||||
for (const QString & str : std::as_const(files)) {
|
||||
QUrl url = QUrl::fromLocalFile(str);
|
||||
if (url.isValid()) {
|
||||
urlList.append(url);
|
||||
auto cnt = totalCount();
|
||||
if (cnt == 0) {
|
||||
// If playlist is empty, current index should be invalid.
|
||||
m_currentIndex = std::nullopt;
|
||||
} else {
|
||||
if (m_currentIndex.has_value()) {
|
||||
// Clamp current index in playlist range.
|
||||
// If it is already in range, not change was done.
|
||||
auto currentIndex = *m_currentIndex;
|
||||
if (currentIndex < 0) {
|
||||
m_currentIndex = 0;
|
||||
} else if (currentIndex >= cnt) {
|
||||
m_currentIndex = cnt - 1;
|
||||
} else {
|
||||
; // No change.
|
||||
}
|
||||
} else {
|
||||
// If there is no set value for current index,
|
||||
// set it at the beginning of playlist.
|
||||
m_currentIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return urlList;
|
||||
}
|
||||
|
||||
bool PlaylistManager::checkUrl(const QUrl& url)
|
||||
{
|
||||
return url.isValid() && url.isLocalFile();
|
||||
}
|
||||
|
||||
QUrl PlaylistManager::urlFromString(const QString& file)
|
||||
{
|
||||
return QUrl::fromLocalFile(file);
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
+107
-65
@@ -5,84 +5,126 @@
|
||||
#pragma once
|
||||
|
||||
#include <QUrl>
|
||||
#include <QAbstractListModel>
|
||||
|
||||
class PlaylistModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum PlaylistRole {
|
||||
UrlRole = Qt::UserRole
|
||||
};
|
||||
Q_ENUM(PlaylistRole)
|
||||
Q_PROPERTY(QStringList autoLoadFilterSuffixes MEMBER m_autoLoadSuffixes NOTIFY autoLoadFilterSuffixesChanged)
|
||||
|
||||
explicit PlaylistModel(QObject *parent = nullptr);
|
||||
~PlaylistModel() override;
|
||||
|
||||
void setPlaylist(const QList<QUrl> & urls);
|
||||
QModelIndex loadPlaylist(const QList<QUrl> & urls);
|
||||
QModelIndex loadPlaylist(const QUrl & url);
|
||||
QModelIndex appendToPlaylist(const QUrl & url);
|
||||
bool removeAt(int index);
|
||||
int indexOf(const QUrl & url) const;
|
||||
QUrl urlByIndex(int index) const;
|
||||
QStringList autoLoadFilterSuffixes() const;
|
||||
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
signals:
|
||||
void autoLoadFilterSuffixesChanged(QStringList suffixes);
|
||||
|
||||
private:
|
||||
// model data
|
||||
QList<QUrl> m_playlist;
|
||||
// properties
|
||||
QStringList m_autoLoadSuffixes = {};
|
||||
// internal
|
||||
QString m_currentDir;
|
||||
};
|
||||
#include <QObject>
|
||||
#include <optional>
|
||||
|
||||
/// The manager of playlist.
|
||||
class PlaylistManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
Q_PROPERTY(int currentIndex MEMBER m_currentIndex NOTIFY currentIndexChanged)
|
||||
Q_PROPERTY(QStringList autoLoadFilterSuffixes WRITE setAutoLoadFilterSuffixes)
|
||||
Q_PROPERTY(PlaylistModel * model READ model CONSTANT)
|
||||
|
||||
explicit PlaylistManager(QObject *parent = nullptr);
|
||||
/// Constructs a PlaylistManager with an optional parent QObject.
|
||||
explicit PlaylistManager(QObject* parent = nullptr);
|
||||
/// Destructor.
|
||||
~PlaylistManager();
|
||||
|
||||
PlaylistModel * model();
|
||||
/// Get the allowed file suffixes of this playlist.
|
||||
/// @return The current list of allowed suffixes.
|
||||
const QStringList& allowedSuffixes() const;
|
||||
/// Set the allowed file suffixes of this playlist.
|
||||
/// @remark This only works for "load the whole directory by one file" case.
|
||||
/// @param[in] nameFilters The new list of allowed suffixes.
|
||||
void setAllowedSuffixes(const QStringList& nameFilters);
|
||||
|
||||
void setPlaylist(const QList<QUrl> & url);
|
||||
Q_INVOKABLE QModelIndex loadPlaylist(const QList<QUrl> & urls);
|
||||
Q_INVOKABLE QModelIndex loadPlaylist(const QUrl & url);
|
||||
Q_INVOKABLE QModelIndex loadM3U8Playlist(const QUrl & url);
|
||||
/// Load user specified files as playlist.
|
||||
/// @remark The wrapper for another overload.
|
||||
/// @return True for successful load, otherwise false.
|
||||
bool loadPlaylist(const QStringList& files);
|
||||
/// Load user specified URLs as playlist.
|
||||
/// @remark This function automatically dispatch these files into correct sub-load functions.
|
||||
/// @return True for successful load, otherwise false.
|
||||
bool loadPlaylist(const QList<QUrl>& urls);
|
||||
/// Clear the entire playlist and reset current index.
|
||||
void clearPlaylist();
|
||||
|
||||
int totalCount() const;
|
||||
QModelIndex previousIndex() const;
|
||||
QModelIndex nextIndex() const;
|
||||
QModelIndex curIndex() const;
|
||||
/// Append a local file path to the playlist.
|
||||
void appendToPlaylist(const QString& file);
|
||||
/// Append a URL to the playlist.
|
||||
void appendToPlaylist(const QUrl& url);
|
||||
/// Insert a local file path at the given index.
|
||||
/// @return True if inserted successfully, false if index is out of range.
|
||||
bool insertIntoPlaylist(qsizetype index, const QString& file);
|
||||
/// Insert a URL at the given index.
|
||||
/// @return True if inserted successfully, false if index is out of range.
|
||||
bool insertIntoPlaylist(qsizetype index, const QUrl& url);
|
||||
/// Remove the entry at the given index.
|
||||
/// @return True if removed successfully, false if index is out of range.
|
||||
bool removeFromPlaylist(qsizetype index);
|
||||
|
||||
/// Returns the total number of items in the playlist.
|
||||
/// @return Total count of entries in the playlist.
|
||||
qsizetype totalCount() const;
|
||||
/// Try fetching previous index.
|
||||
/// @details This previous index is gallery-loop-aware.
|
||||
/// @return std::nullopt if playlist is empty, otherwise the previous index considering gallery loop.
|
||||
std::optional<qsizetype> previousIndex() const;
|
||||
/// Try fetching next index.
|
||||
/// @details This next index is gallery-loop-aware.
|
||||
/// @return std::nullopt if playlist is empty, otherwise the next index considering gallery loop.
|
||||
std::optional<qsizetype> nextIndex() const;
|
||||
/// Check whether current index is the first index.
|
||||
/// @return False if current index is not the first index or playlist is empty, otherwise true.
|
||||
bool isFirstIndex() const;
|
||||
/// Check whether current index is the last index.
|
||||
/// @return False if current index is not the last index or playlist is empty, otherwise true.
|
||||
bool isLastIndex() const;
|
||||
void setCurrentIndex(const QModelIndex & index);
|
||||
QUrl urlByIndex(const QModelIndex & index);
|
||||
QString localFileByIndex(const QModelIndex & index);
|
||||
bool removeAt(const QModelIndex & index);
|
||||
|
||||
void setAutoLoadFilterSuffixes(const QStringList &nameFilters);
|
||||
|
||||
static QList<QUrl> convertToUrlList(const QStringList & files);
|
||||
/// Returns the current index.
|
||||
/// @return std::nullopt if the playlist is empty, otherwise the current index.
|
||||
std::optional<qsizetype> currentIndex() const;
|
||||
/// Set the current index.
|
||||
/// @remark Index only can be set when playlist is not empty.
|
||||
/// @return True if successful set, including "not changed" set, otherwise false.
|
||||
/// This return value only indicates whether we can set index and given index is valid for setting.
|
||||
/// If "no changed" set occurs, this function returns true, but doesn't raise current index changed signal.
|
||||
bool setCurrentIndex(qsizetype index);
|
||||
/// Returns the URL at the given index.
|
||||
/// @return The URL if index is valid.
|
||||
/// This class guarantee that this QUrl is pointing to a local file.
|
||||
QUrl urlByIndex(qsizetype index) const;
|
||||
/// Returns the local file path at the given index.
|
||||
/// @return The local file path if index is valid.
|
||||
/// This class guarantee that local file path always is valid.
|
||||
QString localFileByIndex(qsizetype index) const;
|
||||
|
||||
signals:
|
||||
void currentIndexChanged(int index);
|
||||
void totalCountChanged(int count);
|
||||
void allowedSuffixesChanged(const QStringList& suffixes);
|
||||
/// Raised when index was changed bu setter.
|
||||
/// The argument holds the new current index.
|
||||
/// Please note that this signal will NOT be raised by the whole playlist change caused index change.
|
||||
void currentIndexChanged(qsizetype index);
|
||||
/// The whole playlist was changed, involving loading, editing and deleting some or full entries.
|
||||
/// The argument holds the total count of new play list.
|
||||
void playlistChanged(qsizetype count);
|
||||
|
||||
private:
|
||||
int m_currentIndex = -1;
|
||||
PlaylistModel m_model;
|
||||
/// The fundamental playlist setter.
|
||||
/// @details All playlist load functions will call this function to set playlist at last.
|
||||
/// @param[in] urls ALl urls in this list should be checked by checkUrl() function.
|
||||
/// @param[in] indexHint Try to set current index to this index if possible.
|
||||
void setPlaylist(const QList<QUrl>&& urls, std::optional<qsizetype> indexHint = std::nullopt);
|
||||
/// Load playlist from a single file.
|
||||
/// @details This function scans the same directory for siblings matching allowed suffixes, sorted naturally.
|
||||
/// @return True if the playlist was loaded successfully, false otherwise.
|
||||
bool loadSingleFilePlaylist(const QUrl& url);
|
||||
/// Load playlist from multiple URLs directly.
|
||||
/// @return True if all URLs are valid, false otherwise.
|
||||
bool loadMultipleFilesPlaylist(const QList<QUrl>&& urls);
|
||||
/// Load playlist from an M3U/M3U8 playlist file.
|
||||
/// @return True if loaded successfully, false otherwise.
|
||||
bool loadM3U8Playlist(const QUrl& url);
|
||||
|
||||
/// Make sure that current index is valid.
|
||||
/// @details This function usually is used by those functions manipulating playlist.
|
||||
void sanitizeIndex();
|
||||
|
||||
/// Check whether given QUrl is proper to be put into playlist.
|
||||
/// @details Only the url pointing to local file can be put into playlist.
|
||||
static bool checkUrl(const QUrl& url);
|
||||
/// Convert a string to local file into a QUrl.
|
||||
/// @param[in] file The string pointing to local file.
|
||||
static QUrl urlFromString(const QString& file);
|
||||
|
||||
std::optional<qsizetype> m_currentIndex;
|
||||
QList<QUrl> m_playlist;
|
||||
QStringList m_allowedSuffixes;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// SPDX-FileCopyrightText: 2026 yyc12345 <yyc12321@outlook.com>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "playlistscout.h"
|
||||
|
||||
#include "playlistscouts/qtscout.h"
|
||||
#include "playlistscouts/explorerscout.h"
|
||||
|
||||
std::optional<PlaylistScout::ScoutResult> PlaylistScout::scout(
|
||||
const QUrl &url, const QStringList &allowedSuffixes) {
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
// In Windows, Qt approach can not aware Windows Explorer custom sort order.
|
||||
// So we need to try using Win32 functions for fetching it at first.
|
||||
if (auto rv = PlaylistScout::Explorer::scout(url, allowedSuffixes); rv.has_value()) {
|
||||
return rv;
|
||||
}
|
||||
#endif // Q_OS_WIN
|
||||
|
||||
// TODO: We may add macOS Finder, KDE Dolphin and etc supports in future.
|
||||
|
||||
// Qt way is the last fallback.
|
||||
return PlaylistScout::Qt::scout(url, allowedSuffixes);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// SPDX-FileCopyrightText: 2026 yyc12345 <yyc12321@outlook.com>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QUrl>
|
||||
#include <QStringList>
|
||||
#include <QList>
|
||||
#include <optional>
|
||||
|
||||
namespace PlaylistScout {
|
||||
/// The result of scout
|
||||
struct ScoutResult {
|
||||
/// The final fetched playlist (it can be empty).
|
||||
/// Each item must be a QUrl pointing to a local file.
|
||||
QList<QUrl> playlist;
|
||||
/// The index of given file located in playlist.
|
||||
/// Nullopt if it is not presented in playlist.
|
||||
std::optional<qsizetype> indexHint;
|
||||
};
|
||||
|
||||
/// @brief Scout available image files in given file's parent dir with given allowed suffixes.
|
||||
/// @param[in] url The path to file whose parent dir is our target dir for finding image files.
|
||||
/// @param[in] allowedSuffixes
|
||||
/// Each items in this list is a wildcard pattern for matching allowed file name, like \c *.png .
|
||||
/// The fetched item will be preserved if its name matching any of them.
|
||||
/// If this list is empty, there is no restrictions for finding files.
|
||||
/// @return Scout result if success, otherwise nullopt.
|
||||
std::optional<ScoutResult> scout(const QUrl &url, const QStringList &allowedSuffixes);
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
// SPDX-FileCopyrightText: 2026 yyc12345 <yyc12321@outlook.com>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "explorerscout.h"
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
|
||||
#include <QDir>
|
||||
#include <QDebug>
|
||||
#include <QFileInfo>
|
||||
#include <QRegularExpression>
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include <exdisp.h> // IShellWindows, CLSID_ShellWindows
|
||||
#include <servprov.h> // IServiceProvider
|
||||
#include <shlguid.h> // SID_STopLevelBrowser
|
||||
#include <shlobj.h>
|
||||
|
||||
// YYC MARK:
|
||||
// We use custom RAII wrappers instead of existing COM smart pointers because the
|
||||
// project may be built with MinGW, which does not have good compatibility with
|
||||
// CComPtr and Microsoft's WRL ComPtr (<wrl/client.h>).
|
||||
// Furthermore, PIDLs are not COM objects (they are freed via ILFree, not Release),
|
||||
// so no standard smart pointer covers them regardless of compiler.
|
||||
|
||||
/// RAII wrapper for COM initialization (CoInitialize / CoUninitialize).
|
||||
/// Must be declared before any COM objects (e.g., ComPtr, UniquePidl) so that
|
||||
/// CoUninitialize runs after all COM objects are released during scope exit.
|
||||
class ComGuard {
|
||||
public:
|
||||
ComGuard() : m_hr(CoInitialize(nullptr)) {}
|
||||
~ComGuard() { if (SUCCEEDED(m_hr)) CoUninitialize(); }
|
||||
ComGuard(const ComGuard&) = delete;
|
||||
ComGuard& operator=(const ComGuard&) = delete;
|
||||
|
||||
bool IsInitialized() const { return SUCCEEDED(m_hr); }
|
||||
|
||||
private:
|
||||
HRESULT m_hr;
|
||||
};
|
||||
|
||||
/// RAII wrapper for COM interface pointers.
|
||||
template <typename T>
|
||||
class ComPtr {
|
||||
public:
|
||||
ComPtr() = default;
|
||||
ComPtr(std::nullptr_t) {}
|
||||
~ComPtr() { Reset(); }
|
||||
ComPtr(const ComPtr&) = delete;
|
||||
ComPtr& operator=(const ComPtr&) = delete;
|
||||
ComPtr(ComPtr&& other) noexcept : m_ptr(other.m_ptr) { other.m_ptr = nullptr; }
|
||||
ComPtr& operator=(ComPtr&& other) noexcept {
|
||||
if (this != &other) {
|
||||
Reset();
|
||||
m_ptr = other.m_ptr;
|
||||
other.m_ptr = nullptr;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
T* operator->() const { return m_ptr; }
|
||||
T* Get() const { return m_ptr; }
|
||||
T** GetAddressOf() { return &m_ptr; }
|
||||
explicit operator bool() const { return m_ptr != nullptr; }
|
||||
void Reset() { if (m_ptr) { m_ptr->Release(); m_ptr = nullptr; } }
|
||||
|
||||
private:
|
||||
T* m_ptr = nullptr;
|
||||
};
|
||||
|
||||
/// RAII wrapper for PIDL (Item ID List).
|
||||
class UniquePidl {
|
||||
public:
|
||||
UniquePidl() = default;
|
||||
explicit UniquePidl(LPITEMIDLIST pidl) : m_pidl(pidl) {}
|
||||
~UniquePidl() { Reset(); }
|
||||
UniquePidl(const UniquePidl&) = delete;
|
||||
UniquePidl& operator=(const UniquePidl&) = delete;
|
||||
UniquePidl(UniquePidl&& other) noexcept : m_pidl(other.m_pidl) { other.m_pidl = nullptr; }
|
||||
UniquePidl& operator=(UniquePidl&& other) noexcept {
|
||||
if (this != &other) {
|
||||
Reset();
|
||||
m_pidl = other.m_pidl;
|
||||
other.m_pidl = nullptr;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
LPCITEMIDLIST Get() const { return m_pidl; }
|
||||
LPITEMIDLIST* GetAddressOf() { return &m_pidl; }
|
||||
explicit operator bool() const { return m_pidl != nullptr; }
|
||||
void Reset() { if (m_pidl) { ILFree(m_pidl); m_pidl = nullptr; } }
|
||||
|
||||
private:
|
||||
LPITEMIDLIST m_pidl = nullptr;
|
||||
};
|
||||
|
||||
/// Outputs a diagnostic warning for debugging Windows Explorer integration failures.
|
||||
static void ReportError(const QString& detail)
|
||||
{
|
||||
qWarning("Windows Explorer file list fetch error: %s",
|
||||
qPrintable(detail));
|
||||
}
|
||||
|
||||
/// Normalizes a Windows directory path for case-insensitive comparison:
|
||||
/// converts to native separators, lowercases, and ensures a trailing backslash.
|
||||
static QString NormalizePath(const QString& path)
|
||||
{
|
||||
QString result = QDir::toNativeSeparators(path).toLower();
|
||||
if (!result.endsWith(QChar('\\'))) {
|
||||
result += QChar('\\');
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Checks whether the Explorer window represented by the given IDispatch is
|
||||
/// displaying the target folder. Returns its IFolderView2 on match, or null.
|
||||
static ComPtr<IFolderView2> TryMatchWindow(IDispatch* dispatch, const QString& normalizedTarget)
|
||||
{
|
||||
// Navigate: IDispatch -> IServiceProvider -> IShellBrowser.
|
||||
ComPtr<IServiceProvider> serviceProvider;
|
||||
if (FAILED(dispatch->QueryInterface(IID_PPV_ARGS(serviceProvider.GetAddressOf())))) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ComPtr<IShellBrowser> shellBrowser;
|
||||
if (FAILED(serviceProvider->QueryService(SID_STopLevelBrowser,
|
||||
IID_PPV_ARGS(shellBrowser.GetAddressOf())))) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// IShellBrowser -> active IShellView.
|
||||
ComPtr<IShellView> shellView;
|
||||
if (FAILED(shellBrowser->QueryActiveShellView(shellView.GetAddressOf()))) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Retrieve the folder's path via IPersistIDList.
|
||||
ComPtr<IPersistIDList> persistIdList;
|
||||
if (FAILED(shellView->QueryInterface(IID_PPV_ARGS(persistIdList.GetAddressOf())))) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UniquePidl folderPidl;
|
||||
if (FAILED(persistIdList->GetIDList(folderPidl.GetAddressOf()))) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
PWSTR pathStr = nullptr;
|
||||
if (FAILED(SHGetNameFromIDList(folderPidl.Get(), SIGDN_DESKTOPABSOLUTEPARSING, &pathStr))) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QString windowPath = NormalizePath(QString::fromWCharArray(pathStr));
|
||||
CoTaskMemFree(pathStr);
|
||||
|
||||
if (windowPath != normalizedTarget) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Path matches — query IFolderView2 from the ShellView.
|
||||
ComPtr<IFolderView2> folderView2;
|
||||
if (FAILED(shellView->QueryInterface(IID_PPV_ARGS(folderView2.GetAddressOf())))) {
|
||||
return nullptr;
|
||||
}
|
||||
return folderView2;
|
||||
}
|
||||
|
||||
/// Tries to find a live Explorer window that is currently displaying the given
|
||||
/// folder path. Returns the IFolderView2 from that window, or an empty ComPtr.
|
||||
///
|
||||
/// Explorer's sort order is stored only in the running Explorer process's memory.
|
||||
/// Creating a new shell view via IShellFolder::CreateViewObject yields a default
|
||||
/// sort (always by name), so we must find the actual live IFolderView2 instead.
|
||||
///
|
||||
/// Reference:
|
||||
/// - https://www.stephenli.ca/getting-windows-explorers-current-directory-sort-order
|
||||
/// - https://github.com/d2phap/ImageGlass/blob/75a296287f338edcf409eee3917531b25ff5d927/v9/Components/ImageGlass.Base/FileSystem/FileFinder.cs
|
||||
/// - NuGet Package: D2Phap.EggShell
|
||||
static ComPtr<IFolderView2> FindLiveFolderView(const QString& targetFolderPath)
|
||||
{
|
||||
// Create the Shell windows collection (all open Explorer windows).
|
||||
ComPtr<IShellWindows> shellWindows;
|
||||
if (FAILED(CoCreateInstance(CLSID_ShellWindows, nullptr, CLSCTX_ALL,
|
||||
IID_PPV_ARGS(shellWindows.GetAddressOf())))) {
|
||||
ReportError("Failed to create IShellWindows instance");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QString normalizedTarget = NormalizePath(targetFolderPath);
|
||||
|
||||
// 1. Try the Desktop window first — it may contain the file the user opened.
|
||||
{
|
||||
VARIANT varLoc;
|
||||
VariantInit(&varLoc);
|
||||
varLoc.vt = VT_I4;
|
||||
varLoc.lVal = 0;
|
||||
|
||||
VARIANT varLocRoot;
|
||||
VariantInit(&varLocRoot);
|
||||
|
||||
long desktopHwnd = 0;
|
||||
ComPtr<IDispatch> desktopDispatch;
|
||||
HRESULT findHr = shellWindows->FindWindowSW(
|
||||
&varLoc, &varLocRoot, SWC_DESKTOP,
|
||||
&desktopHwnd, SWFO_NEEDDISPATCH,
|
||||
desktopDispatch.GetAddressOf());
|
||||
|
||||
VariantClear(&varLoc);
|
||||
VariantClear(&varLocRoot);
|
||||
|
||||
if (SUCCEEDED(findHr) && desktopDispatch) {
|
||||
ComPtr<IFolderView2> result = TryMatchWindow(desktopDispatch.Get(), normalizedTarget);
|
||||
if (result) return result;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Iterate regular Explorer windows.
|
||||
LONG count = 0;
|
||||
shellWindows->get_Count(&count);
|
||||
|
||||
for (LONG i = 0; i < count; i++) {
|
||||
VARIANT varIndex;
|
||||
VariantInit(&varIndex);
|
||||
varIndex.vt = VT_I4;
|
||||
varIndex.lVal = i;
|
||||
|
||||
ComPtr<IDispatch> dispatch;
|
||||
HRESULT itemHr = shellWindows->Item(varIndex, dispatch.GetAddressOf());
|
||||
VariantClear(&varIndex);
|
||||
|
||||
if (FAILED(itemHr) || !dispatch) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ComPtr<IFolderView2> result = TryMatchWindow(dispatch.Get(), normalizedTarget);
|
||||
if (result) return result;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::optional<PlaylistScout::ScoutResult> PlaylistScout::Explorer::scout(
|
||||
const QUrl &url, const QStringList &allowedSuffixes) {
|
||||
PlaylistScout::ScoutResult rv;
|
||||
|
||||
QFileInfo info(url.toLocalFile());
|
||||
QString folderPath = info.path();
|
||||
QString currentFileName = info.fileName();
|
||||
|
||||
// Pre-compile wildcard suffix filters into regular expressions for performance.
|
||||
QList<QRegularExpression> nameRegexps;
|
||||
for (const QString &pattern : allowedSuffixes) {
|
||||
nameRegexps.append(QRegularExpression(
|
||||
QRegularExpression::wildcardToRegularExpression(pattern),
|
||||
QRegularExpression::CaseInsensitiveOption));
|
||||
}
|
||||
|
||||
// Initialize COM. The ComGuard must be declared before any COM objects
|
||||
// below so that CoUninitialize runs after all of them are released.
|
||||
ComGuard comGuard;
|
||||
if (!comGuard.IsInitialized()) {
|
||||
ReportError("COM initialization failed");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Find a live Explorer window that is currently displaying the target folder.
|
||||
ComPtr<IFolderView2> folderView2 = FindLiveFolderView(folderPath);
|
||||
if (!folderView2) {
|
||||
ReportError("No open Explorer window matches the folder: " + folderPath);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Get items already sorted in Explorer's view order, as an IShellItemArray.
|
||||
// SVGIO_ALLVIEW returns all items; SVGIO_FLAG_VIEWORDER respects the view's sort.
|
||||
ComPtr<IShellItemArray> itemArray;
|
||||
if (FAILED(folderView2->Items(SVGIO_ALLVIEW | SVGIO_FLAG_VIEWORDER,
|
||||
IID_PPV_ARGS(itemArray.GetAddressOf())))) {
|
||||
ReportError("IFolderView2::Items(SVGIO_ALLVIEW | SVGIO_FLAG_VIEWORDER) failed");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
DWORD itemCount = 0;
|
||||
itemArray->GetCount(&itemCount);
|
||||
|
||||
// Enumerate sorted items, filter by pattern, and build the playlist.
|
||||
for (DWORD i = 0; i < itemCount; i++) {
|
||||
ComPtr<IShellItem> shellItem;
|
||||
if (FAILED(itemArray->GetItemAt(i, shellItem.GetAddressOf()))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the full parsing path of the item.
|
||||
PWSTR pathStr = nullptr;
|
||||
if (FAILED(shellItem->GetDisplayName(SIGDN_DESKTOPABSOLUTEPARSING, &pathStr)) || !pathStr) {
|
||||
continue;
|
||||
}
|
||||
QString filePath = QString::fromWCharArray(pathStr);
|
||||
CoTaskMemFree(pathStr);
|
||||
|
||||
// Skip directories — the view may include folders alongside files.
|
||||
QFileInfo fileInfo(filePath);
|
||||
if (!fileInfo.isFile()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QString fileName = fileInfo.fileName();
|
||||
|
||||
// Match against compiled wildcard patterns.
|
||||
bool matches = nameRegexps.isEmpty();
|
||||
for (const QRegularExpression &re : std::as_const(nameRegexps)) {
|
||||
if (re.match(fileName).hasMatch()) {
|
||||
matches = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!matches) {
|
||||
continue;
|
||||
}
|
||||
|
||||
rv.playlist.append(QUrl::fromLocalFile(filePath));
|
||||
if (fileName == currentFileName) {
|
||||
rv.indexHint = rv.playlist.size() - 1;
|
||||
}
|
||||
}
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
#endif // Q_OS_WIN
|
||||
@@ -0,0 +1,16 @@
|
||||
// SPDX-FileCopyrightText: 2026 yyc12345 <yyc12321@outlook.com>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../playlistscout.h"
|
||||
#include <QtSystemDetection>
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
|
||||
namespace PlaylistScout::Explorer {
|
||||
std::optional<ScoutResult> scout(const QUrl &url, const QStringList &allowedSuffixes);
|
||||
}
|
||||
|
||||
#endif // Q_OS_WIN
|
||||
@@ -0,0 +1,40 @@
|
||||
// SPDX-FileCopyrightText: 2025 Gary Wang <git@blumia.net>
|
||||
// SPDX-FileCopyrightText: 2026 yyc12345 <yyc12321@outlook.com>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "qtscout.h"
|
||||
|
||||
#include <QFileInfo>
|
||||
#include <QDir>
|
||||
#include <QCollator>
|
||||
|
||||
std::optional<PlaylistScout::ScoutResult> PlaylistScout::Qt::scout(
|
||||
const QUrl &url, const QStringList &allowedSuffixes) {
|
||||
PlaylistScout::ScoutResult rv;
|
||||
|
||||
QFileInfo info(url.toLocalFile());
|
||||
QDir dir(info.path());
|
||||
QString currentFileName = info.fileName();
|
||||
|
||||
QStringList entryList = dir.entryList(
|
||||
allowedSuffixes, QDir::Files | QDir::NoSymLinks, QDir::NoSort);
|
||||
|
||||
QCollator collator;
|
||||
collator.setNumericMode(true);
|
||||
|
||||
std::sort(entryList.begin(), entryList.end(), collator);
|
||||
|
||||
for (qsizetype i = 0; i < entryList.count(); i++) {
|
||||
const QString &fileName = entryList.at(i);
|
||||
const QString &oneEntry = dir.absoluteFilePath(fileName);
|
||||
const QUrl &url = QUrl::fromLocalFile(oneEntry);
|
||||
|
||||
rv.playlist.append(url);
|
||||
if (fileName == currentFileName) {
|
||||
rv.indexHint = i;
|
||||
}
|
||||
}
|
||||
|
||||
return rv;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// SPDX-FileCopyrightText: 2026 yyc12345 <yyc12321@outlook.com>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../playlistscout.h"
|
||||
|
||||
namespace PlaylistScout::Qt {
|
||||
std::optional<ScoutResult> scout(const QUrl &url, const QStringList &allowedSuffixes);
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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") }
|
||||
}
|
||||
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user