Files
sarasacw-picture/app/mainwindow.cpp
T

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

580 lines
16 KiB
C++
Raw Normal View History

// SPDX-FileCopyrightText: 2025 Gary Wang <git@blumia.net>
//
// SPDX-License-Identifier: MIT
#include "mainwindow.h"
2026-07-24 14:57:41 +08:00
#include "ui_mainwindow.h"
#include "settings.h"
#include "bottombuttongroup.h"
#include "graphicsview.h"
#include "navigatorview.h"
#include "graphicsscene.h"
#include "settingsdialog.h"
#include "aboutdialog.h"
#include "metadatamodel.h"
#include "metadatadialog.h"
#include "actionmanager.h"
#include "playlistmanager.h"
#include <QMouseEvent>
#include <QMovie>
#include <QDebug>
#include <QGraphicsTextItem>
#include <QApplication>
#include <QStyle>
#include <QScreen>
#include <QMenu>
#include <QShortcut>
#include <QClipboard>
#include <QMimeData>
#include <QWindow>
#include <QFile>
#include <QTimer>
#include <QFileDialog>
#include <QFileSystemWatcher>
#include <QStandardPaths>
#include <QStringBuilder>
#include <QProcess>
#include <QDesktopServices>
2024-04-19 00:48:28 +08:00
#include <QMessageBox>
#ifdef HAVE_QTDBUS
#include <QDBusInterface>
2022-11-01 20:58:48 +08:00
#include <QDBusConnectionInterface>
#endif // HAVE_QTDBUS
using namespace Qt::Literals::StringLiterals;
MainWindow::MainWindow(QWidget *parent)
2026-07-13 15:59:39 +08:00
: QWidget(parent)
2026-07-24 14:57:41 +08:00
, ui(new QT_PREPEND_NAMESPACE(Ui::MainWindow))
, m_am(new ActionManager)
, m_pm(new PlaylistManager(this))
, m_fileSystemWatcher(new QFileSystemWatcher(this))
{
2026-07-24 14:57:41 +08:00
ui->setupUi(this);
2026-07-13 15:59:39 +08:00
this->setWindowFlags(Qt::Window | Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::WindowMinMaxButtonsHint | Qt::WindowCloseButtonHint);
2026-07-04 16:54:48 +08:00
// YYC MARK:
// Blumis set original value is 350 x 330.
// It is too small for modern device,
// so I level it up to SVGA resolution 800 x 600.
this->setMinimumSize(800, 600);
this->setWindowIcon(QIcon(u":/icons/app-icon.svg"_s));
2024-07-30 22:54:43 +08:00
this->setAcceptDrops(true);
2026-07-24 14:57:41 +08:00
// Initialize playlist manager
2026-07-13 15:40:59 +08:00
m_pm->setAllowedSuffixes(supportedImageFormats());
2026-07-24 14:57:41 +08:00
// Initialize graphics scene
GraphicsScene * scene = new GraphicsScene(this);
2026-07-24 14:57:41 +08:00
// 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){
2026-07-24 14:57:41 +08:00
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();
});
2026-07-24 14:57:41 +08:00
connect(ui->m_gv, &GraphicsView::viewportRectChanged,
ui->m_nav, &NavigatorView::updateMainViewportRegion);
2026-07-24 14:57:41 +08:00
// Setup action manager
m_am->setupAction(this);
2026-07-24 14:57:41 +08:00
// Setup bottom button group
ui->m_bottomButtonGroup->addActions({
m_am->actionActualSize,
2026-07-12 21:00:11 +08:00
m_am->actionFitToScreen,
m_am->actionZoomIn,
m_am->actionZoomOut,
m_am->actionPrevPicture,
2026-07-12 21:00:11 +08:00
m_am->actionProperties,
m_am->actionNextPicture,
2026-07-12 21:00:11 +08:00
m_am->actionRotateCounterClockwise,
m_am->actionRotateClockwise,
m_am->actionFlipHorizontal,
m_am->actionFlipVertical,
2026-07-24 14:57:41 +08:00
});
// Setup right click menus
{
2026-07-24 14:57:41 +08:00
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);
ui->m_subMenuTransform->addSeparator();
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);
}
2026-07-24 14:57:41 +08:00
// Connect playlist manager and file watcher signals for photo browsering.
2026-07-13 15:40:59 +08:00
connect(m_pm, &PlaylistManager::playlistChanged, this, std::bind(&MainWindow::updateActionState, this));
2026-07-13 15:40:59 +08:00
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](){
2026-07-13 15:40:59 +08:00
QTimer::singleShot(500, this, std::bind(&MainWindow::galleryCurrent, this, true));
});
2026-07-24 14:57:41 +08:00
// Change window location
centerWindow();
QTimer::singleShot(0, this, [this](){
m_am->setupShortcuts();
});
}
MainWindow::~MainWindow()
{
2026-07-24 14:57:41 +08:00
delete ui;
}
2026-07-13 15:40:59 +08:00
void MainWindow::showFiles(const QStringList &files)
{
2026-07-13 15:40:59 +08:00
m_pm->loadPlaylist(files);
2026-07-24 14:57:41 +08:00
ui->m_nav->fitInView(ui->m_nav->sceneRect(), Qt::KeepAspectRatio);
2026-07-13 15:40:59 +08:00
}
2026-07-13 15:40:59 +08:00
void MainWindow::showFiles(const QList<QUrl>& urls)
{
m_pm->loadPlaylist(urls);
2026-07-24 14:57:41 +08:00
ui->m_nav->fitInView(ui->m_nav->sceneRect(), Qt::KeepAspectRatio);
}
void MainWindow::initWindowSize()
{
switch (Settings::instance()->initWindowSizeBehavior()) {
case Settings::WindowSizeBehavior::Maximized:
showMaximized();
break;
case Settings::WindowSizeBehavior::Windowed:
default:
showNormal();
break;
}
}
void MainWindow::clearGallery()
{
2026-07-13 15:40:59 +08:00
m_pm->clearPlaylist();
}
void MainWindow::galleryPrev()
{
const bool loopGallery = Settings::instance()->loopGallery();
if (!loopGallery && m_pm->isFirstIndex()) return;
2026-07-13 15:40:59 +08:00
auto index = m_pm->previousIndex();
if (index.has_value()) {
m_pm->setCurrentIndex(*index);
}
}
void MainWindow::galleryNext()
{
const bool loopGallery = Settings::instance()->loopGallery();
if (!loopGallery && m_pm->isLastIndex()) return;
2026-07-13 15:40:59 +08:00
auto index = m_pm->nextIndex();
if (index.has_value()) {
m_pm->setCurrentIndex(*index);
}
}
2026-07-13 15:40:59 +08:00
void MainWindow::galleryCurrent(bool fromFileWatcher)
2024-04-15 01:16:00 +08:00
{
2026-07-13 15:40:59 +08:00
auto index = m_pm->currentIndex();
if (index.has_value()) {
QString localFilePath = m_pm->localFileByIndex(*index);
2026-07-24 14:57:41 +08:00
ui->m_gv->showFileFromPath(localFilePath);
2026-07-13 15:40:59 +08:00
if (!fromFileWatcher) {
updateFileWatcher(localFilePath);
setWindowTitle(m_pm->urlByIndex(*index).fileName());
}
} else {
2026-07-24 14:57:41 +08:00
ui->m_gv->showText(tr("Drag image here"));
setWindowTitle(QString());
2026-07-13 15:40:59 +08:00
updateFileWatcher();
2024-04-15 01:16:00 +08:00
}
2026-07-13 15:40:59 +08:00
updateActionState();
2024-04-15 01:16:00 +08:00
}
QStringList MainWindow::supportedImageFormats()
{
QStringList formatFilters {
#if QT_VERSION < QT_VERSION_CHECK(6, 8, 0)
QStringLiteral("*.jfif")
#endif // QT_VERSION < QT_VERSION_CHECK(6, 8, 0)
};
for (const QByteArray &item : QImageReader::supportedImageFormats()) {
formatFilters.append(QStringLiteral("*.") % QString::fromLocal8Bit(item));
}
return formatFilters;
}
void MainWindow::wheelEvent(QWheelEvent *event)
{
QPoint numDegrees = event->angleDelta() / 8;
bool needWeelEvent = false, wheelUp = false;
bool actionIsZoom = event->modifiers().testFlag(Qt::ControlModifier)
|| Settings::instance()->mouseWheelBehavior() == Settings::MouseWheelBehavior::Zoom;
// NOTE: Only checking angleDelta since the QWheelEvent::pixelDelta() doc says
// pixelDelta() value is driver specific and unreliable on X11...
// We are not scrolling the canvas, just zoom in or out, so it probably
// doesn't matter here.
if (!numDegrees.isNull() && numDegrees.y() != 0) {
needWeelEvent = true;
wheelUp = numDegrees.y() > 0;
}
if (needWeelEvent) {
if (actionIsZoom) {
if (wheelUp) {
on_actionZoomIn_triggered();
} else {
on_actionZoomOut_triggered();
}
} else {
if (wheelUp) {
galleryPrev();
} else {
galleryNext();
}
}
event->accept();
} else {
2026-07-13 15:59:39 +08:00
QWidget::wheelEvent(event);
}
}
void MainWindow::contextMenuEvent(QContextMenuEvent *event)
{
2026-07-24 14:57:41 +08:00
ui->m_menu->exec(mapToGlobal(event->pos()));
2026-07-13 15:59:39 +08:00
return QWidget::contextMenuEvent(event);
}
2024-07-30 22:54:43 +08:00
void MainWindow::dragEnterEvent(QDragEnterEvent *event)
{
2026-07-13 15:40:59 +08:00
if (event->mimeData()->hasUrls()) {
2024-07-30 22:54:43 +08:00
event->acceptProposedAction();
} else {
event->ignore();
}
2026-07-13 15:59:39 +08:00
return QWidget::dragEnterEvent(event);
2024-07-30 22:54:43 +08:00
}
void MainWindow::dragMoveEvent(QDragMoveEvent *event)
{
Q_UNUSED(event)
}
void MainWindow::dropEvent(QDropEvent *event)
{
event->acceptProposedAction();
const QMimeData * mimeData = event->mimeData();
if (mimeData->hasUrls()) {
2026-07-13 15:40:59 +08:00
showFiles(mimeData->urls());
2024-07-30 22:54:43 +08:00
} else {
2026-07-24 14:57:41 +08:00
ui->m_gv->showText(tr("Not supported mimedata: %1").arg(mimeData->formats().first()));
2024-07-30 22:54:43 +08:00
}
}
void MainWindow::centerWindow()
{
this->setGeometry(
QStyle::alignedRect(
Qt::LeftToRight,
Qt::AlignCenter,
this->size(),
qApp->screenAt(QCursor::pos())->availableGeometry()
)
);
}
void MainWindow::toggleAvoidResetTransform()
{
2026-07-24 14:57:41 +08:00
ui->m_gv->setAvoidResetTransform(!ui->m_gv->isAvoidResetTransform());
}
void MainWindow::toggleFullscreen()
{
if (isFullScreen()) {
showNormal();
} else {
showFullScreen();
}
}
void MainWindow::toggleMaximize()
{
if (isMaximized()) {
showNormal();
} else {
showMaximized();
}
}
QSize MainWindow::sizeHint() const
{
return QSize(710, 530);
}
2026-07-12 21:00:11 +08:00
// region: Action Trigger
void MainWindow::on_actionCopyPixmap_triggered()
{
QClipboard *cb = QApplication::clipboard();
2026-07-24 14:57:41 +08:00
cb->setPixmap(ui->m_gv->scene()->renderToPixmap());
}
void MainWindow::on_actionCopyFilePath_triggered()
{
2026-07-13 15:40:59 +08:00
auto index = m_pm->currentIndex();
if (!index.has_value()) return;
QClipboard *cb = QApplication::clipboard();
cb->setText(m_pm->localFileByIndex(*index));
}
2026-07-12 21:00:11 +08:00
void MainWindow::on_actionPrevPicture_triggered()
{
2026-07-12 21:00:11 +08:00
galleryPrev();
}
2026-07-12 21:00:11 +08:00
void MainWindow::on_actionNextPicture_triggered()
{
galleryNext();
}
2026-07-12 21:00:11 +08:00
void MainWindow::on_actionActualSize_triggered()
{
2026-07-24 14:57:41 +08:00
ui->m_gv->resetScale();
ui->m_gv->setEnableAutoFitToScreen(false);
2026-07-12 21:00:11 +08:00
}
void MainWindow::on_actionFitToScreen_triggered()
{
2026-07-24 14:57:41 +08:00
ui->m_gv->fitInView(ui->m_nav->sceneRect(), Qt::KeepAspectRatio);
ui->m_gv->setEnableAutoFitToScreen(ui->m_gv->scaleFactor() <= 1);
2026-07-12 21:00:11 +08:00
}
void MainWindow::on_actionFitByWidth_triggered()
{
2026-07-24 14:57:41 +08:00
ui->m_gv->fitByOrientation(Qt::Orientation::Horizontal);
2026-07-12 21:00:11 +08:00
}
void MainWindow::on_actionFitByHeight_triggered()
{
2026-07-24 14:57:41 +08:00
ui->m_gv->fitByOrientation(Qt::Orientation::Vertical);
2026-07-12 21:00:11 +08:00
}
void MainWindow::on_actionZoomIn_triggered()
{
2026-07-24 14:57:41 +08:00
if (ui->m_gv->scaleFactor() < 1000) {
ui->m_gv->zoomView(1.25);
}
2024-04-15 01:16:00 +08:00
}
2026-07-12 21:00:11 +08:00
void MainWindow::on_actionZoomOut_triggered()
{
2026-07-24 14:57:41 +08:00
ui->m_gv->zoomView(0.8);
2026-07-12 21:00:11 +08:00
}
void MainWindow::on_actionFlipHorizontal_triggered()
{
2026-07-24 14:57:41 +08:00
ui->m_gv->flipView(true);
2026-07-12 21:00:11 +08:00
}
void MainWindow::on_actionFlipVertical_triggered()
{
2026-07-24 14:57:41 +08:00
ui->m_gv->flipView(false);
2026-07-12 21:00:11 +08:00
}
void MainWindow::on_actionRotateClockwise_triggered()
{
2026-07-24 14:57:41 +08:00
ui->m_gv->rotateView(true);
ui->m_gv->displayScene();
2026-07-12 21:00:11 +08:00
}
void MainWindow::on_actionRotateCounterClockwise_triggered()
{
2026-07-24 14:57:41 +08:00
ui->m_gv->rotateView(false);
ui->m_gv->displayScene();
2026-07-12 21:00:11 +08:00
}
void MainWindow::on_actionToggleAvoidResetTransform_triggered()
{
toggleAvoidResetTransform();
}
void MainWindow::on_actionToggleCheckerboard_triggered()
{
2026-07-24 14:57:41 +08:00
ui->m_gv->toggleCheckerboard(QGuiApplication::queryKeyboardModifiers().testFlag(Qt::ShiftModifier));
2026-07-12 21:00:11 +08:00
}
void MainWindow::on_actionTogglePauseAnimation_triggered()
{
2026-07-24 14:57:41 +08:00
ui->m_gv->scene()->togglePauseAnimation();
2026-07-12 21:00:11 +08:00
}
void MainWindow::on_actionAnimationNextFrame_triggered()
{
2026-07-24 14:57:41 +08:00
ui->m_gv->scene()->skipAnimationFrame(1);
2026-07-12 21:00:11 +08:00
}
2024-04-15 01:16:00 +08:00
void MainWindow::on_actionTrash_triggered()
{
2026-07-12 21:00:11 +08:00
auto index = m_pm->currentIndex();
if (!index.has_value()) return;
if (!m_pm->urlByIndex(*index).isLocalFile()) return;
2024-04-15 01:16:00 +08:00
2026-07-12 21:00:11 +08:00
QFile file(m_pm->localFileByIndex(*index));
2024-04-15 01:16:00 +08:00
QFileInfo fileInfo(file.fileName());
QMessageBox::StandardButton result = QMessageBox::question(this, tr("Move to Trash"),
tr("Are you sure you want to move \"%1\" to recycle bin?").arg(fileInfo.fileName()));
if (result == QMessageBox::Yes) {
bool succ = file.moveToTrash();
if (!succ) {
QMessageBox::warning(this, tr("Failed to move file to trash"),
2024-04-15 01:16:00 +08:00
tr("Move to trash failed, it might caused by file permission issue, file system limitation, or platform limitation."));
} else {
2026-07-13 15:40:59 +08:00
m_pm->removeFromPlaylist(*index);
2024-04-15 01:16:00 +08:00
}
}
}
void MainWindow::on_actionLocateInFileManager_triggered()
{
2026-07-13 15:40:59 +08:00
auto index = m_pm->currentIndex();
if (!index.has_value()) return;
QUrl currentFileUrl = m_pm->urlByIndex(*index);
if (!currentFileUrl.isValid()) return;
QFileInfo fileInfo(currentFileUrl.toLocalFile());
if (!fileInfo.exists()) return;
QUrl && folderUrl = QUrl::fromLocalFile(fileInfo.absolutePath());
#ifdef Q_OS_WIN
QProcess::startDetached("explorer", QStringList() << "/select," << QDir::toNativeSeparators(fileInfo.absoluteFilePath()));
#elif defined(Q_OS_LINUX) and defined(HAVE_QTDBUS)
// Use https://www.freedesktop.org/wiki/Specifications/file-manager-interface/ if possible
2022-11-01 20:58:48 +08:00
const QDBusConnectionInterface * dbusIface = QDBusConnection::sessionBus().interface();
if (!dbusIface || !dbusIface->isServiceRegistered(QLatin1String("org.freedesktop.FileManager1"))) {
QDesktopServices::openUrl(folderUrl);
return;
}
QDBusInterface fm1Iface(u"org.freedesktop.FileManager1"_s,
u"/org/freedesktop/FileManager1"_s,
u"org.freedesktop.FileManager1"_s);
2022-11-01 20:58:48 +08:00
fm1Iface.setTimeout(1000);
fm1Iface.callWithArgumentList(QDBus::Block, "ShowItems", {
QStringList{currentFileUrl.toString()},
2022-11-01 20:58:48 +08:00
QString()
});
if (fm1Iface.lastError().isValid()) {
QDesktopServices::openUrl(folderUrl);
}
#else
QDesktopServices::openUrl(folderUrl);
#endif // Q_OS_WIN
}
2026-07-12 21:00:11 +08:00
void MainWindow::on_actionProperties_triggered()
{
2026-07-13 15:40:59 +08:00
auto index = m_pm->currentIndex();
if (!index.has_value()) return;
QUrl currentFileUrl = m_pm->urlByIndex(*index);
2026-07-12 21:00:11 +08:00
if (!currentFileUrl.isValid()) return;
MetadataModel * md = new MetadataModel();
md->setFile(currentFileUrl.toLocalFile());
MetadataDialog * ad = new MetadataDialog(this);
ad->setMetadataModel(md);
ad->exec();
ad->deleteLater();
}
2026-07-12 21:00:11 +08:00
void MainWindow::on_actionSettings_triggered()
{
SettingsDialog * sd = new SettingsDialog(this);
sd->exec();
sd->deleteLater();
}
void MainWindow::on_actionHelp_triggered()
{
AboutDialog * ad = new AboutDialog(this);
ad->exec();
ad->deleteLater();
}
// endregion
bool MainWindow::updateFileWatcher(const QString &basePath)
{
m_fileSystemWatcher->removePaths(m_fileSystemWatcher->files());
if (!basePath.isEmpty()) return m_fileSystemWatcher->addPath(basePath);
return false;
}
2026-07-13 15:40:59 +08:00
void MainWindow::updateActionState() {
2026-07-24 14:57:41 +08:00
m_am->updateActionState(m_pm, ui->m_gv);
2026-07-13 15:40:59 +08:00
}