1 /*
2  * LXImage-Qt - a simple and fast image viewer
3  * Copyright (C) 2013  PCMan <pcman.tw@gmail.com>
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License along
16  * with this program; if not, write to the Free Software Foundation, Inc.,
17  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18  *
19  */
20 
21 #include "mainwindow.h"
22 #include <QActionGroup>
23 #include <QDir>
24 #include <QFileInfo>
25 #include <QMessageBox>
26 #include <QFileDialog>
27 #include <QImage>
28 #include <QImageReader>
29 #include <QImageWriter>
30 #include <QClipboard>
31 #include <QPainter>
32 #include <QPrintDialog>
33 #include <QPrinter>
34 #include <QWheelEvent>
35 #include <QMouseEvent>
36 #include <QTimer>
37 #include <QScreen>
38 #include <QWindow>
39 #include <QDockWidget>
40 #include <QScrollBar>
41 #include <QHeaderView>
42 #include <QStandardPaths>
43 #include <QDateTime>
44 #include <QX11Info>
45 
46 #include "application.h"
47 #include <libfm-qt/folderview.h>
48 #include <libfm-qt/filepropsdialog.h>
49 #include <libfm-qt/fileoperation.h>
50 #include <libfm-qt/folderitemdelegate.h>
51 #include <libfm-qt/utilities.h>
52 
53 #include "mrumenu.h"
54 #include "resizeimagedialog.h"
55 #include "upload/uploaddialog.h"
56 #include "ui_shortcuts.h"
57 
58 using namespace LxImage;
59 
MainWindow()60 MainWindow::MainWindow():
61   QMainWindow(),
62   contextMenu_(new QMenu(this)),
63   slideShowTimer_(nullptr),
64   image_(),
65   // currentFileInfo_(nullptr),
66   imageModified_(false),
67   startMaximized_(false),
68   folder_(nullptr),
69   folderModel_(new Fm::FolderModel()),
70   proxyModel_(new Fm::ProxyFolderModel()),
71   modelFilter_(new ModelFilter()),
72   thumbnailsDock_(nullptr),
73   thumbnailsView_(nullptr),
74   loadJob_(nullptr),
75   saveJob_(nullptr),
76   fileMenu_(nullptr),
77   showFullScreen_(false) {
78 
79   setAttribute(Qt::WA_DeleteOnClose); // FIXME: check if current image is saved before close
80 
81   Application* app = static_cast<Application*>(qApp);
82   app->addWindow();
83 
84   Settings& settings = app->settings();
85 
86   ui.setupUi(this);
87   if(QX11Info::isPlatformX11()) {
88     connect(ui.actionScreenshot, &QAction::triggered, app, &Application::screenshot);
89   }
90   else {
91     ui.actionScreenshot->setVisible(false);
92   }
93   connect(ui.actionPreferences, &QAction::triggered, app , &Application::editPreferences);
94 
95   proxyModel_->addFilter(modelFilter_);
96   proxyModel_->sort(Fm::FolderModel::ColumnFileName, Qt::AscendingOrder);
97   proxyModel_->setSourceModel(folderModel_);
98 
99   ui.view->setSmoothOnZoom(settings.smoothOnZoom());
100 
101   // build context menu
102   ui.view->setContextMenuPolicy(Qt::CustomContextMenu);
103   connect(ui.view, &QWidget::customContextMenuRequested, this, &MainWindow::onContextMenu);
104 
105   connect(ui.view, &ImageView::fileDropped, this, &MainWindow::onFileDropped);
106 
107   connect(ui.view, &ImageView::zooming, this, &MainWindow::onZooming);
108 
109   // install an event filter on the image view
110   ui.view->installEventFilter(this);
111 
112   ui.view->setBackgroundBrush(QBrush(settings.bgColor()));
113 
114   ui.view->updateOutline();
115 
116   ui.view->showOutline(settings.isOutlineShown());
117   ui.actionShowOutline->setChecked(settings.isOutlineShown());
118 
119   setShowExifData(settings.showExifData());
120   ui.actionShowExifData->setChecked(settings.showExifData());
121 
122   setShowThumbnails(settings.showThumbnails());
123   ui.actionShowThumbnails->setChecked(settings.showThumbnails());
124 
125   addAction(ui.actionMenubar);
126   on_actionMenubar_triggered(settings.isMenubarShown());
127   ui.actionMenubar->setChecked(settings.isMenubarShown());
128 
129   ui.toolBar->setVisible(settings.isToolbarShown());
130   ui.actionToolbar->setChecked(settings.isToolbarShown());
131   connect(ui.actionToolbar, &QAction::triggered, this, [this](int checked) {
132     if(!isFullScreen()) { // toolbar is hidden in fullscreen
133       ui.toolBar->setVisible(checked);
134     }
135   });
136   // toolbar visibility can change in its context menu
137   connect(ui.toolBar, &QToolBar::visibilityChanged, this, [this](int visible) {
138     if(!isFullScreen()) { // toolbar is hidden in fullscreen
139       ui.actionToolbar->setChecked(visible);
140     }
141   });
142 
143   ui.annotationsToolBar->setVisible(settings.isAnnotationsToolbarShown());
144   ui.actionAnnotations->setChecked(settings.isAnnotationsToolbarShown());
145   connect(ui.actionAnnotations, &QAction::triggered, this, [this](int checked) {
146     if(!isFullScreen()) { // annotations toolbar is hidden in fullscreen
147       ui.annotationsToolBar->setVisible(checked);
148     }
149   });
150   // annotations toolbar visibility can change in its context menu
151   connect(ui.annotationsToolBar, &QToolBar::visibilityChanged, this, [this](int visible) {
152     if(!isFullScreen()) { // annotations toolbar is hidden in fullscreen
153       ui.actionAnnotations->setChecked(visible);
154     }
155   });
156 
157   auto aGroup = new QActionGroup(this);
158   ui.actionZoomFit->setActionGroup(aGroup);
159   ui.actionOriginalSize->setActionGroup(aGroup);
160   ui.actionZoomFit->setChecked(true);
161 
162   contextMenu_->addAction(ui.actionPrevious);
163   contextMenu_->addAction(ui.actionNext);
164   contextMenu_->addSeparator();
165   contextMenu_->addAction(ui.actionZoomOut);
166   contextMenu_->addAction(ui.actionZoomIn);
167   contextMenu_->addAction(ui.actionOriginalSize);
168   contextMenu_->addAction(ui.actionZoomFit);
169   contextMenu_->addSeparator();
170   contextMenu_->addAction(ui.actionSlideShow);
171   contextMenu_->addAction(ui.actionFullScreen);
172   contextMenu_->addAction(ui.actionShowOutline);
173   contextMenu_->addAction(ui.actionShowExifData);
174   contextMenu_->addAction(ui.actionShowThumbnails);
175   contextMenu_->addSeparator();
176   contextMenu_->addAction(ui.actionMenubar);
177   contextMenu_->addAction(ui.actionToolbar);
178   contextMenu_->addAction(ui.actionAnnotations);
179   contextMenu_->addSeparator();
180   contextMenu_->addAction(ui.actionRotateClockwise);
181   contextMenu_->addAction(ui.actionRotateCounterclockwise);
182   contextMenu_->addAction(ui.actionFlipHorizontal);
183   contextMenu_->addAction(ui.actionFlipVertical);
184 
185   // Open images when MRU items are clicked
186   ui.menuRecently_Opened_Files->setMaxItems(settings.maxRecentFiles());
187   connect(ui.menuRecently_Opened_Files, &MruMenu::itemClicked, this, &MainWindow::onFileDropped);
188 
189   // Create an action group for the annotation tools
190   QActionGroup *annotationGroup = new QActionGroup(this);
191   annotationGroup->addAction(ui.actionDrawNone);
192   annotationGroup->addAction(ui.actionDrawArrow);
193   annotationGroup->addAction(ui.actionDrawRectangle);
194   annotationGroup->addAction(ui.actionDrawCircle);
195   annotationGroup->addAction(ui.actionDrawNumber);
196   ui.actionDrawNone->setChecked(true);
197 
198   // the "Open With..." menu
199   connect(ui.menu_File, &QMenu::aboutToShow, this, &MainWindow::fileMenuAboutToShow);
200   connect(ui.openWithMenu, &QMenu::aboutToShow, this, &MainWindow::createOpenWithMenu);
201   connect(ui.openWithMenu, &QMenu::aboutToHide, this, &MainWindow::deleteOpenWithMenu);
202 
203   // create hard-coded keyboard shortcuts; they are set in setShortcuts() if not ambiguous
204   QShortcut* shortcut = new QShortcut(this);
205   hardCodedShortcuts_[Qt::Key_Left] = shortcut;
206   connect(shortcut, &QShortcut::activated, this, &MainWindow::on_actionPrevious_triggered);
207   shortcut = new QShortcut(this);
208   hardCodedShortcuts_[Qt::Key_Backspace] = shortcut;
209   connect(shortcut, &QShortcut::activated, this, &MainWindow::on_actionPrevious_triggered);
210   shortcut = new QShortcut(this);
211   hardCodedShortcuts_[Qt::Key_Right] = shortcut;
212   connect(shortcut, &QShortcut::activated, this, &MainWindow::on_actionNext_triggered);
213   shortcut = new QShortcut(this);
214   hardCodedShortcuts_[Qt::Key_Space] = shortcut;
215   connect(shortcut, &QShortcut::activated, this, &MainWindow::on_actionNext_triggered);
216   shortcut = new QShortcut(this);
217   hardCodedShortcuts_[Qt::Key_Home] = shortcut; // already in GUI but will be forced if removed
218   connect(shortcut, &QShortcut::activated, this, &MainWindow::on_actionFirst_triggered);
219   shortcut = new QShortcut(this);
220   hardCodedShortcuts_[Qt::Key_End] = shortcut; // already in GUI but will be forced if removed
221   connect(shortcut, &QShortcut::activated, this, &MainWindow::on_actionLast_triggered);
222   shortcut = new QShortcut(this);
223   hardCodedShortcuts_[Qt::Key_Escape] = shortcut;
224   connect(shortcut, &QShortcut::activated, this, &MainWindow::onKeyboardEscape);
225 
226   // set custom and hard-coded shortcuts
227   setShortcuts();
228 }
229 
~MainWindow()230 MainWindow::~MainWindow() {
231   delete slideShowTimer_;
232   delete thumbnailsView_;
233   delete thumbnailsDock_;
234 
235   if(loadJob_) {
236     loadJob_->cancel();
237     // we don't need to do delete here. It will be done automatically
238   }
239   //if(currentFileInfo_)
240   //  fm_file_info_unref(currentFileInfo_);
241   delete folderModel_;
242   delete proxyModel_;
243   delete modelFilter_;
244 
245   Application* app = static_cast<Application*>(qApp);
246   app->removeWindow();
247 }
248 
on_actionMenubar_triggered(bool checked)249 void MainWindow::on_actionMenubar_triggered(bool checked) {
250   if(!checked) {
251     // If menubar is hidden, shortcut keys inside menus will be disabled. Therefore,
252     // we need to add menubar actions manually to enable shortcuts before hiding menubar.
253     addActions(ui.menubar->actions());
254     ui.menubar->setVisible(false);
255   }
256   else if(!isFullScreen()) { // menubar is hidden in fullscreen
257     ui.menubar->setVisible(true);
258     // when menubar is shown again, remove the previously added actions.
259     const auto _actions = ui.menubar->actions();
260     for(const auto& action : _actions) {
261       removeAction(action);
262     }
263   }
264 }
265 
on_actionAbout_triggered()266 void MainWindow::on_actionAbout_triggered() {
267   QMessageBox::about(this, tr("About"),
268                      QStringLiteral("<center><b><big>LXImage-Qt %1</big></b></center><br>").arg(qApp->applicationVersion())
269                      + tr("A simple and fast image viewer")
270                      + QStringLiteral("<br><br>")
271                      + tr("Copyright (C) ") + tr("2013-2021")
272                      + QStringLiteral("<br><a href='https://lxqt-project.org'>")
273                      + tr("LXQt Project")
274                      + QStringLiteral("</a><br><br>")
275                      + tr("Development: ")
276                      + QStringLiteral("<a href='https://github.com/lxqt/lximage-qt'>https://github.com/lxqt/lximage-qt</a><br><br>")
277                      + tr("Author: ")
278                      + QStringLiteral("<a href='mailto:pcman.tw@gmail.com?Subject=My%20Subject'>Hong Jen Yee (PCMan)</a>"));
279 }
280 
on_actionOriginalSize_triggered()281 void MainWindow::on_actionOriginalSize_triggered() {
282   if(ui.actionOriginalSize->isChecked()) {
283     ui.view->setAutoZoomFit(false);
284     ui.view->zoomOriginal();
285   }
286 }
287 
on_actionHiddenShortcuts_triggered()288 void MainWindow::on_actionHiddenShortcuts_triggered() {
289     class HiddenShortcutsDialog : public QDialog {
290     public:
291         explicit HiddenShortcutsDialog(QWidget* parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags())
292             : QDialog(parent, f) {
293             ui.setupUi(this);
294             ui.treeWidget->setRootIsDecorated(false);
295             ui.treeWidget->header()->setSectionResizeMode(QHeaderView::Stretch);
296             ui.treeWidget->header()->setSectionsClickable(true);
297             ui.treeWidget->sortByColumn(0, Qt::AscendingOrder);
298             ui.treeWidget->setSortingEnabled(true);
299         }
300     private:
301         Ui::HiddenShortcutsDialog ui;
302     };
303     HiddenShortcutsDialog dialog(this);
304     dialog.exec();
305 }
306 
on_actionZoomFit_triggered()307 void MainWindow::on_actionZoomFit_triggered() {
308   if(ui.actionZoomFit->isChecked()) {
309     ui.view->setAutoZoomFit(true);
310     ui.view->zoomFit();
311   }
312 }
313 
on_actionZoomIn_triggered()314 void MainWindow::on_actionZoomIn_triggered() {
315   if(!ui.view->image().isNull()) {
316     ui.view->setAutoZoomFit(false);
317     ui.view->zoomIn();
318   }
319 }
320 
on_actionZoomOut_triggered()321 void MainWindow::on_actionZoomOut_triggered() {
322   if(!ui.view->image().isNull()) {
323     ui.view->setAutoZoomFit(false);
324     ui.view->zoomOut();
325   }
326 }
327 
onZooming()328 void MainWindow::onZooming() {
329   ui.actionZoomFit->setChecked(false);
330   ui.actionOriginalSize->setChecked(false);
331 }
332 
on_actionDrawNone_triggered()333 void MainWindow::on_actionDrawNone_triggered() {
334   ui.view->activateTool(ImageView::ToolNone);
335 }
336 
on_actionDrawArrow_triggered()337 void MainWindow::on_actionDrawArrow_triggered() {
338   ui.view->activateTool(ImageView::ToolArrow);
339 }
340 
on_actionDrawRectangle_triggered()341 void MainWindow::on_actionDrawRectangle_triggered() {
342   ui.view->activateTool(ImageView::ToolRectangle);
343 }
344 
on_actionDrawCircle_triggered()345 void MainWindow::on_actionDrawCircle_triggered() {
346   ui.view->activateTool(ImageView::ToolCircle);
347 }
348 
on_actionDrawNumber_triggered()349 void MainWindow::on_actionDrawNumber_triggered() {
350   ui.view->activateTool(ImageView::ToolNumber);
351 }
352 
353 
onFolderLoaded()354 void MainWindow::onFolderLoaded() {
355   // if currently we're showing a file, get its index in the folder now
356   // since the folder is fully loaded.
357   if(currentFile_ && !currentIndex_.isValid()) {
358     currentIndex_ = indexFromPath(currentFile_);
359     if(thumbnailsView_) { // showing thumbnails
360       // select current file in the thumbnails view
361       thumbnailsView_->childView()->setCurrentIndex(currentIndex_);
362       thumbnailsView_->childView()->scrollTo(currentIndex_, QAbstractItemView::EnsureVisible);
363     }
364   }
365   // this is used to open the first image of a folder
366   else if (!currentFile_) {
367     on_actionFirst_triggered();
368   }
369 }
370 
openImageFile(const QString & fileName)371 void MainWindow::openImageFile(const QString& fileName) {
372   const Fm::FilePath path = Fm::FilePath::fromPathStr(qPrintable(fileName));
373     // the same file! do not load it again
374   if(currentFile_ && currentFile_ == path)
375     return;
376 
377   if (QFileInfo(fileName).isDir()) {
378     if(path == folderPath_)
379       return;
380 
381     const QList<QByteArray> formats = QImageReader::supportedImageFormats();
382     QStringList formatsFilters;
383     for (const QByteArray& format: formats)
384       formatsFilters << QStringLiteral("*.") + QString::fromUtf8(format);
385     QDir dir(fileName);
386     dir.setNameFilters(formatsFilters);
387     dir.setFilter(QDir::Files | QDir::NoDotAndDotDot);
388     if(dir.entryList().isEmpty())
389       return;
390 
391     currentFile_ = Fm::FilePath{};
392     loadFolder(path);
393   }
394   else {
395     // load the image file asynchronously
396     loadImage(path);
397     loadFolder(path.parent());
398   }
399 }
400 
401 // paste the specified image into the current view,
402 // reset the window, remove loaded folders, and
403 // invalidate current file name.
pasteImage(QImage newImage)404 void MainWindow::pasteImage(QImage newImage) {
405   // cancel loading of current image
406   if(loadJob_) {
407     loadJob_->cancel(); // the job object will be freed automatically later
408     loadJob_ = nullptr;
409   }
410   setModified(true);
411 
412   currentIndex_ = QModelIndex(); // invaludate current index since we don't have a folder model now
413   currentFile_ = Fm::FilePath{};
414 
415   image_ = newImage;
416   ui.view->setImage(image_);
417   // always fit the image on pasting
418   ui.actionZoomFit->setChecked(true);
419   ui.view->setAutoZoomFit(true);
420   ui.view->zoomFit();
421 
422   updateUI();
423 }
424 
425 // popup a file dialog and retrieve the selected image file name
openFileName()426 QString MainWindow::openFileName() {
427   QString filterStr;
428   QList<QByteArray> formats = QImageReader::supportedImageFormats();
429   QList<QByteArray>::iterator it = formats.begin();
430   for(;;) {
431     filterStr += QLatin1String("*.");
432     filterStr += QString::fromUtf8((*it).toLower());
433     ++it;
434     if(it != formats.end())
435       filterStr += QLatin1Char(' ');
436     else
437       break;
438   }
439 
440   QString curFileName;
441   if(currentFile_) {
442     curFileName = QString::fromUtf8(currentFile_.displayName().get());
443   }
444   else {
445     curFileName = QString::fromUtf8(Fm::FilePath::homeDir().toString().get());
446   }
447   QString fileName = QFileDialog::getOpenFileName(
448     this, tr("Open File"), curFileName,
449           tr("Image files (%1)").arg(filterStr));
450   return fileName;
451 }
452 
openDirectory()453 QString MainWindow::openDirectory() {
454   QString curDirName;
455   if(currentFile_ && currentFile_.parent()) {
456     curDirName = QString::fromUtf8(currentFile_.parent().displayName().get());
457   }
458   else {
459     curDirName = QString::fromUtf8(Fm::FilePath::homeDir().toString().get());
460   }
461   QString directory = QFileDialog::getExistingDirectory(this,
462           tr("Open directory"), curDirName);
463   return directory;
464 }
465 
466 // popup a file dialog and retrieve the selected image file name
saveFileName(const QString & defaultName)467 QString MainWindow::saveFileName(const QString& defaultName) {
468   QString filterStr;
469   QList<QByteArray> formats = QImageWriter::supportedImageFormats();
470   QList<QByteArray>::iterator it = formats.begin();
471   for(;;) {
472     filterStr += QLatin1String("*.");
473     filterStr += QString::fromUtf8((*it).toLower());
474     ++it;
475     if(it != formats.end())
476       filterStr += QLatin1Char(' ');
477     else
478       break;
479   }
480   // FIXME: should we generate better filter strings? one format per item?
481 
482   QString fileName;
483   QFileDialog diag(this, tr("Save File"), defaultName,
484                tr("Image files (%1)").arg(filterStr));
485   diag.setAcceptMode(QFileDialog::AcceptMode::AcceptSave);
486   diag.setFileMode(QFileDialog::FileMode::AnyFile);
487   diag.setDefaultSuffix(QStringLiteral("png"));
488 
489   if (diag.exec()) {
490     fileName = diag.selectedFiles().at(0);
491   }
492 
493   return fileName;
494 }
495 
on_actionOpenFile_triggered()496 void MainWindow::on_actionOpenFile_triggered() {
497   QString fileName = openFileName();
498   if(!fileName.isEmpty()) {
499     openImageFile(fileName);
500   }
501 }
502 
on_actionOpenDirectory_triggered()503 void MainWindow::on_actionOpenDirectory_triggered() {
504   QString directory = openDirectory();
505   if(!directory.isEmpty()) {
506     openImageFile(directory);
507   }
508 }
509 
on_actionReload_triggered()510 void MainWindow::on_actionReload_triggered() {
511   if (currentFile_) {
512     loadImage(currentFile_);
513   }
514 }
515 
on_actionNewWindow_triggered()516 void MainWindow::on_actionNewWindow_triggered() {
517   Application* app = static_cast<Application*>(qApp);
518   MainWindow* window = new MainWindow();
519   window->resize(app->settings().windowWidth(), app->settings().windowHeight());
520 
521   if(app->settings().windowMaximized())
522     window->setWindowState(window->windowState() | Qt::WindowMaximized);
523 
524   window->show();
525 }
526 
on_actionSave_triggered()527 void MainWindow::on_actionSave_triggered() {
528   if(saveJob_) // if we're currently saving another file
529     return;
530 
531   if(!image_.isNull()) {
532     if(currentFile_)
533       saveImage(currentFile_);
534     else
535       on_actionSaveAs_triggered();
536   }
537 }
538 
on_actionSaveAs_triggered()539 void MainWindow::on_actionSaveAs_triggered() {
540   if(saveJob_) // if we're currently saving another file
541     return;
542   QString curFileName;
543   if(currentFile_) {
544     curFileName = QString::fromUtf8(currentFile_.displayName().get());
545   }
546 
547   QString fileName = saveFileName(curFileName);
548   if(!fileName.isEmpty()) {
549     const Fm::FilePath path = Fm::FilePath::fromPathStr(qPrintable(fileName));
550     // save the image file asynchronously
551     saveImage(path);
552   }
553 }
554 
on_actionDelete_triggered()555 void MainWindow::on_actionDelete_triggered() {
556   // delete or trash the current file
557   if(currentFile_) {
558     if(static_cast<Application*>(qApp)->settings().useTrash()) {
559       Fm::FileOperation::trashFiles({currentFile_}, false);
560     }
561     else {
562       Fm::FileOperation::deleteFiles({currentFile_}, true);
563     }
564   }
565 }
566 
on_actionFileProperties_triggered()567 void MainWindow::on_actionFileProperties_triggered() {
568   if(currentIndex_.isValid()) {
569     const auto file = proxyModel_->fileInfoFromIndex(currentIndex_);
570     // it's better to use an async job to query the file info since it's
571     // possible that loading of the folder is not finished and the file info is
572     // not available yet, but it's overkill for a rarely used function.
573     if(file)
574       Fm::FilePropsDialog::showForFile(std::move(file));
575   }
576 }
577 
on_actionClose_triggered()578 void MainWindow::on_actionClose_triggered() {
579   deleteLater();
580 }
581 
on_actionNext_triggered()582 void MainWindow::on_actionNext_triggered() {
583   if(proxyModel_->rowCount() <= 1)
584     return;
585   if(currentIndex_.isValid()) {
586     QModelIndex index;
587     if(currentIndex_.row() < proxyModel_->rowCount() - 1)
588       index = proxyModel_->index(currentIndex_.row() + 1, 0);
589     else
590       index = proxyModel_->index(0, 0);
591     const auto info = proxyModel_->fileInfoFromIndex(index);
592     if(info)
593       loadImage(info->path(), index);
594   }
595 }
596 
on_actionPrevious_triggered()597 void MainWindow::on_actionPrevious_triggered() {
598   if(proxyModel_->rowCount() <= 1)
599     return;
600   if(currentIndex_.isValid()) {
601     QModelIndex index;
602     if(currentIndex_.row() > 0)
603       index = proxyModel_->index(currentIndex_.row() - 1, 0);
604     else
605       index = proxyModel_->index(proxyModel_->rowCount() - 1, 0);
606     const auto info = proxyModel_->fileInfoFromIndex(index);
607     if(info)
608       loadImage(info->path(), index);
609   }
610 }
611 
on_actionFirst_triggered()612 void MainWindow::on_actionFirst_triggered() {
613   QModelIndex index = proxyModel_->index(0, 0);
614   if(index.isValid()) {
615     const auto info = proxyModel_->fileInfoFromIndex(index);
616     if(info)
617       loadImage(info->path(), index);
618   }
619 }
620 
on_actionLast_triggered()621 void MainWindow::on_actionLast_triggered() {
622   QModelIndex index = proxyModel_->index(proxyModel_->rowCount() - 1, 0);
623   if(index.isValid()) {
624     const auto info = proxyModel_->fileInfoFromIndex(index);
625     if(info)
626       loadImage(info->path(), index);
627   }
628 }
629 
loadFolder(const Fm::FilePath & newFolderPath)630 void MainWindow::loadFolder(const Fm::FilePath & newFolderPath) {
631   if(folder_) { // an folder is already loaded
632     if(newFolderPath == folderPath_) { // same folder, ignore
633       return;
634     }
635     disconnect(folder_.get(), nullptr, this, nullptr); // disconnect from all signals
636   }
637 
638   folderPath_ = newFolderPath;
639   folder_ = Fm::Folder::fromPath(folderPath_);
640   currentIndex_ = QModelIndex(); // set current index to invalid
641   folderModel_->setFolder(folder_);
642   if(folder_->isLoaded()) { // the folder may be already loaded elsewhere
643       onFolderLoaded();
644   }
645   connect(folder_.get(), &Fm::Folder::finishLoading, this, &MainWindow::onFolderLoaded);
646   connect(folder_.get(), &Fm::Folder::filesRemoved, this, &MainWindow::onFilesRemoved);
647 }
648 
649 // the image is loaded (the method is only called if the loading is not cancelled)
onImageLoaded()650 void MainWindow::onImageLoaded() {
651   // Note: As the signal finished() is emitted from different thread,
652   // we can get it even after canceling the job (and setting the loadJob_
653   // to nullptr). This simple check should be enough.
654   if (sender() == loadJob_)
655   {
656     // Add to the MRU menu
657     ui.menuRecently_Opened_Files->addItem(QString::fromUtf8(loadJob_->filePath().localPath().get()));
658 
659     image_ = loadJob_->image();
660     exifData_ = loadJob_->getExifData();
661 
662     loadJob_ = nullptr; // the job object will be freed later automatically
663 
664     // set image zoom, like in loadImage()
665     if(static_cast<Application*>(qApp)->settings().forceZoomFit()) {
666       ui.actionZoomFit->setChecked(true);
667     }
668     ui.view->setAutoZoomFit(ui.actionZoomFit->isChecked());
669     if(ui.actionOriginalSize->isChecked()) {
670       ui.view->zoomOriginal();
671     }
672 
673     ui.view->setImage(image_);
674 
675     // currentIndex_ should be corrected after loading
676     currentIndex_ = indexFromPath(currentFile_);
677 
678     updateUI();
679 
680     /* we resized and moved the window without showing
681        it in updateUI(), so we need to show it here */
682     if(!isVisible()) {
683       if(startMaximized_) {
684         setWindowState(windowState() | Qt::WindowMaximized);
685       }
686       showAndRaise();
687     }
688   }
689 }
690 
onImageSaved()691 void MainWindow::onImageSaved() {
692   if(!saveJob_->failed()) {
693     loadImage(saveJob_->filePath());
694     loadFolder(saveJob_->filePath().parent());
695   }
696   saveJob_ = nullptr;
697 }
698 
699 // filter events of other objects, mainly the image view.
eventFilter(QObject * watched,QEvent * event)700 bool MainWindow::eventFilter(QObject* watched, QEvent* event) {
701   if(watched == ui.view) { // we got an event for the image view
702     switch(event->type()) {
703       case QEvent::Wheel: { // mouse wheel event
704         QWheelEvent* wheelEvent = static_cast<QWheelEvent*>(event);
705         if(wheelEvent->modifiers() == 0) {
706           QPoint angleDelta = wheelEvent->angleDelta();
707           Qt::Orientation orient = (qAbs(angleDelta.x()) > qAbs(angleDelta.y()) ? Qt::Horizontal : Qt::Vertical);
708           int delta = (orient == Qt::Horizontal ? angleDelta.x() : angleDelta.y());
709           // NOTE: Each turn of a mouse wheel can change the image without problem but
710           // touchpads trigger wheel events with much smaller angle deltas. Therefore,
711           // we wait until a threshold is passed. 120 is an appropriate value because
712           // most mouse types create angle deltas that are multiples of 120.
713           static int deltaThreshold = 0;
714           deltaThreshold += abs(delta);
715           if(deltaThreshold >= 120) {
716             deltaThreshold = 0;
717             if(delta < 0)
718               on_actionNext_triggered(); // next image
719             else
720               on_actionPrevious_triggered(); // previous image
721           }
722         }
723         break;
724       }
725       case QEvent::MouseButtonDblClick: {
726         QMouseEvent* mouseEvent = static_cast<QMouseEvent*>(event);
727         if(mouseEvent->button() == Qt::LeftButton)
728           ui.actionFullScreen->trigger();
729         break;
730       }
731       default:;
732     }
733   }
734   return QObject::eventFilter(watched, event);
735 }
736 
indexFromPath(const Fm::FilePath & filePath)737 QModelIndex MainWindow::indexFromPath(const Fm::FilePath & filePath) {
738   // if the folder is already loaded, figure out our index
739   // otherwise, it will be done again in onFolderLoaded() when the folder is fully loaded.
740   if(folder_ && folder_->isLoaded()) {
741     QModelIndex index;
742     int count = proxyModel_->rowCount();
743     for(int row = 0; row < count; ++row) {
744       index = proxyModel_->index(row, 0);
745       const auto info = proxyModel_->fileInfoFromIndex(index);
746       if(info && filePath == info->path()) {
747         return index;
748       }
749     }
750   }
751   return QModelIndex();
752 }
753 
754 
updateUI()755 void MainWindow::updateUI() {
756   if(currentIndex_.isValid()) {
757     if(thumbnailsView_) { // showing thumbnails
758       // select current file in the thumbnails view
759       thumbnailsView_->childView()->setCurrentIndex(currentIndex_);
760       thumbnailsView_->childView()->scrollTo(currentIndex_, QAbstractItemView::EnsureVisible);
761     }
762 
763     if(exifDataDock_) {
764       setShowExifData(true);
765     }
766   }
767 
768   QString title;
769   if(currentFile_) {
770     const Fm::CStrPtr dispName = currentFile_.displayName();
771     if(loadJob_) { // if loading is in progress
772       title = tr("[*]%1 (Loading...) - Image Viewer")
773                 .arg(QString::fromUtf8(dispName.get()));
774       ui.statusBar->setText();
775     }
776     else {
777       if(image_.isNull()) {
778         title = tr("[*]%1 (Failed to Load) - Image Viewer")
779                   .arg(QString::fromUtf8(dispName.get()));
780         ui.statusBar->setText();
781       }
782       else {
783         const QString filePath = QString::fromUtf8(dispName.get());
784         title = tr("[*]%1 (%2x%3) - Image Viewer")
785                   .arg(filePath)
786                   .arg(image_.width())
787                   .arg(image_.height());
788         ui.statusBar->setText(QStringLiteral("%1×%2").arg(image_.width()).arg(image_.height()),
789                               filePath);
790         if (!isVisible()) {
791           /* Here we try to implement the following behavior as far as possible:
792               (1) A minimum size of 400x400 is assumed;
793               (2) The window is scaled to fit the image;
794               (3) But for too big images, the window is scaled down;
795               (4) The window is centered on the screen.
796 
797              To have a correct position, we should move the window BEFORE
798              it's shown and we also need to know the dimensions of its view.
799              Therefore, we first use show() without really showing the window. */
800 
801           // the maximization setting may be lost in resizeEvent because we resize the window below
802           startMaximized_ = static_cast<Application*>(qApp)->settings().windowMaximized();
803           setAttribute(Qt::WA_DontShowOnScreen);
804           show();
805           int scrollThickness = style()->pixelMetric(QStyle::PM_ScrollBarExtent);
806           QSize newSize = size() + image_.size() - ui.view->size() + QSize(scrollThickness, scrollThickness);
807           QScreen *appScreen = QGuiApplication::screenAt(QCursor::pos());
808           if(appScreen == nullptr) {
809             appScreen = QGuiApplication::primaryScreen();
810           }
811           const QRect ag = appScreen ? appScreen->availableGeometry() : QRect();
812           // since the window isn't decorated yet, we have to assume a max thickness for its frame
813           QSize maxFrame = QSize(50, 100);
814           if(newSize.width() > ag.width() - maxFrame.width()
815              || newSize.height() > ag.height() - maxFrame.height()) {
816             newSize.scale(ag.width() - maxFrame.width(), ag.height() - maxFrame.height(), Qt::KeepAspectRatio);
817           }
818           newSize = newSize.expandedTo(QSize(400, 400)); // a minimum size of 400x400 is good
819           move(ag.x() + (ag.width() - newSize.width())/2,
820                ag.y() + (ag.height() - newSize.height())/2);
821           resize(newSize);
822           hide(); // hide it to show it again later, at onImageLoaded()
823           setAttribute(Qt::WA_DontShowOnScreen, false);
824         }
825       }
826     }
827     // TODO: update status bar, show current index in the folder
828   }
829   else {
830     title = tr("[*]Image Viewer");
831     ui.statusBar->setText();
832   }
833   setWindowTitle(title);
834   setWindowModified(imageModified_);
835 }
836 
837 // Load the specified image file asynchronously in a worker thread.
838 // When the loading is finished, onImageLoaded() will be called.
loadImage(const Fm::FilePath & filePath,QModelIndex index)839 void MainWindow::loadImage(const Fm::FilePath & filePath, QModelIndex index) {
840   // cancel loading of current image
841   if(loadJob_) {
842     loadJob_->cancel(); // the job object will be freed automatically later
843     loadJob_ = nullptr;
844   }
845   if(imageModified_) {
846     // TODO: ask the user to save the modified image?
847     // this should be made optional
848     setModified(false);
849   }
850 
851   currentIndex_ = index;
852   currentFile_ = filePath;
853   // clear current image, but do not update the view now to prevent flickers
854   image_ = QImage();
855 
856   const Fm::CStrPtr basename = currentFile_.baseName();
857   char* mime_type = g_content_type_guess(basename.get(), nullptr, 0, nullptr);
858   QString mimeType;
859   if (mime_type) {
860     mimeType = QString::fromUtf8(mime_type);
861     g_free(mime_type);
862   }
863   if(mimeType == QLatin1String("image/gif")
864      || mimeType == QLatin1String("image/svg+xml") || mimeType == QLatin1String("image/svg+xml-compressed")) {
865     if(!currentIndex_.isValid()) {
866       // since onImageLoaded is not called here,
867       // currentIndex_ should be set
868       currentIndex_ = indexFromPath(currentFile_);
869     }
870     const Fm::CStrPtr file_name = currentFile_.toString();
871 
872     // set image zoom, like in onImageLoaded()
873     if(static_cast<Application*>(qApp)->settings().forceZoomFit()) {
874       ui.actionZoomFit->setChecked(true);
875     }
876     ui.view->setAutoZoomFit(ui.actionZoomFit->isChecked());
877     if(ui.actionOriginalSize->isChecked()) {
878       ui.view->zoomOriginal();
879     }
880 
881     if(mimeType == QLatin1String("image/gif"))
882       ui.view->setGifAnimation(QString::fromUtf8(file_name.get()));
883     else
884       ui.view->setSVG(QString::fromUtf8(file_name.get()));
885     image_ = ui.view->image();
886     updateUI();
887     if(!isVisible()) {
888       if(startMaximized_) {
889         setWindowState(windowState() | Qt::WindowMaximized);
890       }
891       showAndRaise();
892     }
893   }
894   else {
895     // start a new gio job to load the specified image
896     loadJob_ = new LoadImageJob(currentFile_);
897     connect(loadJob_, &Fm::Job::finished, this, &MainWindow::onImageLoaded);
898     connect(loadJob_, &Fm::Job::error, this
899         , [] (const Fm::GErrorPtr & err, Fm::Job::ErrorSeverity /*severity*/, Fm::Job::ErrorAction & /*response*/)
900           {
901             // TODO: show a info bar?
902             qWarning().noquote() << "lximage-qt:" << err.message();
903           }
904         , Qt::BlockingQueuedConnection);
905     loadJob_->runAsync();
906 
907     updateUI();
908   }
909 }
910 
saveImage(const Fm::FilePath & filePath)911 void MainWindow::saveImage(const Fm::FilePath & filePath) {
912   if(saveJob_) // do not launch a new job if the current one is still in progress
913     return;
914   // start a new gio job to save current image to the specified path
915   saveJob_ = new SaveImageJob(ui.view->image(), filePath);
916   connect(saveJob_, &Fm::Job::finished, this, &MainWindow::onImageSaved);
917   connect(saveJob_, &Fm::Job::error, this
918         , [this] (const Fm::GErrorPtr & err, Fm::Job::ErrorSeverity severity, Fm::Job::ErrorAction & /*response*/)
919         {
920           // TODO: show a info bar?
921           if(severity > Fm::Job::ErrorSeverity::MODERATE) {
922             QMessageBox::critical(this, QObject::tr("Error"), err.message());
923           }
924           else {
925             qWarning().noquote() << "lximage-qt:" << err.message();
926           }
927         }
928       , Qt::BlockingQueuedConnection);
929   saveJob_->runAsync();
930   // FIXME: add a cancel button to the UI? update status bar?
931 }
932 
on_actionRotateClockwise_triggered()933 void MainWindow::on_actionRotateClockwise_triggered() {
934   if(!image_.isNull()) {
935     ui.view->rotateImage(true);
936     image_ = ui.view->image();
937     setModified(true);
938   }
939 }
940 
on_actionRotateCounterclockwise_triggered()941 void MainWindow::on_actionRotateCounterclockwise_triggered() {
942   if(!image_.isNull()) {
943     ui.view->rotateImage(false);
944     image_ = ui.view->image();
945     setModified(true);
946   }
947 }
948 
on_actionCopy_triggered()949 void MainWindow::on_actionCopy_triggered() {
950   QClipboard *clipboard = QApplication::clipboard();
951   QImage copiedImage = image_;
952   // FIXME: should we copy the currently scaled result instead of the original image?
953   /*
954   double factor = ui.view->scaleFactor();
955   if(factor == 1.0)
956     copiedImage = image_;
957   else
958     copiedImage = image_.scaled();
959   */
960   clipboard->setImage(copiedImage);
961 }
962 
on_actionCopyPath_triggered()963 void MainWindow::on_actionCopyPath_triggered() {
964   if(currentFile_) {
965     const Fm::CStrPtr dispName = currentFile_.displayName();
966     QApplication::clipboard()->setText(QString::fromUtf8(dispName.get()));
967   }
968 }
969 
on_actionRenameFile_triggered()970 void MainWindow::on_actionRenameFile_triggered() {
971   // rename inline if the thumbnail bar is shown; otherwise, show the rename dialog
972   if(!currentIndex_.isValid()) {
973     return;
974   }
975   if(thumbnailsView_ && thumbnailsView_->isVisible()) {
976     QAbstractItemView* view = thumbnailsView_->childView();
977     view->scrollTo(currentIndex_);
978     view->edit(currentIndex_);
979   }
980   else if(const auto file = proxyModel_->fileInfoFromIndex(currentIndex_)) {
981     Fm::renameFile(file, this);
982   }
983 }
984 
on_actionPaste_triggered()985 void MainWindow::on_actionPaste_triggered() {
986   QClipboard *clipboard = QApplication::clipboard();
987   QImage image = clipboard->image();
988   if(!image.isNull()) {
989     pasteImage(image);
990   }
991 }
992 
on_actionUpload_triggered()993 void MainWindow::on_actionUpload_triggered()
994 {
995     if(currentFile_.isValid()) {
996       UploadDialog(this, QString::fromUtf8(currentFile_.localPath().get())).exec();
997     }
998     // if there is no open file, save the image to "/tmp" and delete it after upload
999     else if(!ui.view->image().isNull()) {
1000       QString tmpFileName;
1001       QString tmp = QStandardPaths::writableLocation(QStandardPaths::TempLocation);
1002       if(!tmp.isEmpty()) {
1003         QDir tmpDir(tmp);
1004         if(tmpDir.exists()) {
1005           const QString curTime = QDateTime::currentDateTime().toString(QStringLiteral("yyyyMMddhhmmss"));
1006           tmpFileName = tmp + QStringLiteral("/lximage-") + curTime + QStringLiteral(".png");
1007         }
1008       }
1009       if(!tmpFileName.isEmpty() && saveJob_ == nullptr) {
1010         const Fm::FilePath filePath = Fm::FilePath::fromPathStr(qPrintable(tmpFileName));
1011         saveJob_ = new SaveImageJob(ui.view->image(), filePath);
1012         connect(saveJob_, &Fm::Job::finished, this, [this, tmpFileName] {
1013           saveJob_ = nullptr;
1014           UploadDialog(this, tmpFileName).exec();
1015           QFile::remove(tmpFileName);
1016         });
1017         saveJob_->runAsync();
1018       }
1019     }
1020 }
1021 
on_actionFlipVertical_triggered()1022 void MainWindow::on_actionFlipVertical_triggered() {
1023   if(!image_.isNull()) {
1024     ui.view->flipImage(false);
1025     image_ = ui.view->image();
1026     setModified(true);
1027   }
1028 }
1029 
on_actionFlipHorizontal_triggered()1030 void MainWindow::on_actionFlipHorizontal_triggered() {
1031   if(!image_.isNull()) {
1032     ui.view->flipImage(true);
1033     image_ = ui.view->image();
1034     setModified(true);
1035   }
1036 }
1037 
on_actionResize_triggered()1038 void MainWindow::on_actionResize_triggered() {
1039   if(image_.isNull()) {
1040     return;
1041   }
1042   ResizeImageDialog *dialog = new ResizeImageDialog(this);
1043   dialog->setOriginalSize(image_.size());
1044   if(dialog->exec() == QDialog::Accepted) {
1045     QSize newSize = dialog->scaledSize();
1046     if(ui.view->resizeImage(newSize)) {
1047       image_ = ui.view->image();
1048       setModified(true);
1049     }
1050   }
1051   dialog->deleteLater();
1052 }
1053 
setModified(bool modified)1054 void MainWindow::setModified(bool modified) {
1055   imageModified_ = modified;
1056   updateUI(); // should be done even if imageModified_ is not changed (because of transformations)
1057 }
1058 
applySettings()1059 void MainWindow::applySettings() {
1060   Application* app = static_cast<Application*>(qApp);
1061   Settings& settings = app->settings();
1062   if(isFullScreen())
1063     ui.view->setBackgroundBrush(QBrush(settings.fullScreenBgColor()));
1064   else
1065     ui.view->setBackgroundBrush(QBrush(settings.bgColor()));
1066   ui.view->updateOutline();
1067   ui.view->setSmoothOnZoom(settings.smoothOnZoom());
1068   ui.menuRecently_Opened_Files->setMaxItems(settings.maxRecentFiles());
1069 
1070   // also, update shortcuts
1071   setShortcuts(true);
1072 }
1073 
1074 // Sets or updates shortcuts.
setShortcuts(bool update)1075 void MainWindow::setShortcuts(bool update) {
1076   Application* app = static_cast<Application*>(qApp);
1077   const auto actions = findChildren<QAction*>();
1078 
1079   // get default shortcuts if this is the first window
1080   if(app->defaultShortcuts().isEmpty()) {
1081     QHash<QString, Application::ShortcutDescription> defaultShortcuts;
1082     for(const auto& action : actions) {
1083       if(action->objectName().isEmpty() || action->text().isEmpty()) {
1084         continue;
1085       }
1086       QKeySequence seq = action->shortcut();
1087       Application::ShortcutDescription s;
1088       s.displayText = action->text().remove(QLatin1Char('&')); // without mnemonics
1089       s.shortcut = seq;
1090       defaultShortcuts.insert(action->objectName(), s);
1091     }
1092     app->setDefaultShortcuts(defaultShortcuts);
1093   }
1094 
1095   auto hardCodedShortcuts = hardCodedShortcuts_;
1096 
1097   // set custom shortcuts
1098   QHash<QString, QString> ca = app->settings().customShortcutActions();
1099   for(const auto& action : actions) {
1100     const QString objectName = action->objectName();
1101     if(ca.contains(objectName)) {
1102       // custom shortcuts are saved in the PortableText format
1103       auto keySeq = QKeySequence(ca.take(objectName), QKeySequence::PortableText);
1104       action->setShortcut(keySeq);
1105       if(!hardCodedShortcuts.isEmpty()) {
1106         for(int i = 0; i < keySeq.count(); ++i) {
1107           if(hardCodedShortcuts.contains(keySeq[i])) { // would be ambiguous
1108             hardCodedShortcuts.take(keySeq[i])->setKey(QKeySequence());
1109           }
1110         }
1111       }
1112     }
1113     else if (update) { // restore default shortcuts
1114       action->setShortcut(app->defaultShortcuts().value(objectName).shortcut);
1115     }
1116     if(!update && ca.isEmpty()) {
1117       break;
1118     }
1119   }
1120 
1121   // set unambiguous hard-coded shortcuts too
1122   // but force Home and End keys if they are not action shortcuts
1123   if(hardCodedShortcuts.contains(Qt::Key_Home) && ui.actionFirst->shortcut() == QKeySequence(Qt::Key_Home)) {
1124     hardCodedShortcuts.take(Qt::Key_Home)->setKey(QKeySequence());
1125   }
1126   if(hardCodedShortcuts.contains(Qt::Key_End) && ui.actionLast->shortcut() == QKeySequence(Qt::Key_End)) {
1127     hardCodedShortcuts.take(Qt::Key_End)->setKey(QKeySequence());
1128   }
1129   QMap<int, QShortcut*>::const_iterator it = hardCodedShortcuts.constBegin();
1130   while (it != hardCodedShortcuts.constEnd()) {
1131     it.value()->setKey(QKeySequence(it.key()));
1132     ++it;
1133   }
1134 }
1135 
on_actionPrint_triggered()1136 void MainWindow::on_actionPrint_triggered() {
1137   // QPrinter printer(QPrinter::HighResolution);
1138   QPrinter printer;
1139   QPrintDialog dlg(&printer);
1140   if(dlg.exec() == QDialog::Accepted) {
1141     QPainter painter;
1142     painter.begin(&printer);
1143 
1144     // fit the target rectangle into the viewport if needed and center it
1145     const QRectF viewportRect = painter.viewport();
1146     QRectF targetRect = image_.rect();
1147     if(viewportRect.width() < targetRect.width()) {
1148       targetRect.setSize(QSize(viewportRect.width(), targetRect.height() * (viewportRect.width() / targetRect.width())));
1149     }
1150     if(viewportRect.height() < targetRect.height()) {
1151       targetRect.setSize(QSize(targetRect.width() * (viewportRect.height() / targetRect.height()), viewportRect.height()));
1152     }
1153     targetRect.moveCenter(viewportRect.center());
1154 
1155     // set the viewport and window of the painter and paint the image
1156     painter.setViewport(targetRect.toRect());
1157     painter.setWindow(image_.rect());
1158     painter.drawImage(0, 0, image_);
1159 
1160     painter.end();
1161 
1162     // FIXME: The following code divides the image into columns and could be used later as an option.
1163     /*QRect pageRect = printer.pageRect();
1164     int cols = (image_.width() / pageRect.width()) + (image_.width() % pageRect.width() ? 1 : 0);
1165     int rows = (image_.height() / pageRect.height()) + (image_.height() % pageRect.height() ? 1 : 0);
1166     for(int row = 0; row < rows; ++row) {
1167       for(int col = 0; col < cols; ++col) {
1168         QRect srcRect(pageRect.width() * col, pageRect.height() * row, pageRect.width(), pageRect.height());
1169         painter.drawImage(QPoint(0, 0), image_, srcRect);
1170         if(col + 1 == cols && row + 1 == rows) // this is the last page
1171           break;
1172         printer.newPage();
1173       }
1174     }
1175     painter.end();*/
1176   }
1177 }
1178 
1179 // TODO: This can later be used for doing slide show
on_actionFullScreen_triggered(bool checked)1180 void MainWindow::on_actionFullScreen_triggered(bool checked) {
1181   if(checked)
1182     showFullScreen();
1183   else
1184     showNormal();
1185 }
1186 
on_actionSlideShow_triggered(bool checked)1187 void MainWindow::on_actionSlideShow_triggered(bool checked) {
1188   if(checked) {
1189     if(!slideShowTimer_) {
1190       slideShowTimer_ = new QTimer();
1191       // switch to the next image when timeout
1192       connect(slideShowTimer_, &QTimer::timeout, this, &MainWindow::on_actionNext_triggered);
1193     }
1194     Application* app = static_cast<Application*>(qApp);
1195     slideShowTimer_->start(app->settings().slideShowInterval() * 1000);
1196     // showFullScreen();
1197     ui.actionSlideShow->setIcon(QIcon::fromTheme(QStringLiteral("media-playback-stop")));
1198   }
1199   else {
1200     if(slideShowTimer_) {
1201       delete slideShowTimer_;
1202       slideShowTimer_ = nullptr;
1203       ui.actionSlideShow->setIcon(QIcon::fromTheme(QStringLiteral("media-playback-start")));
1204     }
1205   }
1206 }
1207 
on_actionShowThumbnails_triggered(bool checked)1208 void MainWindow::on_actionShowThumbnails_triggered(bool checked) {
1209   setShowThumbnails(checked);
1210 }
1211 
on_actionShowOutline_triggered(bool checked)1212 void MainWindow::on_actionShowOutline_triggered(bool checked) {
1213   ui.view->showOutline(checked);
1214 }
1215 
on_actionShowExifData_triggered(bool checked)1216 void MainWindow::on_actionShowExifData_triggered(bool checked) {
1217   setShowExifData(checked);
1218   if(checked && exifDataDock_) {
1219     exifDataDock_->show(); // needed in the full-screen state
1220   }
1221 }
1222 
setShowThumbnails(bool show)1223 void MainWindow::setShowThumbnails(bool show) {
1224   Settings& settings = static_cast<Application*>(qApp)->settings();
1225 
1226   if(show) {
1227     if(!thumbnailsDock_) {
1228       thumbnailsDock_ = new QDockWidget(this);
1229       thumbnailsDock_->setFeatures(QDockWidget::NoDockWidgetFeatures); // FIXME: should use DockWidgetClosable
1230       thumbnailsDock_->setWindowTitle(tr("Thumbnails"));
1231       thumbnailsView_ = new Fm::FolderView(Fm::FolderView::IconMode);
1232       thumbnailsView_->setIconSize(Fm::FolderView::IconMode, QSize(settings.thumbnailSize(), settings.thumbnailSize()));
1233       thumbnailsView_->setAutoSelectionDelay(0);
1234       thumbnailsDock_->setWidget(thumbnailsView_);
1235       addDockWidget(settings.thumbnailsPosition(), thumbnailsDock_);
1236       QListView* listView = static_cast<QListView*>(thumbnailsView_->childView());
1237       listView->setSelectionMode(QAbstractItemView::SingleSelection);
1238       Fm::FolderItemDelegate* delegate = static_cast<Fm::FolderItemDelegate*>(listView->itemDelegateForColumn(Fm::FolderModel::ColumnFileName));
1239       int frameWidth = thumbnailsView_->style()->pixelMetric(QStyle::PM_DefaultFrameWidth, nullptr, thumbnailsView_);
1240       int scrollBarExtent = thumbnailsView_->style()->styleHint(QStyle::SH_ScrollBar_Transient, nullptr, thumbnailsView_) ?
1241                             0 : thumbnailsView_->style()->pixelMetric(QStyle::PM_ScrollBarExtent);
1242       switch(settings.thumbnailsPosition()) {
1243         case Qt::LeftDockWidgetArea:
1244         case Qt::RightDockWidgetArea:
1245           listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
1246           listView->setFlow(QListView::LeftToRight);
1247           if(delegate) {
1248             thumbnailsView_->setFixedWidth(delegate->itemSize().width() + 2 * frameWidth + scrollBarExtent);
1249           }
1250           break;
1251         case Qt::TopDockWidgetArea:
1252         case Qt::BottomDockWidgetArea:
1253         default:
1254           listView->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
1255           listView->setFlow(QListView::TopToBottom);
1256           if(delegate) {
1257             thumbnailsView_->setFixedHeight(delegate->itemSize().height() + 2 * frameWidth + scrollBarExtent);
1258           }
1259           break;
1260       }
1261       thumbnailsView_->setModel(proxyModel_);
1262       proxyModel_->setShowThumbnails(true);
1263       if (currentFile_) { // select the loaded image
1264         currentIndex_ = indexFromPath(currentFile_);
1265         listView->setCurrentIndex(currentIndex_);
1266         // wait to center the selection
1267         QCoreApplication::processEvents();
1268         listView->scrollTo(currentIndex_, QAbstractItemView::PositionAtCenter);
1269       }
1270       connect(thumbnailsView_->selectionModel(), &QItemSelectionModel::selectionChanged,
1271               this, &MainWindow::onThumbnailSelChanged);
1272     }
1273     else if (!thumbnailsDock_->isVisible()) {
1274       thumbnailsDock_->show();
1275       ui.actionShowThumbnails->setChecked(true);
1276     }
1277   }
1278   else {
1279     if(thumbnailsDock_) {
1280       delete thumbnailsView_;
1281       thumbnailsView_ = nullptr;
1282       delete thumbnailsDock_;
1283       thumbnailsDock_ = nullptr;
1284     }
1285     proxyModel_->setShowThumbnails(false);
1286   }
1287 }
1288 
updateThumbnails()1289 void MainWindow::updateThumbnails() {
1290   if(thumbnailsView_ == nullptr) {
1291     return;
1292   }
1293   int thumbSize = static_cast<Application*>(qApp)->settings().thumbnailSize();
1294   QSize newSize(thumbSize, thumbSize);
1295   if(thumbnailsView_->iconSize(Fm::FolderView::IconMode) == newSize) {
1296     return;
1297   }
1298 
1299   thumbnailsView_->setIconSize(Fm::FolderView::IconMode, newSize);
1300   QListView* listView = static_cast<QListView*>(thumbnailsView_->childView());
1301   if(Fm::FolderItemDelegate* delegate = static_cast<Fm::FolderItemDelegate*>(listView->itemDelegateForColumn(Fm::FolderModel::ColumnFileName))) {
1302     int frameWidth = thumbnailsView_->style()->pixelMetric(QStyle::PM_DefaultFrameWidth, nullptr, thumbnailsView_);
1303     int scrollBarExtent = thumbnailsView_->style()->styleHint(QStyle::SH_ScrollBar_Transient, nullptr, thumbnailsView_) ?
1304                           0 : thumbnailsView_->style()->pixelMetric(QStyle::PM_ScrollBarExtent);
1305     if(listView->flow() == QListView::LeftToRight) {
1306       thumbnailsView_->setFixedWidth(delegate->itemSize().width() + 2 * frameWidth + scrollBarExtent);
1307     }
1308     else {
1309       thumbnailsView_->setFixedHeight(delegate->itemSize().height() + 2 * frameWidth + scrollBarExtent);
1310     }
1311   }
1312 }
1313 
setShowExifData(bool show)1314 void MainWindow::setShowExifData(bool show) {
1315   Settings& settings = static_cast<Application*>(qApp)->settings();
1316   // Close the dock if it exists and show is false
1317   if(exifDataDock_ && !show) {
1318     settings.setExifDatakWidth(exifDataDock_->width());
1319     delete exifDataDock_;
1320     exifDataDock_ = nullptr;
1321   }
1322 
1323   // Be sure the dock was created before rendering content to it
1324   if(show && !exifDataDock_) {
1325     exifDataDock_ = new QDockWidget(tr("EXIF Data"), this);
1326     exifDataDock_->setFeatures(QDockWidget::NoDockWidgetFeatures);
1327     addDockWidget(Qt::RightDockWidgetArea, exifDataDock_);
1328     resizeDocks({exifDataDock_}, {settings.exifDatakWidth()}, Qt::Horizontal);
1329   }
1330 
1331   // Render the content to the dock
1332   if(show) {
1333     QWidget* exifDataDockView_ = new QWidget();
1334 
1335     QVBoxLayout* exifDataDockViewContent_ = new QVBoxLayout();
1336     QTableWidget* exifDataContentTable_ = new QTableWidget();
1337 
1338     // Table setup
1339     exifDataContentTable_->setColumnCount(2);
1340     exifDataContentTable_->setShowGrid(false);
1341     exifDataContentTable_->horizontalHeader()->hide();
1342     exifDataContentTable_->verticalHeader()->hide();
1343 
1344     // The table is not editable
1345     exifDataContentTable_->setEditTriggers(QAbstractItemView::NoEditTriggers);
1346 
1347     // Write the EXIF Data to the table
1348     const auto keys =exifData_.keys();
1349     for(const QString& key : keys) {
1350       int rowCount = exifDataContentTable_->rowCount();
1351 
1352       exifDataContentTable_->insertRow(rowCount);
1353       exifDataContentTable_->setItem(rowCount,0, new QTableWidgetItem(key));
1354       exifDataContentTable_->setItem(rowCount,1, new QTableWidgetItem(exifData_.value(key)));
1355     }
1356 
1357     // Table setup after content was added
1358     exifDataContentTable_->resizeColumnsToContents();
1359     exifDataContentTable_->setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel);
1360 
1361     exifDataDockViewContent_->addWidget(exifDataContentTable_);
1362     exifDataDockView_->setLayout(exifDataDockViewContent_);
1363     exifDataDock_->setWidget(exifDataDockView_);
1364   }
1365 }
1366 
changeEvent(QEvent * event)1367 void MainWindow::changeEvent(QEvent* event) {
1368   // TODO: hide menu/toolbars in full screen mode and make the background black.
1369   if(event->type() == QEvent::WindowStateChange) {
1370     Settings& settings = static_cast<Application*>(qApp)->settings();
1371     if(isFullScreen()) { // changed to fullscreen mode
1372       ui.view->setFrameStyle(QFrame::NoFrame);
1373       ui.view->setBackgroundBrush(QBrush(settings.fullScreenBgColor()));
1374       ui.view->updateOutline();
1375       ui.toolBar->hide();
1376       ui.annotationsToolBar->hide();
1377       ui.statusBar->hide();
1378       // It's logical to hide the thumbnail dock on full-screening. The user could show it
1379       // in the full-screen mode explicitly.
1380       if(thumbnailsDock_) {
1381         thumbnailsDock_->hide();
1382         ui.actionShowThumbnails->setChecked(false);
1383       }
1384       if(exifDataDock_) {
1385         settings.setExifDatakWidth(exifDataDock_->width()); // the user may have resized it
1386         exifDataDock_->hide();
1387         ui.actionShowExifData->setChecked(false);
1388       }
1389       // menubar is hidden in fullscreen mode but we need its menu shortcuts
1390       on_actionMenubar_triggered(false);
1391       ui.view->hideCursor(true);
1392     }
1393     else { // restore to normal window mode
1394       ui.view->setFrameStyle(QFrame::StyledPanel|QFrame::Sunken);
1395       ui.view->setBackgroundBrush(QBrush(settings.bgColor()));
1396       ui.view->updateOutline();
1397       if(ui.actionMenubar->isChecked()) {
1398         on_actionMenubar_triggered(true);
1399       }
1400       if(ui.actionToolbar->isChecked()){
1401         ui.toolBar->show();
1402       }
1403       if(ui.actionAnnotations->isChecked()){
1404         ui.annotationsToolBar->show();
1405       }
1406       ui.statusBar->show();
1407       if(thumbnailsDock_) {
1408         // The thumbnail dock exists but was hidden on full-screening. So, it should be restored.
1409         thumbnailsDock_->show();
1410         ui.actionShowThumbnails->setChecked(true);
1411       }
1412       if(exifDataDock_) {
1413         // The exif data dock exists but was hidden on full-screening. So, it should be restored.
1414         exifDataDock_->show();
1415         ui.actionShowExifData->setChecked(true);
1416       }
1417       ui.view->hideCursor(false);
1418     }
1419   }
1420   QWidget::changeEvent(event);
1421 }
1422 
resizeEvent(QResizeEvent * event)1423 void MainWindow::resizeEvent(QResizeEvent *event) {
1424   QMainWindow::resizeEvent(event);
1425   Settings& settings = static_cast<Application*>(qApp)->settings();
1426   if(settings.rememberWindowSize()) {
1427     settings.setLastWindowMaximized(isMaximized());
1428 
1429     if(!isMaximized()) {
1430         settings.setLastWindowWidth(width());
1431         settings.setLastWindowHeight(height());
1432     }
1433   }
1434 }
1435 
closeEvent(QCloseEvent * event)1436 void MainWindow::closeEvent(QCloseEvent *event)
1437 {
1438   QWidget::closeEvent(event);
1439   Settings& settings = static_cast<Application*>(qApp)->settings();
1440   if(exifDataDock_) {
1441     settings.setExifDatakWidth(exifDataDock_->width());
1442   }
1443   if(settings.rememberWindowSize()) {
1444     settings.setLastWindowMaximized(isMaximized());
1445 
1446     if(!isMaximized()) {
1447         settings.setLastWindowWidth(width());
1448         settings.setLastWindowHeight(height());
1449     }
1450   }
1451 }
1452 
onContextMenu(QPoint pos)1453 void MainWindow::onContextMenu(QPoint pos) {
1454   contextMenu_->exec(ui.view->mapToGlobal(pos));
1455 }
1456 
onKeyboardEscape()1457 void MainWindow::onKeyboardEscape() {
1458   if(isFullScreen())
1459     ui.actionFullScreen->trigger(); // will also "uncheck" the menu entry
1460   else
1461     on_actionClose_triggered();
1462 }
1463 
onThumbnailSelChanged(const QItemSelection & selected,const QItemSelection &)1464 void MainWindow::onThumbnailSelChanged(const QItemSelection& selected, const QItemSelection& /*deselected*/) {
1465   // the selected item of thumbnail view is changed
1466   if(!selected.isEmpty()) {
1467     QModelIndex index = selected.indexes().first();
1468     if(index.isValid()) {
1469       // WARNING: Adding the condition index != currentIndex_ would be wrong because currentIndex_ may not be updated yet
1470       const auto file = proxyModel_->fileInfoFromIndex(index);
1471       if(file) {
1472         loadImage(file->path(), index);
1473         return;
1474       }
1475     }
1476   }
1477   // no image to show; reload to show a blank view and update variables
1478   on_actionReload_triggered();
1479 }
1480 
onFilesRemoved(const Fm::FileInfoList & files)1481 void MainWindow::onFilesRemoved(const Fm::FileInfoList& files) {
1482   if(thumbnailsView_) {
1483     return; // onThumbnailSelChanged() will do the job
1484   }
1485   for(auto& file : files) {
1486     if(file->path() == currentFile_) {
1487       if(proxyModel_->rowCount() >= 1 && currentIndex_.isValid()) {
1488         QModelIndex index;
1489         if(currentIndex_.row() < proxyModel_->rowCount()) {
1490           index = currentIndex_;
1491         }
1492         else {
1493           index = proxyModel_->index(proxyModel_->rowCount() - 1, 0);
1494         }
1495         const auto info = proxyModel_->fileInfoFromIndex(index);
1496         if(info) {
1497           loadImage(info->path(), index);
1498           return;
1499         }
1500       }
1501       // no image to show; reload to show a blank view
1502       on_actionReload_triggered();
1503       return;
1504     }
1505   }
1506 }
1507 
onFileDropped(const QString path)1508 void MainWindow::onFileDropped(const QString path) {
1509     openImageFile(path);
1510 }
1511 
fileMenuAboutToShow()1512 void MainWindow::fileMenuAboutToShow() {
1513   // the "Open With..." submenu of Fm::FileMenu is shown
1514   // only if there is a file with a valid mime type
1515   if(currentIndex_.isValid()) {
1516     if(const auto file = proxyModel_->fileInfoFromIndex(currentIndex_)) {
1517       if(file->mimeType()) {
1518         ui.openWithMenu->setEnabled(true);
1519         return;
1520       }
1521     }
1522   }
1523   ui.openWithMenu->setEnabled(false);
1524 }
1525 
createOpenWithMenu()1526 void MainWindow::createOpenWithMenu() {
1527   if(currentIndex_.isValid()) {
1528     if(const auto file = proxyModel_->fileInfoFromIndex(currentIndex_)) {
1529       if(file->mimeType()) {
1530         // We want the "Open With..." submenu. It will be deleted alongside
1531         // fileMenu_ when openWithMenu hides (-> deleteOpenWithMenu)
1532         Fm::FileInfoList files;
1533         files.push_back(file);
1534         fileMenu_ = new Fm::FileMenu(files, file, Fm::FilePath());
1535         if(QMenu* menu = fileMenu_->openWithMenuAction()->menu()) {
1536           ui.openWithMenu->addActions(menu->actions());
1537         }
1538       }
1539     }
1540   }
1541 }
1542 
deleteOpenWithMenu()1543 void MainWindow::deleteOpenWithMenu() {
1544   if(fileMenu_) {
1545     fileMenu_->deleteLater();
1546   }
1547 }
1548 
showAndRaise()1549 void MainWindow::showAndRaise() {
1550     if(showFullScreen_) {
1551       showFullScreen();
1552       ui.actionFullScreen->setChecked(true);
1553     }
1554     else {
1555       show();
1556     }
1557     raise();
1558     activateWindow();
1559     QTimer::singleShot (100, this, [this]() { // steal the focus forcefully
1560       if(QWindow *win = windowHandle()){
1561         win->requestActivate();
1562       }
1563     });
1564 }
1565