1
0
Files
sarasacw-picture/app/mainwindow.cpp
yyc12345 cd2eac44f1 revert: revert feature about saving image as new file with different format.
This reverts commit 5b2255e903.

The feature about saving image as new file with different format is not the goal of an image viewer.
There is no need to keep feature diversity in this fork.
So I simply remove it.
2026-07-02 15:48:50 +08:00

961 lines
29 KiB
C++

// SPDX-FileCopyrightText: 2025 Gary Wang <git@blumia.net>
//
// SPDX-License-Identifier: MIT
#include "mainwindow.h"
#include "settings.h"
#include "toolbutton.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>
#include <QMessageBox>
#ifdef HAVE_QTDBUS
#include <QDBusInterface>
#include <QDBusConnectionInterface>
#endif // HAVE_QTDBUS
using namespace Qt::Literals::StringLiterals;
MainWindow::MainWindow(QWidget *parent)
: FramelessWindow(parent)
, m_am(new ActionManager)
, m_pm(new PlaylistManager(this))
, m_fileSystemWatcher(new QFileSystemWatcher(this))
{
if (Settings::instance()->stayOnTop()) {
this->setWindowFlag(Qt::WindowStaysOnTopHint);
}
this->setAttribute(Qt::WA_TranslucentBackground, true);
this->setMinimumSize(350, 330);
this->setWindowIcon(QIcon(u":/icons/app-icon.svg"_s));
this->setMouseTracking(true);
this->setAcceptDrops(true);
m_pm->setAutoLoadFilterSuffixes(supportedImageFormats());
m_fadeOutAnimation = new QPropertyAnimation(this, "windowOpacity"_ba);
m_fadeOutAnimation->setDuration(300);
m_fadeOutAnimation->setStartValue(1);
m_fadeOutAnimation->setEndValue(0);
m_floatUpAnimation = new QPropertyAnimation(this, "geometry"_ba);
m_floatUpAnimation->setDuration(300);
m_floatUpAnimation->setEasingCurve(QEasingCurve::OutCirc);
m_exitAnimationGroup = new QParallelAnimationGroup(this);
m_exitAnimationGroup->addAnimation(m_fadeOutAnimation);
m_exitAnimationGroup->addAnimation(m_floatUpAnimation);
connect(m_exitAnimationGroup, &QParallelAnimationGroup::finished,
this, &MainWindow::doCloseWindow);
GraphicsScene * scene = new GraphicsScene(this);
m_graphicsView = new GraphicsView(this);
m_graphicsView->setScene(scene);
this->setCentralWidget(m_graphicsView);
m_gv = new NavigatorView(this);
m_gv->setFixedSize(220, 160);
m_gv->setScene(scene);
m_gv->setMainView(m_graphicsView);
m_gv->fitInView(m_gv->sceneRect(), Qt::KeepAspectRatio);
connect(m_graphicsView, &GraphicsView::navigatorViewRequired,
this, [this](bool required, const QTransform & tf){
m_gv->setTransform(GraphicsView::resetScale(tf));
m_gv->fitInView(m_gv->sceneRect(), Qt::KeepAspectRatio);
m_gv->setVisible(required);
m_gv->updateMainViewportRegion();
});
connect(m_graphicsView, &GraphicsView::viewportRectChanged,
m_gv, &NavigatorView::updateMainViewportRegion);
m_closeButton = new ToolButton(true, m_graphicsView);
m_closeButton->setIconSize(QSize(32, 32));
m_closeButton->setFixedSize(QSize(50, 50));
m_closeButton->setIconResourcePath(":/icons/window-close.svg");
connect(m_closeButton, &QAbstractButton::clicked,
this, &MainWindow::closeWindow);
m_prevButton = new ToolButton(false, m_graphicsView);
m_prevButton->setIconSize(QSize(75, 75));
m_prevButton->setIconResourcePath(":/icons/go-previous.svg");
m_prevButton->setVisible(false);
m_prevButton->setOpacity(0, false);
m_nextButton = new ToolButton(false, m_graphicsView);
m_nextButton->setIconSize(QSize(75, 75));
m_nextButton->setIconResourcePath(":/icons/go-next.svg");
m_nextButton->setVisible(false);
m_nextButton->setOpacity(0, false);
connect(m_prevButton, &QAbstractButton::clicked,
this, &MainWindow::galleryPrev);
connect(m_nextButton, &QAbstractButton::clicked,
this, &MainWindow::galleryNext);
m_am->setupAction(this);
m_bottomButtonGroup = new BottomButtonGroup({
m_am->actionActualSize,
m_am->actionToggleMaximize,
m_am->actionZoomIn,
m_am->actionZoomOut,
m_am->actionToggleCheckerboard,
m_am->actionRotateClockwise
}, this);
m_bottomButtonGroup->setOpacity(0, false);
m_gv->setOpacity(0, false);
m_closeButton->setOpacity(0, false);
connect(m_pm, &PlaylistManager::totalCountChanged, this, &MainWindow::updateGalleryButtonsVisibility);
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));
connect(m_fileSystemWatcher, &QFileSystemWatcher::fileChanged, this, [this](){
QTimer::singleShot(500, this, std::bind(&MainWindow::galleryCurrent, this, false, true));
});
QShortcut * fullscreenShorucut = new QShortcut(QKeySequence(QKeySequence::FullScreen), this);
connect(fullscreenShorucut, &QShortcut::activated,
this, &MainWindow::toggleFullscreen);
centerWindow();
QTimer::singleShot(0, this, [this](){
m_am->setupShortcuts();
Settings::instance()->applyUserShortcuts(this);
});
// allow some mouse events can go through these widgets for resizing window.
installResizeCapture(m_closeButton);
installResizeCapture(m_graphicsView);
installResizeCapture(m_graphicsView->viewport());
installResizeCapture(m_gv);
installResizeCapture(m_gv->viewport());
}
MainWindow::~MainWindow()
{
}
void MainWindow::showUrls(const QList<QUrl> &urls)
{
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_graphicsView->showFileFromPath(firstUrl.toLocalFile());
m_pm->loadPlaylist(urls);
} else {
m_graphicsView->showText(tr("File url list is empty"));
m_pm->setPlaylist(urls);
return;
}
m_gv->fitInView(m_gv->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:
showNormal();
break;
default:
adjustWindowSizeBySceneRect();
break;
}
}
void MainWindow::adjustWindowSizeBySceneRect()
{
if (m_pm->totalCount() < 1) return;
QSize sceneSize = m_graphicsView->sceneRect().toRect().size();
QSize sceneSizeWithMargins = sceneSize + QSize(130, 125);
if (m_graphicsView->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_graphicsView->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({});
}
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_graphicsView->showFileFromPath(m_pm->localFileByIndex(index));
}
}
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_graphicsView->showFileFromPath(m_pm->localFileByIndex(index));
}
}
// Only use this to update minor information.
void MainWindow::galleryCurrent(bool showLoadImageHintWhenEmpty, bool reloadImage)
{
QModelIndex index = m_pm->curIndex();
bool shouldResetfileWatcher = true;
if (index.isValid()) {
const QString & localFilePath(m_pm->localFileByIndex(index));
if (reloadImage) m_graphicsView->showFileFromPath(localFilePath);
shouldResetfileWatcher = !updateFileWatcher(localFilePath);
setWindowTitle(m_pm->urlByIndex(index).fileName());
} else if (showLoadImageHintWhenEmpty && m_pm->totalCount() <= 0) {
m_graphicsView->showText(QCoreApplication::translate("GraphicsScene", "Drag image here"));
setWindowTitle(QString());
}
updateGalleryButtonsVisibility();
if (shouldResetfileWatcher) updateFileWatcher();
}
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::showEvent(QShowEvent *event)
{
updateWidgetsPosition();
return FramelessWindow::showEvent(event);
}
void MainWindow::enterEvent(QEnterEvent *event)
{
m_bottomButtonGroup->setOpacity(1);
m_gv->setOpacity(1);
m_closeButton->setOpacity(1);
m_prevButton->setOpacity(1);
m_nextButton->setOpacity(1);
return FramelessWindow::enterEvent(event);
}
void MainWindow::leaveEvent(QEvent *event)
{
m_bottomButtonGroup->setOpacity(0);
m_gv->setOpacity(0);
m_closeButton->setOpacity(0);
m_prevButton->setOpacity(0);
m_nextButton->setOpacity(0);
return FramelessWindow::leaveEvent(event);
}
void MainWindow::mousePressEvent(QMouseEvent *event)
{
if (event->buttons() & Qt::LeftButton && !isMaximized()) {
m_clickedOnWindow = true;
m_oldMousePos = event->pos();
// qDebug() << m_oldMousePos << m_graphicsView->transform().m11()
// << m_graphicsView->transform().m22() << m_graphicsView->matrix().m12();
event->accept();
}
return FramelessWindow::mousePressEvent(event);
}
void MainWindow::mouseMoveEvent(QMouseEvent *event)
{
if (event->buttons() & Qt::LeftButton && m_clickedOnWindow && !isMaximized() && !isFullScreen()) {
if (!window()->windowHandle()->startSystemMove()) {
move(event->globalPosition().toPoint() - m_oldMousePos);
}
event->accept();
}
return FramelessWindow::mouseMoveEvent(event);
}
void MainWindow::mouseReleaseEvent(QMouseEvent *event)
{
m_clickedOnWindow = false;
// It seems the forward/back mouse button won't generate a key event [1] so we can't use
// QShortcut or QKeySequence to indicate these shortcuts, so we do it here.
// Reference:
// [1]: https://codereview.qt-project.org/c/qt/qtbase/+/177475
if (event->button() == Qt::ForwardButton || event->button() == Qt::BackButton) {
event->button() == Qt::BackButton ? galleryPrev() : galleryNext();
event->accept();
}
return FramelessWindow::mouseReleaseEvent(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;
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 {
FramelessWindow::wheelEvent(event);
}
}
void MainWindow::resizeEvent(QResizeEvent *event)
{
updateWidgetsPosition();
return FramelessWindow::resizeEvent(event);
}
void MainWindow::contextMenuEvent(QContextMenuEvent *event)
{
QMenu * menu = new QMenu;
QMenu * copyMenu = new QMenu(tr("&Copy"));
QUrl currentFileUrl = currentImageFileUrl();
QAction * copyPixmap = m_am->actionCopyPixmap;
QAction * copyFilePath = m_am->actionCopyFilePath;
copyMenu->setIcon(QIcon::fromTheme(u"edit-copy"_s));
copyMenu->addAction(copyPixmap);
if (currentFileUrl.isValid()) {
copyMenu->addAction(copyFilePath);
}
QAction * paste = m_am->actionPaste;
QAction * trash = m_am->actionTrash;
QAction * stayOnTopMode = m_am->actionToggleStayOnTop;
stayOnTopMode->setCheckable(true);
stayOnTopMode->setChecked(stayOnTop());
QAction * protectedMode = m_am->actionToggleProtectMode;
protectedMode->setCheckable(true);
protectedMode->setChecked(m_protectedMode);
QAction * avoidResetTransform = m_am->actionToggleAvoidResetTransform;
avoidResetTransform->setCheckable(true);
avoidResetTransform->setChecked(m_graphicsView->avoidResetTransform());
QAction * toggleSettings = m_am->actionSettings;
QAction * helpAction = m_am->actionHelp;
QAction * propertiesAction = m_am->actionProperties;
#if 0
menu->addAction(m_am->actionOpen);
#endif // 0
if (copyMenu->actions().count() == 1) {
menu->addActions(copyMenu->actions());
} else {
menu->addMenu(copyMenu);
}
if (canPaste()) {
menu->addAction(paste);
}
menu->addSeparator();
menu->addAction(m_am->actionHorizontalFlip);
#if 0
menu->addAction(m_am->actionFitInView);
menu->addAction(m_am->actionFitByWidth);
#endif // 0
menu->addSeparator();
menu->addAction(stayOnTopMode);
menu->addAction(protectedMode);
menu->addAction(avoidResetTransform);
menu->addSeparator();
menu->addAction(toggleSettings);
menu->addAction(helpAction);
if (currentFileUrl.isValid()) {
menu->addSeparator();
if (currentFileUrl.isLocalFile()) {
menu->addAction(trash);
menu->addAction(m_am->actionLocateInFileManager);
}
menu->addAction(propertiesAction);
}
menu->exec(mapToGlobal(event->pos()));
menu->deleteLater();
copyMenu->deleteLater();
return FramelessWindow::contextMenuEvent(event);
}
void MainWindow::dragEnterEvent(QDragEnterEvent *event)
{
if (event->mimeData()->hasUrls() || event->mimeData()->hasImage() || event->mimeData()->hasText()) {
event->acceptProposedAction();
} else {
event->ignore();
}
return FramelessWindow::dragEnterEvent(event);
}
void MainWindow::dragMoveEvent(QDragMoveEvent *event)
{
Q_UNUSED(event)
}
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_graphicsView->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_graphicsView->showText(tr("Image data is invalid"));
} else {
m_graphicsView->showImage(pixmap);
}
} else if (mimeData->hasText()) {
m_graphicsView->showText(mimeData->text());
} else {
m_graphicsView->showText(tr("Not supported mimedata: %1").arg(mimeData->formats().first()));
}
}
void MainWindow::centerWindow()
{
this->setGeometry(
QStyle::alignedRect(
Qt::LeftToRight,
Qt::AlignCenter,
this->size(),
qApp->screenAt(QCursor::pos())->availableGeometry()
)
);
}
void MainWindow::closeWindow()
{
if (Settings::instance()->useBuiltInCloseAnimation()) {
QRect windowRect(this->geometry());
m_floatUpAnimation->setStartValue(windowRect);
m_floatUpAnimation->setEndValue(windowRect.adjusted(0, -80, 0, 0));
m_floatUpAnimation->setStartValue(QRect(this->geometry().x(), this->geometry().y(), this->geometry().width(), this->geometry().height()));
m_floatUpAnimation->setEndValue(QRect(this->geometry().x(), this->geometry().y()-80, this->geometry().width(), this->geometry().height()));
m_exitAnimationGroup->start();
} else {
doCloseWindow();
}
}
void MainWindow::updateWidgetsPosition()
{
m_closeButton->move(width() - m_closeButton->width(), 0);
m_prevButton->move(25, (height() - m_prevButton->sizeHint().height()) / 2);
m_nextButton->move(width() - m_nextButton->sizeHint().width() - 25,
(height() - m_prevButton->sizeHint().height()) / 2);
m_bottomButtonGroup->move((width() - m_bottomButtonGroup->width()) / 2,
height() - m_bottomButtonGroup->height());
m_gv->move(width() - m_gv->width(), height() - m_gv->height());
}
void MainWindow::toggleProtectedMode()
{
m_protectedMode = !m_protectedMode;
m_closeButton->setVisible(!m_protectedMode);
updateGalleryButtonsVisibility();
}
void MainWindow::toggleStayOnTop()
{
setWindowFlag(Qt::WindowStaysOnTopHint, !stayOnTop());
show();
}
void MainWindow::toggleAvoidResetTransform()
{
m_graphicsView->setAvoidResetTransform(!m_graphicsView->avoidResetTransform());
}
bool MainWindow::stayOnTop() const
{
return windowFlags().testFlag(Qt::WindowStaysOnTopHint);
}
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)
{
if (!m_protectedMode || force) {
closeWindow();
}
}
void MainWindow::toggleFullscreen()
{
if (isFullScreen()) {
showNormal();
} else {
showFullScreen();
}
}
void MainWindow::toggleMaximize()
{
if (isMaximized()) {
showNormal();
} else {
showMaximized();
}
}
QSize MainWindow::sizeHint() const
{
return QSize(710, 530);
}
void MainWindow::on_actionOpen_triggered()
{
QStringList picturesLocations = QStandardPaths::standardLocations(QStandardPaths::PicturesLocation);
QUrl pictureUrl = picturesLocations.isEmpty() ? QUrl::fromLocalFile(picturesLocations.first())
: QUrl::fromLocalFile(QDir::homePath());
QList<QUrl> urls(QFileDialog::getOpenFileUrls(this, QString(), pictureUrl));
if (!urls.isEmpty()) {
showUrls(urls);
}
}
void MainWindow::on_actionActualSize_triggered()
{
m_graphicsView->resetScale();
m_graphicsView->setEnableAutoFitInView(false);
m_graphicsView->setLongImageMode(false);
}
void MainWindow::on_actionToggleMaximize_triggered()
{
toggleMaximize();
}
void MainWindow::on_actionZoomIn_triggered()
{
if (m_graphicsView->scaleFactor() < 1000) {
m_graphicsView->zoomView(1.25);
}
}
void MainWindow::on_actionZoomOut_triggered()
{
m_graphicsView->zoomView(0.8);
}
void MainWindow::on_actionHorizontalFlip_triggered()
{
m_graphicsView->flipView();
}
void MainWindow::on_actionFitInView_triggered()
{
m_graphicsView->fitInView(m_gv->sceneRect(), Qt::KeepAspectRatio);
m_graphicsView->setEnableAutoFitInView(m_graphicsView->scaleFactor() <= 1);
m_graphicsView->setLongImageMode(false);
}
void MainWindow::on_actionFitByWidth_triggered()
{
m_graphicsView->fitByOrientation();
}
void MainWindow::on_actionFitLongImage_triggered()
{
m_graphicsView->setLongImageMode(true);
m_graphicsView->fitLongImage();
}
void MainWindow::on_actionCopyPixmap_triggered()
{
QClipboard *cb = QApplication::clipboard();
cb->setPixmap(m_graphicsView->scene()->renderToPixmap());
}
void MainWindow::on_actionCopyFilePath_triggered()
{
QUrl currentFileUrl(currentImageFileUrl());
if (currentFileUrl.isValid()) {
QClipboard *cb = QApplication::clipboard();
cb->setText(currentFileUrl.toLocalFile());
}
}
void MainWindow::on_actionPaste_triggered()
{
QImage clipboardImage;
QUrl clipboardFileUrl;
const QMimeData * clipboardData = QApplication::clipboard()->mimeData();
if (clipboardData->hasImage()) {
QVariant imageVariant(clipboardData->imageData());
if (imageVariant.isValid()) {
clipboardImage = qvariant_cast<QImage>(imageVariant);
}
} else if (clipboardData->hasText()) {
QString clipboardText(clipboardData->text());
if (clipboardText.startsWith("PICTURE:")) {
QString maybeFilename(clipboardText.mid(8));
if (QFile::exists(maybeFilename)) {
clipboardFileUrl = QUrl::fromLocalFile(maybeFilename);
}
}
}
if (!clipboardImage.isNull()) {
setWindowTitle(tr("Image From Clipboard"));
m_graphicsView->showImage(clipboardImage);
clearGallery();
} else if (clipboardFileUrl.isValid()) {
m_graphicsView->showFileFromPath(clipboardFileUrl.toLocalFile());
m_pm->loadPlaylist(clipboardFileUrl);
}
}
void MainWindow::on_actionTrash_triggered()
{
QModelIndex index = m_pm->curIndex();
if (!m_pm->urlByIndex(index).isLocalFile()) return;
QFile file(m_pm->localFileByIndex(index));
QFileInfo fileInfo(file.fileName());
QMessageBox::StandardButton result = QMessageBox::question(this, tr("Move to Trash"),
tr("Are you sure you want to move \"%1\" to recycle bin?").arg(fileInfo.fileName()));
if (result == QMessageBox::Yes) {
bool succ = file.moveToTrash();
if (!succ) {
QMessageBox::warning(this, tr("Failed to move file to trash"),
tr("Move to trash failed, it might caused by file permission issue, file system limitation, or platform limitation."));
} else {
m_pm->removeAt(index);
galleryCurrent(true, true);
}
}
}
void MainWindow::on_actionToggleCheckerboard_triggered()
{
// TODO: is that okay to do this since we plan to support custom shortcuts?
m_graphicsView->toggleCheckerboard(QGuiApplication::queryKeyboardModifiers().testFlag(Qt::ShiftModifier));
}
void MainWindow::on_actionRotateClockwise_triggered()
{
m_graphicsView->rotateView();
m_graphicsView->displayScene();
}
void MainWindow::on_actionRotateCounterClockwise_triggered()
{
m_graphicsView->rotateView(false);
m_graphicsView->displayScene();
}
void MainWindow::on_actionPrevPicture_triggered()
{
galleryPrev();
}
void MainWindow::on_actionNextPicture_triggered()
{
galleryNext();
}
void MainWindow::on_actionTogglePauseAnimation_triggered()
{
m_graphicsView->scene()->togglePauseAnimation();
}
void MainWindow::on_actionAnimationNextFrame_triggered()
{
m_graphicsView->scene()->skipAnimationFrame(1);
}
void MainWindow::on_actionToggleStayOnTop_triggered()
{
toggleStayOnTop();
}
void MainWindow::on_actionToggleProtectMode_triggered()
{
toggleProtectedMode();
}
void MainWindow::on_actionToggleAvoidResetTransform_triggered()
{
toggleAvoidResetTransform();
}
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();
}
void MainWindow::on_actionProperties_triggered()
{
QUrl currentFileUrl = currentImageFileUrl();
if (!currentFileUrl.isValid()) return;
MetadataModel * md = new MetadataModel();
md->setFile(currentFileUrl.toLocalFile());
MetadataDialog * ad = new MetadataDialog(this);
ad->setMetadataModel(md);
ad->exec();
ad->deleteLater();
}
void MainWindow::on_actionLocateInFileManager_triggered()
{
QUrl currentFileUrl = currentImageFileUrl();
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
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);
fm1Iface.setTimeout(1000);
fm1Iface.callWithArgumentList(QDBus::Block, "ShowItems", {
QStringList{currentFileUrl.toString()},
QString()
});
if (fm1Iface.lastError().isValid()) {
QDesktopServices::openUrl(folderUrl);
}
#else
QDesktopServices::openUrl(folderUrl);
#endif // Q_OS_WIN
}
void MainWindow::on_actionQuitApp_triggered()
{
quitAppAction(false);
}
void MainWindow::doCloseWindow()
{
#ifdef Q_OS_MAC
this->hide();
#else
this->close();
#endif
}
bool MainWindow::updateFileWatcher(const QString &basePath)
{
m_fileSystemWatcher->removePaths(m_fileSystemWatcher->files());
if (!basePath.isEmpty()) return m_fileSystemWatcher->addPath(basePath);
return false;
}
void MainWindow::updateGalleryButtonsVisibility()
{
const int galleryFileCount = m_pm->totalCount();
const bool loopGallery = Settings::instance()->loopGallery();
m_prevButton->setVisible(!m_protectedMode && galleryFileCount > 1);
m_nextButton->setVisible(!m_protectedMode && galleryFileCount > 1);
m_prevButton->setEnabled(loopGallery || !m_pm->isFirstIndex());
m_nextButton->setEnabled(loopGallery || !m_pm->isLastIndex());
}