* Added build-dependency libqt5svg5-dev * Bumped compat to 10 * Bumped minimum version debhelper (>= 10) * Bumped minimum version libfm-qt-dev (>= 0.11.1) * Bumped minimum version liblxqt0-dev (>= 0.11.0) * Bumped minimum version libqt5xdg-dev (>= 2.0.0) * Removed --parallel from rules, standard compat 10
951 lines
31 KiB
C++
951 lines
31 KiB
C++
/*
|
|
<one line to give the program's name and a brief idea of what it does.>
|
|
Copyright (C) 2013 - 2014 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
|
|
|
|
This program is free software; you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation; either version 2 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License along
|
|
with this program; if not, write to the Free Software Foundation, Inc.,
|
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
*/
|
|
|
|
|
|
#include "mainwindow.h"
|
|
#include <QMessageBox>
|
|
#include <QFileDialog>
|
|
#include <QImage>
|
|
#include <QImageReader>
|
|
#include <QImageWriter>
|
|
#include <QClipboard>
|
|
#include <QPainter>
|
|
#include <QPrintDialog>
|
|
#include <QPrinter>
|
|
#include <QWheelEvent>
|
|
#include <QMouseEvent>
|
|
#include <QTimer>
|
|
#include <QShortcut>
|
|
#include <QDockWidget>
|
|
#include <QScrollBar>
|
|
#include <QDesktopWidget>
|
|
#include <QGraphicsSvgItem>
|
|
#include "application.h"
|
|
#include <libfm-qt/path.h>
|
|
#include <libfm-qt/folderview.h>
|
|
#include <libfm-qt/filepropsdialog.h>
|
|
#include <libfm-qt/fileoperation.h>
|
|
|
|
using namespace LxImage;
|
|
|
|
MainWindow::MainWindow():
|
|
QMainWindow(),
|
|
contextMenu_(new QMenu(this)),
|
|
slideShowTimer_(nullptr),
|
|
image_(),
|
|
currentFile_(nullptr),
|
|
// currentFileInfo_(nullptr),
|
|
imageModified_(false),
|
|
folder_(nullptr),
|
|
folderPath_(nullptr),
|
|
folderModel_(new Fm::FolderModel()),
|
|
proxyModel_(new Fm::ProxyFolderModel()),
|
|
modelFilter_(new ModelFilter()),
|
|
thumbnailsDock_(nullptr),
|
|
thumbnailsView_(nullptr),
|
|
loadJob_(nullptr),
|
|
saveJob_(nullptr) {
|
|
|
|
setAttribute(Qt::WA_DeleteOnClose); // FIXME: check if current image is saved before close
|
|
|
|
Application* app = static_cast<Application*>(qApp);
|
|
app->addWindow();
|
|
|
|
Settings& settings = app->settings();
|
|
|
|
ui.setupUi(this);
|
|
connect(ui.actionScreenshot, &QAction::triggered, app, &Application::screenshot);
|
|
connect(ui.actionPreferences, &QAction::triggered, app , &Application::editPreferences);
|
|
|
|
proxyModel_->addFilter(modelFilter_);
|
|
proxyModel_->sort(Fm::FolderModel::ColumnFileName, Qt::AscendingOrder);
|
|
proxyModel_->setSourceModel(folderModel_);
|
|
|
|
// build context menu
|
|
ui.view->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
connect(ui.view, &QWidget::customContextMenuRequested, this, &MainWindow::onContextMenu);
|
|
|
|
// install an event filter on the image view
|
|
ui.view->installEventFilter(this);
|
|
ui.view->setBackgroundBrush(QBrush(settings.bgColor()));
|
|
|
|
if(settings.showThumbnails())
|
|
setShowThumbnails(true);
|
|
|
|
contextMenu_->addAction(ui.actionPrevious);
|
|
contextMenu_->addAction(ui.actionNext);
|
|
contextMenu_->addSeparator();
|
|
contextMenu_->addAction(ui.actionZoomOut);
|
|
contextMenu_->addAction(ui.actionZoomIn);
|
|
contextMenu_->addAction(ui.actionOriginalSize);
|
|
contextMenu_->addAction(ui.actionZoomFit);
|
|
contextMenu_->addSeparator();
|
|
contextMenu_->addAction(ui.actionSlideShow);
|
|
contextMenu_->addAction(ui.actionFullScreen);
|
|
contextMenu_->addSeparator();
|
|
contextMenu_->addAction(ui.actionRotateClockwise);
|
|
contextMenu_->addAction(ui.actionRotateCounterclockwise);
|
|
contextMenu_->addAction(ui.actionFlipHorizontal);
|
|
contextMenu_->addAction(ui.actionFlipVertical);
|
|
contextMenu_->addAction(ui.actionFlipVertical);
|
|
|
|
// create shortcuts
|
|
QShortcut* shortcut = new QShortcut(Qt::Key_Left, this);
|
|
connect(shortcut, &QShortcut::activated, this, &MainWindow::on_actionPrevious_triggered);
|
|
shortcut = new QShortcut(Qt::Key_Right, this);
|
|
connect(shortcut, &QShortcut::activated, this, &MainWindow::on_actionNext_triggered);
|
|
shortcut = new QShortcut(Qt::Key_Escape, this);
|
|
connect(shortcut, &QShortcut::activated, this, &MainWindow::onExitFullscreen);
|
|
}
|
|
|
|
MainWindow::~MainWindow() {
|
|
if(slideShowTimer_)
|
|
delete slideShowTimer_;
|
|
if(thumbnailsView_)
|
|
delete thumbnailsView_;
|
|
if(thumbnailsDock_)
|
|
delete thumbnailsDock_;
|
|
|
|
if(loadJob_) {
|
|
loadJob_->cancel();
|
|
// we don't need to do delete here. It will be done automatically
|
|
}
|
|
if(currentFile_)
|
|
fm_path_unref(currentFile_);
|
|
//if(currentFileInfo_)
|
|
// fm_file_info_unref(currentFileInfo_);
|
|
if(folder_) {
|
|
g_signal_handlers_disconnect_by_func(folder_, gpointer(_onFolderLoaded), this);
|
|
g_object_unref(folder_);
|
|
}
|
|
if(folderPath_)
|
|
fm_path_unref(folderPath_);
|
|
delete folderModel_;
|
|
delete proxyModel_;
|
|
delete modelFilter_;
|
|
|
|
Application* app = static_cast<Application*>(qApp);
|
|
app->removeWindow();
|
|
}
|
|
|
|
void MainWindow::on_actionAbout_triggered() {
|
|
QMessageBox::about(this, tr("About"),
|
|
tr("LXImage - a simple and fast image viewer\n\n"
|
|
"Copyright (C) 2013\n"
|
|
"LXDE Project: http://lxde.org/\n\n"
|
|
"Authors:\n"
|
|
"Hong Jen Yee (PCMan) <pcman.tw@gmail.com>"));
|
|
}
|
|
|
|
void MainWindow::on_actionOriginalSize_triggered() {
|
|
ui.view->setAutoZoomFit(false);
|
|
ui.view->zoomOriginal();
|
|
}
|
|
|
|
void MainWindow::on_actionZoomFit_triggered() {
|
|
ui.view->setAutoZoomFit(true);
|
|
ui.view->zoomFit();
|
|
}
|
|
|
|
void MainWindow::on_actionZoomIn_triggered() {
|
|
ui.view->setAutoZoomFit(false);
|
|
ui.view->zoomIn();
|
|
}
|
|
|
|
void MainWindow::on_actionZoomOut_triggered() {
|
|
ui.view->setAutoZoomFit(false);
|
|
ui.view->zoomOut();
|
|
}
|
|
|
|
void MainWindow::onFolderLoaded(FmFolder* folder) {
|
|
// if currently we're showing a file, get its index in the folder now
|
|
// since the folder is fully loaded.
|
|
if(currentFile_ && !currentIndex_.isValid()) {
|
|
currentIndex_ = indexFromPath(currentFile_);
|
|
if(thumbnailsView_) { // showing thumbnails
|
|
// select current file in the thumbnails view
|
|
thumbnailsView_->childView()->setCurrentIndex(currentIndex_);
|
|
thumbnailsView_->childView()->scrollTo(currentIndex_, QAbstractItemView::EnsureVisible);
|
|
}
|
|
}
|
|
}
|
|
|
|
void MainWindow::openImageFile(QString fileName) {
|
|
FmPath* path = fm_path_new_for_str(qPrintable(fileName));
|
|
if(currentFile_ && fm_path_equal(currentFile_, path)) {
|
|
// the same file! do not load it again
|
|
fm_path_unref(path);
|
|
return;
|
|
}
|
|
// load the image file asynchronously
|
|
loadImage(path);
|
|
loadFolder(fm_path_get_parent(path));
|
|
fm_path_unref(path);
|
|
}
|
|
|
|
// paste the specified image into the current view,
|
|
// reset the window, remove loaded folders, and
|
|
// invalidate current file name.
|
|
void MainWindow::pasteImage(QImage newImage) {
|
|
// cancel loading of current image
|
|
if(loadJob_) {
|
|
loadJob_->cancel(); // the job object will be freed automatically later
|
|
loadJob_ = nullptr;
|
|
}
|
|
setModified(true);
|
|
|
|
currentIndex_ = QModelIndex(); // invaludate current index since we don't have a folder model now
|
|
if(currentFile_)
|
|
fm_path_unref(currentFile_);
|
|
currentFile_ = nullptr;
|
|
|
|
image_ = newImage;
|
|
ui.view->setImage(image_);
|
|
ui.view->zoomOriginal();
|
|
|
|
updateUI();
|
|
}
|
|
|
|
// popup a file dialog and retrieve the selected image file name
|
|
QString MainWindow::openFileName() {
|
|
QString filterStr;
|
|
QList<QByteArray> formats = QImageReader::supportedImageFormats();
|
|
QList<QByteArray>::iterator it = formats.begin();
|
|
for(;;) {
|
|
filterStr += "*.";
|
|
filterStr += (*it).toLower();
|
|
++it;
|
|
if(it != formats.end())
|
|
filterStr += ' ';
|
|
else
|
|
break;
|
|
}
|
|
|
|
QString fileName = QFileDialog::getOpenFileName(
|
|
this, tr("Open File"), QString(),
|
|
tr("Image files (%1)").arg(filterStr));
|
|
return fileName;
|
|
}
|
|
|
|
// popup a file dialog and retrieve the selected image file name
|
|
QString MainWindow::saveFileName(QString defaultName) {
|
|
QString filterStr;
|
|
QList<QByteArray> formats = QImageWriter::supportedImageFormats();
|
|
QList<QByteArray>::iterator it = formats.begin();
|
|
for(;;) {
|
|
filterStr += "*.";
|
|
filterStr += (*it).toLower();
|
|
++it;
|
|
if(it != formats.end())
|
|
filterStr += ' ';
|
|
else
|
|
break;
|
|
}
|
|
// FIXME: should we generate better filter strings? one format per item?
|
|
|
|
QString fileName = QFileDialog::getSaveFileName(
|
|
this, tr("Save File"), defaultName, tr("Image files (%1)").arg(filterStr));
|
|
|
|
// use png format by default if the extension is not set
|
|
if(!fileName.isEmpty() && fileName.indexOf('.') == -1)
|
|
fileName += ".png";
|
|
return fileName;
|
|
}
|
|
|
|
void MainWindow::on_actionOpenFile_triggered() {
|
|
QString fileName = openFileName();
|
|
if(!fileName.isEmpty()) {
|
|
openImageFile(fileName);
|
|
}
|
|
}
|
|
|
|
void MainWindow::on_actionNewWindow_triggered() {
|
|
Application* app = static_cast<Application*>(qApp);
|
|
MainWindow* window = new MainWindow();
|
|
window->resize(app->settings().windowWidth(), app->settings().windowHeight());
|
|
|
|
if(app->settings().windowMaximized())
|
|
window->setWindowState(window->windowState() | Qt::WindowMaximized);
|
|
|
|
window->show();
|
|
}
|
|
|
|
void MainWindow::on_actionSave_triggered() {
|
|
if(saveJob_) // if we're currently saving another file
|
|
return;
|
|
|
|
if(!image_.isNull()) {
|
|
if(currentFile_)
|
|
saveImage(currentFile_);
|
|
else
|
|
on_actionSaveAs_triggered();
|
|
}
|
|
}
|
|
|
|
void MainWindow::on_actionSaveAs_triggered() {
|
|
if(saveJob_) // if we're currently saving another file
|
|
return;
|
|
QString baseName;
|
|
if(currentFile_) {
|
|
char* dispName = fm_path_display_name(currentFile_, false);
|
|
baseName = QString::fromUtf8(dispName);
|
|
g_free(dispName);
|
|
}
|
|
QString fileName = saveFileName(baseName);
|
|
if(!fileName.isEmpty()) {
|
|
FmPath* path = fm_path_new_for_str(qPrintable(fileName));
|
|
// save the image file asynchronously
|
|
saveImage(path);
|
|
|
|
if(!currentFile_) { // if we haven't load any file yet
|
|
currentFile_ = path;
|
|
loadFolder(fm_path_get_parent(path));
|
|
}
|
|
else
|
|
fm_path_unref(path);
|
|
}
|
|
}
|
|
|
|
void MainWindow::on_actionDelete_triggered() {
|
|
// delete the current file
|
|
if(currentFile_) {
|
|
FmPathList* paths = fm_path_list_new();
|
|
fm_path_list_push_tail(paths, currentFile_);
|
|
Fm::FileOperation::deleteFiles(paths);
|
|
fm_path_list_unref(paths);
|
|
}
|
|
}
|
|
|
|
void MainWindow::on_actionFileProperties_triggered() {
|
|
if(currentIndex_.isValid()) {
|
|
FmFileInfo* file = proxyModel_->fileInfoFromIndex(currentIndex_);
|
|
// it's better to use an async job to query the file info since it's
|
|
// possible that loading of the folder is not finished and the file info is
|
|
// not available yet, but it's overkill for a rarely used function.
|
|
if(file)
|
|
Fm::FilePropsDialog::showForFile(file);
|
|
}
|
|
}
|
|
|
|
void MainWindow::on_actionClose_triggered() {
|
|
deleteLater();
|
|
}
|
|
|
|
void MainWindow::on_actionNext_triggered() {
|
|
if(proxyModel_->rowCount() <= 1)
|
|
return;
|
|
if(currentIndex_.isValid()) {
|
|
QModelIndex index;
|
|
if(currentIndex_.row() < proxyModel_->rowCount() - 1)
|
|
index = proxyModel_->index(currentIndex_.row() + 1, 0);
|
|
else
|
|
index = proxyModel_->index(0, 0);
|
|
FmFileInfo* info = proxyModel_->fileInfoFromIndex(index);
|
|
if(info) {
|
|
FmPath* path = fm_file_info_get_path(info);
|
|
loadImage(path, index);
|
|
}
|
|
}
|
|
}
|
|
|
|
void MainWindow::on_actionPrevious_triggered() {
|
|
if(proxyModel_->rowCount() <= 1)
|
|
return;
|
|
if(currentIndex_.isValid()) {
|
|
QModelIndex index;
|
|
if(currentIndex_.row() > 0)
|
|
index = proxyModel_->index(currentIndex_.row() - 1, 0);
|
|
else
|
|
index = proxyModel_->index(proxyModel_->rowCount() - 1, 0);
|
|
FmFileInfo* info = proxyModel_->fileInfoFromIndex(index);
|
|
if(info) {
|
|
FmPath* path = fm_file_info_get_path(info);
|
|
loadImage(path, index);
|
|
}
|
|
}
|
|
}
|
|
|
|
void MainWindow::on_actionFirst_triggered() {
|
|
QModelIndex index = proxyModel_->index(0, 0);
|
|
if(index.isValid()) {
|
|
FmFileInfo* info = proxyModel_->fileInfoFromIndex(index);
|
|
if(info) {
|
|
FmPath* path = fm_file_info_get_path(info);
|
|
loadImage(path, index);
|
|
}
|
|
}
|
|
}
|
|
|
|
void MainWindow::on_actionLast_triggered() {
|
|
QModelIndex index = proxyModel_->index(proxyModel_->rowCount() - 1, 0);
|
|
if(index.isValid()) {
|
|
FmFileInfo* info = proxyModel_->fileInfoFromIndex(index);
|
|
if(info) {
|
|
FmPath* path = fm_file_info_get_path(info);;
|
|
loadImage(path, index);
|
|
}
|
|
}
|
|
}
|
|
|
|
void MainWindow::loadFolder(FmPath* newFolderPath) {
|
|
if(folder_) { // an folder is already loaded
|
|
if(fm_path_equal(newFolderPath, folderPath_)) // same folder, ignore
|
|
return;
|
|
// free current folder
|
|
g_signal_handlers_disconnect_by_func(folder_, gpointer(_onFolderLoaded), this);
|
|
g_object_unref(folder_);
|
|
fm_path_unref(folderPath_);
|
|
}
|
|
|
|
folderPath_ = fm_path_ref(newFolderPath);
|
|
folder_ = fm_folder_from_path(newFolderPath);
|
|
g_signal_connect(folder_, "finish-loading", G_CALLBACK(_onFolderLoaded), this);
|
|
|
|
folderModel_->setFolder(folder_);
|
|
currentIndex_ = QModelIndex(); // set current index to invalid
|
|
}
|
|
|
|
// the image is loaded (the method is only called if the loading is not cancelled)
|
|
void MainWindow::onImageLoaded(LoadImageJob* job) {
|
|
loadJob_ = nullptr; // the job object will be freed later automatically
|
|
|
|
image_ = job->image();
|
|
ui.view->setAutoZoomFit(true);
|
|
ui.view->setImage(image_);
|
|
|
|
if(!currentIndex_.isValid())
|
|
currentIndex_ = indexFromPath(currentFile_);
|
|
|
|
updateUI();
|
|
|
|
if(job->error()) {
|
|
// if there are errors
|
|
// TODO: show a info bar?
|
|
}
|
|
|
|
/* we resized and moved the window without showing
|
|
it in updateUI(), so we need to show it here */
|
|
show();
|
|
}
|
|
|
|
void MainWindow::onImageSaved(SaveImageJob* job) {
|
|
if(!job->error()) {
|
|
setModified(false);
|
|
}
|
|
saveJob_ = nullptr;
|
|
}
|
|
|
|
// filter events of other objects, mainly the image view.
|
|
bool MainWindow::eventFilter(QObject* watched, QEvent* event) {
|
|
if(watched == ui.view) { // we got an event for the image view
|
|
switch(event->type()) {
|
|
case QEvent::Wheel: { // mouse wheel event
|
|
QWheelEvent* wheelEvent = static_cast<QWheelEvent*>(event);
|
|
if(wheelEvent->modifiers() == 0) {
|
|
int delta = wheelEvent->delta();
|
|
if(delta < 0)
|
|
on_actionNext_triggered(); // next image
|
|
else
|
|
on_actionPrevious_triggered(); // previous image
|
|
}
|
|
break;
|
|
}
|
|
case QEvent::MouseButtonDblClick: {
|
|
QMouseEvent* mouseEvent = static_cast<QMouseEvent*>(event);
|
|
if(mouseEvent->button() == Qt::LeftButton)
|
|
ui.actionFullScreen->trigger();
|
|
break;
|
|
}
|
|
default:;
|
|
}
|
|
}
|
|
else if(thumbnailsView_ && watched == thumbnailsView_->childView()) {
|
|
// scroll the thumbnail view with mouse wheel
|
|
switch(event->type()) {
|
|
case QEvent::Wheel: { // mouse wheel event
|
|
QWheelEvent* wheelEvent = static_cast<QWheelEvent*>(event);
|
|
if(wheelEvent->modifiers() == 0) {
|
|
int delta = wheelEvent->delta();
|
|
QScrollBar* hscroll = thumbnailsView_->childView()->horizontalScrollBar();
|
|
if(hscroll)
|
|
hscroll->setValue(hscroll->value() - delta);
|
|
return true;
|
|
}
|
|
break;
|
|
}
|
|
default:;
|
|
}
|
|
}
|
|
return QObject::eventFilter(watched, event);
|
|
}
|
|
|
|
QModelIndex MainWindow::indexFromPath(FmPath* filePath) {
|
|
// if the folder is already loaded, figure out our index
|
|
// otherwise, it will be done again in onFolderLoaded() when the folder is fully loaded.
|
|
if(folder_ && fm_folder_is_loaded(folder_)) {
|
|
QModelIndex index;
|
|
int count = proxyModel_->rowCount();
|
|
for(int row = 0; row < count; ++row) {
|
|
index = proxyModel_->index(row, 0);
|
|
FmFileInfo* info = proxyModel_->fileInfoFromIndex(index);
|
|
if(info && fm_path_equal(filePath, fm_file_info_get_path(info))) {
|
|
return index;
|
|
}
|
|
}
|
|
}
|
|
return QModelIndex();
|
|
}
|
|
|
|
|
|
void MainWindow::updateUI() {
|
|
if(currentIndex_.isValid()) {
|
|
if(thumbnailsView_) { // showing thumbnails
|
|
// select current file in the thumbnails view
|
|
thumbnailsView_->childView()->setCurrentIndex(currentIndex_);
|
|
thumbnailsView_->childView()->scrollTo(currentIndex_, QAbstractItemView::EnsureVisible);
|
|
}
|
|
}
|
|
|
|
QString title;
|
|
if(currentFile_) {
|
|
char* dispName = fm_path_display_basename(currentFile_);
|
|
if(loadJob_) { // if loading is in progress
|
|
title = tr("[*]%1 (Loading...) - Image Viewer")
|
|
.arg(QString::fromUtf8(dispName));
|
|
}
|
|
else {
|
|
if(image_.isNull()) {
|
|
title = tr("[*]%1 (Failed to Load) - Image Viewer")
|
|
.arg(QString::fromUtf8(dispName));
|
|
}
|
|
else {
|
|
title = tr("[*]%1 (%2x%3) - Image Viewer")
|
|
.arg(QString::fromUtf8(dispName))
|
|
.arg(image_.width())
|
|
.arg(image_.height());
|
|
/* Here we try to implement the following behavior as far as possible:
|
|
(1) A minimum size of 400x400 is assumed;
|
|
(2) The window is scaled to fit the image;
|
|
(3) But for too big images, the window is scaled down;
|
|
(4) The window is centered on the screen. */
|
|
if (!isVisible()) {
|
|
/* To have a correct position, we should move the window BEFORE
|
|
it's shown but we also need to know the dimensions of its view.
|
|
Therefore, we use show() without really showing the window. */
|
|
setAttribute(Qt::WA_DontShowOnScreen);
|
|
show();
|
|
int scrollThickness = style()->pixelMetric(QStyle::PM_ScrollBarExtent);
|
|
QSize newSize = size() + image_.size() - ui.view->size() + QSize(scrollThickness, scrollThickness);
|
|
QRect ag = QApplication::desktop()->availableGeometry();
|
|
// since the window isn't decorated yet, we have to assume a max thickness for its frame
|
|
QSize maxFrame = QSize(50, 100);
|
|
if (newSize.width() > ag.width() - maxFrame.width() || newSize.height() > ag.height() - maxFrame.height())
|
|
newSize.scale (ag.width() - maxFrame.width(), ag.height() - maxFrame.height(), Qt::KeepAspectRatio);
|
|
// a minimum size of 400x400 is good
|
|
if (newSize.width() < 400) newSize.rwidth() = 400;
|
|
if (newSize.height() < 400 ) newSize.rheight() = 400;
|
|
move (ag.x() + (ag.width() - newSize.width())/2,
|
|
ag.y() + (ag.height() - newSize.height())/2);
|
|
resize(newSize);
|
|
hide(); // hide it to show it again later, at onImageLoaded()
|
|
setAttribute(Qt::WA_DontShowOnScreen, false);
|
|
}
|
|
}
|
|
}
|
|
g_free(dispName);
|
|
// TODO: update status bar, show current index in the folder
|
|
}
|
|
else {
|
|
title = tr("[*]Image Viewer");
|
|
}
|
|
setWindowTitle(title);
|
|
setWindowModified(imageModified_);
|
|
}
|
|
|
|
// Load the specified image file asynchronously in a worker thread.
|
|
// When the loading is finished, onImageLoaded() will be called.
|
|
void MainWindow::loadImage(FmPath* filePath, QModelIndex index) {
|
|
// cancel loading of current image
|
|
if(loadJob_) {
|
|
loadJob_->cancel(); // the job object will be freed automatically later
|
|
loadJob_ = nullptr;
|
|
}
|
|
if(imageModified_) {
|
|
// TODO: ask the user to save the modified image?
|
|
// this should be made optional
|
|
setModified(false);
|
|
}
|
|
|
|
currentIndex_ = index;
|
|
if(currentFile_)
|
|
fm_path_unref(currentFile_);
|
|
currentFile_ = fm_path_ref(filePath);
|
|
// clear current image, but do not update the view now to prevent flickers
|
|
image_ = QImage();
|
|
|
|
const char* basename = fm_path_get_basename(currentFile_);
|
|
char* mime_type = g_content_type_guess(basename, NULL, 0, NULL);
|
|
QString mimeType;
|
|
if (mime_type) {
|
|
mimeType = QString(mime_type);
|
|
g_free(mime_type);
|
|
}
|
|
if(mimeType == "image/gif"
|
|
|| mimeType == "image/svg+xml" || mimeType == "image/svg+xml-compressed") {
|
|
char *file_name = fm_path_to_str(currentFile_);
|
|
QString fileName(file_name);
|
|
g_free(file_name);
|
|
ui.view->setAutoZoomFit(true); // like in onImageLoaded()
|
|
if(mimeType == "image/gif")
|
|
ui.view->setGifAnimation(fileName);
|
|
else
|
|
ui.view->setSVG(fileName);
|
|
image_ = ui.view->image();
|
|
updateUI();
|
|
show();
|
|
}
|
|
else {
|
|
// start a new gio job to load the specified image
|
|
loadJob_ = new LoadImageJob(this, filePath);
|
|
loadJob_->start();
|
|
|
|
updateUI();
|
|
}
|
|
}
|
|
|
|
void MainWindow::saveImage(FmPath* filePath) {
|
|
if(saveJob_) // do not launch a new job if the current one is still in progress
|
|
return;
|
|
// start a new gio job to save current image to the specified path
|
|
saveJob_ = new SaveImageJob(this, filePath);
|
|
saveJob_->start();
|
|
// FIXME: add a cancel button to the UI? update status bar?
|
|
}
|
|
|
|
QGraphicsItem* MainWindow::getGraphicsItem() {
|
|
if(!ui.view->items().isEmpty())
|
|
return ui.view->items().at(0);
|
|
return nullptr;
|
|
}
|
|
|
|
void MainWindow::on_actionRotateClockwise_triggered() {
|
|
QGraphicsItem *graphItem = getGraphicsItem();
|
|
if(!image_.isNull()) {
|
|
QTransform transform;
|
|
transform.rotate(90.0);
|
|
image_ = image_.transformed(transform, Qt::SmoothTransformation);
|
|
/* when this is GIF or SVG, we need to rotate its corresponding QImage
|
|
without showing it to have the right measure for auto-zooming */
|
|
ui.view->setImage(image_, graphItem->isWidget() // we have gif animation
|
|
|| static_cast<QGraphicsSvgItem*>(graphItem) // an SVG image
|
|
? false : true);
|
|
setModified(true);
|
|
}
|
|
|
|
if(graphItem) {
|
|
QTransform transform;
|
|
transform.translate(graphItem->sceneBoundingRect().height(), 0);
|
|
transform.rotate(90);
|
|
// we need to apply transformations in the reverse order
|
|
QTransform prevTrans = graphItem->transform();
|
|
graphItem->setTransform(transform, false);
|
|
graphItem->setTransform(prevTrans, true);
|
|
}
|
|
}
|
|
|
|
void MainWindow::on_actionRotateCounterclockwise_triggered() {
|
|
QGraphicsItem *graphItem = getGraphicsItem();
|
|
if(!image_.isNull()) {
|
|
QTransform transform;
|
|
transform.rotate(-90.0);
|
|
image_ = image_.transformed(transform, Qt::SmoothTransformation);
|
|
ui.view->setImage(image_, graphItem->isWidget() || static_cast<QGraphicsSvgItem*>(graphItem)
|
|
? false : true);
|
|
setModified(true);
|
|
}
|
|
|
|
if(graphItem) {
|
|
QTransform transform;
|
|
transform.translate(0, graphItem->sceneBoundingRect().width());
|
|
transform.rotate(-90);
|
|
QTransform prevTrans = graphItem->transform();
|
|
graphItem->setTransform(transform, false);
|
|
graphItem->setTransform(prevTrans, true);
|
|
}
|
|
}
|
|
|
|
void MainWindow::on_actionCopy_triggered() {
|
|
QClipboard *clipboard = QApplication::clipboard();
|
|
QImage copiedImage = image_;
|
|
// FIXME: should we copy the currently scaled result instead of the original image?
|
|
/*
|
|
double factor = ui.view->scaleFactor();
|
|
if(factor == 1.0)
|
|
copiedImage = image_;
|
|
else
|
|
copiedImage = image_.scaled();
|
|
*/
|
|
clipboard->setImage(copiedImage);
|
|
}
|
|
|
|
void MainWindow::on_actionPaste_triggered() {
|
|
QClipboard *clipboard = QApplication::clipboard();
|
|
QImage image = clipboard->image();
|
|
if(!image.isNull()) {
|
|
pasteImage(image);
|
|
}
|
|
}
|
|
|
|
void MainWindow::on_actionFlipVertical_triggered() {
|
|
bool hasQGraphicsItem(false);
|
|
if(QGraphicsItem *graphItem = getGraphicsItem()) {
|
|
hasQGraphicsItem = true;
|
|
QTransform transform;
|
|
transform.scale(1, -1);
|
|
transform.translate(0, -graphItem->sceneBoundingRect().height());
|
|
QTransform prevTrans = graphItem->transform();
|
|
graphItem->setTransform(transform, false);
|
|
graphItem->setTransform(prevTrans, true);
|
|
}
|
|
if(!image_.isNull()) {
|
|
image_ = image_.mirrored(false, true);
|
|
ui.view->setImage(image_, !hasQGraphicsItem);
|
|
setModified(true);
|
|
}
|
|
}
|
|
|
|
void MainWindow::on_actionFlipHorizontal_triggered() {
|
|
bool hasQGraphicsItem(false);
|
|
if(QGraphicsItem *graphItem = getGraphicsItem()) {
|
|
hasQGraphicsItem = true;
|
|
QTransform transform;
|
|
transform.scale(-1, 1);
|
|
transform.translate(-graphItem->sceneBoundingRect().width(), 0);
|
|
QTransform prevTrans = graphItem->transform();
|
|
graphItem->setTransform(transform, false);
|
|
graphItem->setTransform(prevTrans, true);
|
|
}
|
|
if(!image_.isNull()) {
|
|
image_ = image_.mirrored(true, true);
|
|
ui.view->setImage(image_, !hasQGraphicsItem);
|
|
setModified(true);
|
|
}
|
|
}
|
|
|
|
void MainWindow::setModified(bool modified) {
|
|
imageModified_ = modified;
|
|
updateUI();
|
|
}
|
|
|
|
void MainWindow::applySettings() {
|
|
Application* app = static_cast<Application*>(qApp);
|
|
Settings& settings = app->settings();
|
|
if(isFullScreen())
|
|
ui.view->setBackgroundBrush(QBrush(settings.fullScreenBgColor()));
|
|
else
|
|
ui.view->setBackgroundBrush(QBrush(settings.bgColor()));
|
|
}
|
|
|
|
void MainWindow::on_actionPrint_triggered() {
|
|
// QPrinter printer(QPrinter::HighResolution);
|
|
QPrinter printer;
|
|
QPrintDialog dlg(&printer);
|
|
if(dlg.exec() == QDialog::Accepted) {
|
|
QPainter painter;
|
|
painter.begin(&printer);
|
|
QRect pageRect = printer.pageRect();
|
|
int cols = (image_.width() / pageRect.width()) + (image_.width() % pageRect.width() ? 1 : 0);
|
|
int rows = (image_.height() / pageRect.height()) + (image_.height() % pageRect.height() ? 1 : 0);
|
|
for(int row = 0; row < rows; ++row) {
|
|
for(int col = 0; col < cols; ++col) {
|
|
QRect srcRect(pageRect.width() * col, pageRect.height() * row, pageRect.width(), pageRect.height());
|
|
painter.drawImage(QPoint(0, 0), image_, srcRect);
|
|
if(col + 1 == cols && row + 1 == rows) // this is the last page
|
|
break;
|
|
printer.newPage();
|
|
}
|
|
}
|
|
painter.end();
|
|
}
|
|
}
|
|
|
|
// TODO: This can later be used for doing slide show
|
|
void MainWindow::on_actionFullScreen_triggered(bool checked) {
|
|
if(checked)
|
|
showFullScreen();
|
|
else
|
|
showNormal();
|
|
}
|
|
|
|
void MainWindow::on_actionSlideShow_triggered(bool checked) {
|
|
if(checked) {
|
|
if(!slideShowTimer_) {
|
|
slideShowTimer_ = new QTimer();
|
|
// switch to the next image when timeout
|
|
connect(slideShowTimer_, &QTimer::timeout, this, &MainWindow::on_actionNext_triggered);
|
|
}
|
|
Application* app = static_cast<Application*>(qApp);
|
|
slideShowTimer_->start(app->settings().slideShowInterval() * 1000);
|
|
// showFullScreen();
|
|
ui.actionSlideShow->setIcon(QIcon::fromTheme("media-playback-stop"));
|
|
}
|
|
else {
|
|
if(slideShowTimer_) {
|
|
delete slideShowTimer_;
|
|
slideShowTimer_ = nullptr;
|
|
ui.actionSlideShow->setIcon(QIcon::fromTheme("media-playback-start"));
|
|
}
|
|
}
|
|
}
|
|
|
|
void MainWindow::setShowThumbnails(bool show) {
|
|
if(show) {
|
|
if(!thumbnailsDock_) {
|
|
thumbnailsDock_ = new QDockWidget(this);
|
|
thumbnailsDock_->setFeatures(QDockWidget::NoDockWidgetFeatures); // FIXME: should use DockWidgetClosable
|
|
thumbnailsDock_->setWindowTitle(tr("Thumbnails"));
|
|
thumbnailsView_ = new Fm::FolderView(Fm::FolderView::IconMode);
|
|
thumbnailsDock_->setWidget(thumbnailsView_);
|
|
addDockWidget(Qt::BottomDockWidgetArea, thumbnailsDock_);
|
|
QListView* listView = static_cast<QListView*>(thumbnailsView_->childView());
|
|
listView->setFlow(QListView::TopToBottom);
|
|
listView->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
|
listView->setSelectionMode(QAbstractItemView::SingleSelection);
|
|
listView->installEventFilter(this);
|
|
// FIXME: optimize the size of the thumbnail view
|
|
// FIXME if the thumbnail view is docked elsewhere, update the settings.
|
|
int scrollHeight = style()->pixelMetric(QStyle::PM_ScrollBarExtent);
|
|
thumbnailsView_->setFixedHeight(listView->gridSize().height() + scrollHeight);
|
|
thumbnailsView_->setModel(proxyModel_);
|
|
proxyModel_->setShowThumbnails(true);
|
|
if (currentFile_) { // select the loaded image
|
|
currentIndex_ = indexFromPath(currentFile_);
|
|
listView->setCurrentIndex(currentIndex_);
|
|
// wait to center the selection
|
|
QCoreApplication::processEvents();
|
|
listView->scrollTo(currentIndex_, QAbstractItemView::PositionAtCenter);
|
|
}
|
|
connect(thumbnailsView_->selectionModel(), &QItemSelectionModel::selectionChanged,
|
|
this, &MainWindow::onThumbnailSelChanged);
|
|
}
|
|
}
|
|
else {
|
|
if(thumbnailsDock_) {
|
|
delete thumbnailsView_;
|
|
thumbnailsView_ = nullptr;
|
|
delete thumbnailsDock_;
|
|
thumbnailsDock_ = nullptr;
|
|
}
|
|
proxyModel_->setShowThumbnails(false);
|
|
}
|
|
}
|
|
|
|
void MainWindow::on_actionShowThumbnails_triggered(bool checked) {
|
|
setShowThumbnails(checked);
|
|
}
|
|
|
|
void MainWindow::changeEvent(QEvent* event) {
|
|
// TODO: hide menu/toolbars in full screen mode and make the background black.
|
|
if(event->type() == QEvent::WindowStateChange) {
|
|
Application* app = static_cast<Application*>(qApp);
|
|
if(isFullScreen()) { // changed to fullscreen mode
|
|
ui.view->setFrameStyle(QFrame::NoFrame);
|
|
ui.view->setBackgroundBrush(QBrush(app->settings().fullScreenBgColor()));
|
|
ui.toolBar->hide();
|
|
ui.statusBar->hide();
|
|
if(thumbnailsDock_)
|
|
thumbnailsDock_->hide();
|
|
// NOTE: in fullscreen mode, all shortcut keys in the menu are disabled since the menu
|
|
// is disabled. We needs to add the actions to the main window manually to enable the
|
|
// shortcuts again.
|
|
ui.menubar->hide();
|
|
Q_FOREACH(QAction* action, ui.menubar->actions()) {
|
|
if(!action->shortcut().isEmpty())
|
|
addAction(action);
|
|
}
|
|
addActions(ui.menubar->actions());
|
|
ui.view->hideCursor(true);
|
|
}
|
|
else { // restore to normal window mode
|
|
ui.view->setFrameStyle(QFrame::StyledPanel|QFrame::Sunken);
|
|
ui.view->setBackgroundBrush(QBrush(app->settings().bgColor()));
|
|
// now we're going to re-enable the menu, so remove the actions previously added.
|
|
Q_FOREACH(QAction* action, ui.menubar->actions()) {
|
|
if(!action->shortcut().isEmpty())
|
|
removeAction(action);
|
|
}
|
|
ui.menubar->show();
|
|
ui.toolBar->show();
|
|
ui.statusBar->show();
|
|
if(thumbnailsDock_)
|
|
thumbnailsDock_->show();
|
|
ui.view->hideCursor(false);
|
|
}
|
|
}
|
|
QWidget::changeEvent(event);
|
|
}
|
|
|
|
void MainWindow::resizeEvent(QResizeEvent *event) {
|
|
QMainWindow::resizeEvent(event);
|
|
Settings& settings = static_cast<Application*>(qApp)->settings();
|
|
if(settings.rememberWindowSize()) {
|
|
settings.setLastWindowMaximized(isMaximized());
|
|
|
|
if(!isMaximized()) {
|
|
settings.setLastWindowWidth(width());
|
|
settings.setLastWindowHeight(height());
|
|
}
|
|
}
|
|
}
|
|
|
|
void MainWindow::closeEvent(QCloseEvent *event)
|
|
{
|
|
QWidget::closeEvent(event);
|
|
Settings& settings = static_cast<Application*>(qApp)->settings();
|
|
if(settings.rememberWindowSize()) {
|
|
settings.setLastWindowMaximized(isMaximized());
|
|
|
|
if(!isMaximized()) {
|
|
settings.setLastWindowWidth(width());
|
|
settings.setLastWindowHeight(height());
|
|
}
|
|
}
|
|
}
|
|
|
|
void MainWindow::onContextMenu(QPoint pos) {
|
|
contextMenu_->exec(ui.view->mapToGlobal(pos));
|
|
}
|
|
|
|
void MainWindow::onExitFullscreen() {
|
|
if(isFullScreen())
|
|
showNormal();
|
|
}
|
|
|
|
void MainWindow::onThumbnailSelChanged(const QItemSelection& selected, const QItemSelection& deselected) {
|
|
// the selected item of thumbnail view is changed
|
|
if(!selected.isEmpty()) {
|
|
QModelIndex index = selected.first().topLeft();
|
|
if(index.isValid() && index != currentIndex_) {
|
|
FmFileInfo* file = proxyModel_->fileInfoFromIndex(index);
|
|
if(file)
|
|
loadImage(fm_file_info_get_path(file), index);
|
|
}
|
|
}
|
|
}
|