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