viewmainpage.cpp 32 KB
Newer Older
1
2
/*
Gwenview: an image viewer
Aurélien Gâteau's avatar
Aurélien Gâteau committed
3
Copyright 2007 Aurélien Gâteau <agateau@kde.org>
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

*/
David Edmundson's avatar
David Edmundson committed
20
#include "viewmainpage.h"
21
#include "config-gwenview.h"
22
23

// Qt
24
#include <QCheckBox>
25
#include <QItemSelectionModel>
26
#include <QShortcut>
27
#include <QVBoxLayout>
Lukáš Tinkl's avatar
Lukáš Tinkl committed
28
#include <QMenu>
29

30
// KF
Aurélien Gâteau's avatar
Aurélien Gâteau committed
31
32
#include <KActionCollection>
#include <KActionCategory>
Lukáš Tinkl's avatar
Lukáš Tinkl committed
33
#include <KLocalizedString>
Aurélien Gâteau's avatar
Aurélien Gâteau committed
34
#include <KMessageBox>
35
#include <KModelIndexProxyMapper>
Aurélien Gâteau's avatar
Aurélien Gâteau committed
36
#include <KToggleAction>
37
#include <KSqueezedTextLabel>
38
39
40
#ifdef KF5Activities_FOUND
#include <KActivities/ResourceInstance>
#endif
Lukáš Tinkl's avatar
Lukáš Tinkl committed
41

42
// Local
43
#include "gwenview_app_debug.h"
Aurélien Gâteau's avatar
Aurélien Gâteau committed
44
#include "fileoperations.h"
Aurélien Gâteau's avatar
Aurélien Gâteau committed
45
#include <gvcore.h>
46
#include "splitter.h"
47
#include <lib/documentview/abstractdocumentviewadapter.h>
48
#include <lib/documentview/abstractrasterimageviewtool.h>
49
#include <lib/documentview/documentview.h>
50
#include <lib/documentview/documentviewcontainer.h>
51
#include <lib/documentview/documentviewcontroller.h>
52
#include <lib/documentview/documentviewsynchronizer.h>
53
#include <lib/fullscreenbar.h>
54
#include <lib/gvdebug.h>
55
#include <lib/gwenviewconfig.h>
56
#include <lib/mimetypeutils.h>
57
#include <lib/paintutils.h>
58
#include <lib/semanticinfo/sorteddirmodel.h>
59
#include <lib/slidecontainer.h>
60
#include <lib/slideshow.h>
61
#include <lib/statusbartoolbutton.h>
62
#include <lib/thumbnailview/thumbnailbarview.h>
63
#include <lib/zoomwidget.h>
64
#include <lib/zoommode.h>
65
#include <lib/stylesheetutils.h>
66

67
68
namespace Gwenview
{
69

70
71
72
73
#undef ENABLE_LOG
#undef LOG
//#define ENABLE_LOG
#ifdef ENABLE_LOG
Laurent Montel's avatar
Laurent Montel committed
74
#define LOG(x) qCDebug(GWENVIEW_APP_LOG) << x
75
76
77
78
#else
#define LOG(x) ;
#endif

79
const int ViewMainPage::MaxViewCount = 6;
80

81
82
83
/*
 * Layout of mThumbnailSplitter is:
 *
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
 * +-mThumbnailSplitter------------------------------------------------+
 * |+-mAdapterContainer-----------------------------------------------+|
 * ||+-mDocumentViewContainer----------------------------------------+||
 * |||+-DocumentView----------------++-DocumentView-----------------+|||
 * ||||                             ||                              ||||
 * ||||                             ||                              ||||
 * ||||                             ||                              ||||
 * ||||                             ||                              ||||
 * ||||                             ||                              ||||
 * ||||                             ||                              ||||
 * |||+-----------------------------++------------------------------+|||
 * ||+---------------------------------------------------------------+||
 * ||+-mToolContainer------------------------------------------------+||
 * |||                                                               |||
 * ||+---------------------------------------------------------------+||
 * |+-----------------------------------------------------------------+|
 * |===================================================================|
 * |+-mThumbnailBar---------------------------------------------------+|
 * ||                                                                 ||
 * ||                                                                 ||
104
105
 * |+-mStatusBarContainer---------------------------------------------+|
 * ||[mToggleSideBarButton][mToggleThumbnailBarButton]   [mZoomWidget]||
106
107
 * |+-----------------------------------------------------------------+|
 * +-------------------------------------------------------------------+
108
 */
Aurélien Gâteau's avatar
Aurélien Gâteau committed
109
110
struct ViewMainPagePrivate
{
111
    ViewMainPage* q;
112
113
    SlideShow* mSlideShow;
    KActionCollection* mActionCollection;
Aurélien Gâteau's avatar
Aurélien Gâteau committed
114
    GvCore* mGvCore;
115
    KModelIndexProxyMapper* mDirModelToBarModelProxyMapper;
116
117
118
119
120
121
122
123
124
125
126
127
128
129
    QSplitter *mThumbnailSplitter;
    QWidget* mAdapterContainer;
    DocumentViewController* mDocumentViewController;
    QList<DocumentView*> mDocumentViews;
    DocumentViewSynchronizer* mSynchronizer;
    QToolButton* mToggleSideBarButton;
    QToolButton* mToggleThumbnailBarButton;
    ZoomWidget* mZoomWidget;
    DocumentViewContainer* mDocumentViewContainer;
    SlideContainer* mToolContainer;
    QWidget* mStatusBarContainer;
    ThumbnailBarView* mThumbnailBar;
    KToggleAction* mToggleThumbnailBarAction;
    KToggleAction* mSynchronizeAction;
130
    QCheckBox* mSynchronizeCheckBox;
131
    KSqueezedTextLabel* mDocumentCountLabel;
132

133
134
135
    // Activity Resource events reporting needs to be above KPart,
    // in the shell itself, to avoid problems with other MDI applications
    // that use this KPart
136
#ifdef KF5Activities_FOUND
137
    QHash<DocumentView*, KActivities::ResourceInstance*> mActivityResources;
138
#endif
139

140
    bool mCompareMode;
141
    ZoomMode::Enum mZoomMode;
142

143
    void setupWidgets()
144
    {
145
146
147
148
149
        mToolContainer = new SlideContainer;
        mToolContainer->setAutoFillBackground(true);
        mToolContainer->setBackgroundRole(QPalette::Mid);
        //--
        mStatusBarContainer = new QWidget;
150
        mStatusBarContainer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
151
152
153
154
155
156
157
158
159
160
161
162
163
164
        mToggleSideBarButton = new StatusBarToolButton;
        mToggleThumbnailBarButton = new StatusBarToolButton;
        mZoomWidget = new ZoomWidget;
        mSynchronizeCheckBox = new QCheckBox(i18n("Synchronize"));
        mSynchronizeCheckBox->hide();
        mDocumentCountLabel = new KSqueezedTextLabel;
        mDocumentCountLabel->setAlignment(Qt::AlignCenter);
        mDocumentCountLabel->setTextElideMode(Qt::ElideRight);
        QMargins labelMargins = mDocumentCountLabel->contentsMargins();
        labelMargins.setLeft(15);
        labelMargins.setRight(15);
        mDocumentCountLabel->setContentsMargins(labelMargins);

        QHBoxLayout* statusBarContainerLayout = new QHBoxLayout(mStatusBarContainer);
Noah Davis's avatar
Noah Davis committed
165
166
167
168
169
        // Use toolbar-like margins and spacing
        int margins = q->style()->pixelMetric(QStyle::PM_ToolBarItemMargin)
                    + q->style()->pixelMetric(QStyle::PM_ToolBarFrameWidth);
        statusBarContainerLayout->setContentsMargins(margins, margins, margins, margins);
        statusBarContainerLayout->setSpacing(q->style()->pixelMetric(QStyle::PM_ToolBarItemSpacing));
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
        statusBarContainerLayout->addWidget(mToggleSideBarButton);
        statusBarContainerLayout->addWidget(mToggleThumbnailBarButton);
        statusBarContainerLayout->addStretch();
        statusBarContainerLayout->addWidget(mSynchronizeCheckBox);
        // Ensure document count label takes up all available space,
        // so its autohide feature works properly (stretch factor = 1)
        statusBarContainerLayout->addWidget(mDocumentCountLabel, 1);
        statusBarContainerLayout->addStretch();
        statusBarContainerLayout->addWidget(mZoomWidget);
        //--
        mAdapterContainer = new QWidget;

        QVBoxLayout* adapterContainerLayout = new QVBoxLayout(mAdapterContainer);
        adapterContainerLayout->setContentsMargins(0, 0, 0, 0);
        adapterContainerLayout->setSpacing(0);
        mDocumentViewContainer = new DocumentViewContainer;
        mDocumentViewContainer->setAutoFillBackground(true);
        mDocumentViewContainer->setBackgroundRole(QPalette::Base);
        adapterContainerLayout->addWidget(mDocumentViewContainer);
        adapterContainerLayout->addWidget(mToolContainer);
        //--
191
192
193
194
        mThumbnailBar = new ThumbnailBarView;
        ThumbnailBarItemDelegate* delegate = new ThumbnailBarItemDelegate(mThumbnailBar);
        mThumbnailBar->setItemDelegate(delegate);
        mThumbnailBar->setSelectionMode(QAbstractItemView::ExtendedSelection);
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
        //--
        Qt::Orientation orientation = GwenviewConfig::thumbnailBarOrientation();
        mThumbnailSplitter = new Splitter(orientation == Qt::Horizontal ? Qt::Vertical : Qt::Horizontal, q);
        mThumbnailBar->setOrientation(orientation);
        mThumbnailBar->setThumbnailAspectRatio(GwenviewConfig::thumbnailAspectRatio());
        mThumbnailBar->setRowCount(GwenviewConfig::thumbnailBarRowCount());
        mThumbnailSplitter->addWidget(mAdapterContainer);
        mThumbnailSplitter->addWidget(mThumbnailBar);
        mThumbnailSplitter->setSizes(GwenviewConfig::thumbnailSplitterSizes());
        // Show the thumbnail bar after setting the parent to avoid recreating
        // the native window and to avoid QTBUG-87345.
        mThumbnailBar->setVisible(GwenviewConfig::thumbnailBarIsVisible());

        QVBoxLayout* viewMainPageLayout = new QVBoxLayout(q);
        viewMainPageLayout->setContentsMargins(0, 0, 0, 0);
        viewMainPageLayout->setSpacing(0);
        viewMainPageLayout->addWidget(mThumbnailSplitter);
212
        viewMainPageLayout->addWidget(mStatusBarContainer);
213
214
215
216
217
        //--
        mDocumentViewController = new DocumentViewController(mActionCollection, q);
        mDocumentViewController->setZoomWidget(mZoomWidget);
        mDocumentViewController->setToolContainer(mToolContainer);
        mSynchronizer = new DocumentViewSynchronizer(&mDocumentViews, q);
218
219
220
221
    }

    void setupThumbnailBarStyleSheet()
    {
222
223
        QPalette pal = mGvCore->palette(GvCore::NormalViewPalette);
        mThumbnailBar->setPalette(pal);
224
        Qt::Orientation orientation = mThumbnailBar->orientation();
225
226
        QColor bgColor = pal.color(QPalette::Normal, QPalette::Base);
        QColor bgSelColor = pal.color(QPalette::Normal, QPalette::Highlight);
227
228
        QColor bgHovColor = pal.color(QPalette::Normal, QPalette::Highlight);
        
229
230
        // Avoid dark and bright colors
        bgColor.setHsv(bgColor.hue(), bgColor.saturation(), (127 + 3 * bgColor.value()) / 4);
231
232
233
        
        // Hover uses lighter/faded version of select color. Combine with bgColor to adapt to different backgrounds
        bgHovColor.setHsv(bgHovColor.hue(), (bgHovColor.saturation() / 2), ((bgHovColor.value() + bgColor.value()) / 2));
234
235
236
237
238
239

        QColor leftBorderColor = PaintUtils::adjustedHsv(bgColor, 0, 0, qMin(20, 255 - bgColor.value()));
        QColor rightBorderColor = PaintUtils::adjustedHsv(bgColor, 0, 0, -qMin(40, bgColor.value()));
        QColor borderSelColor = PaintUtils::adjustedHsv(bgSelColor, 0, 0, -qMin(60, bgSelColor.value()));

        QString itemCss =
240
            "QListView::item {"
241
242
243
244
245
            "	background-color: %1;"
            "	border-left: 1px solid %2;"
            "	border-right: 1px solid %3;"
            "}";
        itemCss = itemCss.arg(
246
247
248
                      StyleSheetUtils::gradient(orientation, bgColor, 46),
                      StyleSheetUtils::gradient(orientation, leftBorderColor, 36),
                      StyleSheetUtils::gradient(orientation, rightBorderColor, 26));
249
250
251
252
253
254
255
256

        QString itemSelCss =
            "QListView::item:selected {"
            "	background-color: %1;"
            "	border-left: 1px solid %2;"
            "	border-right: 1px solid %2;"
            "}";
        itemSelCss = itemSelCss.arg(
257
258
259
260
261
262
263
264
265
266
267
268
269
                         StyleSheetUtils::gradient(orientation, bgSelColor, 56),
                         StyleSheetUtils::rgba(borderSelColor));
        
        QString itemHovCss =
            "QListView::item:hover:!selected {"
            "  background-color: %1;"
            "  border-left: 1px solid %2;"
            "  border-right: 1px solid %3;"
            "}";
        itemHovCss = itemHovCss.arg(
                         StyleSheetUtils::gradient(orientation, bgHovColor, 56),
                         StyleSheetUtils::rgba(leftBorderColor),
                         StyleSheetUtils::rgba(rightBorderColor));
270

271
        QString css = itemCss + itemSelCss + itemHovCss;
272
273
274
275
276
277
278
279
280
281
282
283
        if (orientation == Qt::Vertical) {
            css.replace("left", "top").replace("right", "bottom");
        }

        mThumbnailBar->setStyleSheet(css);
    }

    DocumentView* createDocumentView()
    {
        DocumentView* view = mDocumentViewContainer->createView();

        // Connect context menu
284
        // If you need to connect another view signal, make sure it is disconnected in deleteDocumentView
Laurent Montel's avatar
Laurent Montel committed
285
286
287
288
289
        QObject::connect(view, &DocumentView::contextMenuRequested, q, &ViewMainPage::showContextMenu);

        QObject::connect(view, &DocumentView::completed, q, &ViewMainPage::completed);
        QObject::connect(view, &DocumentView::previousImageRequested, q, &ViewMainPage::previousImageRequested);
        QObject::connect(view, &DocumentView::nextImageRequested, q, &ViewMainPage::nextImageRequested);
290
291
        QObject::connect(view, &DocumentView::openUrlRequested, q, &ViewMainPage::openUrlRequested);
        QObject::connect(view, &DocumentView::openDirUrlRequested, q, &ViewMainPage::openDirUrlRequested);
Laurent Montel's avatar
Laurent Montel committed
292
293
294
295
296
297
298
        QObject::connect(view, &DocumentView::captionUpdateRequested, q, &ViewMainPage::captionUpdateRequested);
        QObject::connect(view, &DocumentView::toggleFullScreenRequested, q, &ViewMainPage::toggleFullScreenRequested);
        QObject::connect(view, &DocumentView::focused, q, &ViewMainPage::slotViewFocused);
        QObject::connect(view, &DocumentView::hudTrashClicked, q, &ViewMainPage::trashView);
        QObject::connect(view, &DocumentView::hudDeselectClicked, q, &ViewMainPage::deselectView);

        QObject::connect(view, &DocumentView::videoFinished, mSlideShow, &SlideShow::resumeAndGoToNextUrl);
299
300

        mDocumentViews << view;
301
#ifdef KF5Activities_FOUND
302
        mActivityResources.insert(view, new KActivities::ResourceInstance(q->window()->winId(), view));
303
#endif
304
305
306
307

        return view;
    }

308
    void deleteDocumentView(DocumentView* view)
309
    {
310
        if (mDocumentViewController->view() == view) {
311
            mDocumentViewController->setView(nullptr);
312
        }
313
314
315
316
317

        // Make sure we do not get notified about this view while it is going away.
        // mDocumentViewController->deleteView() animates the view deletion so
        // the view still exists for a short while when we come back to the
        // event loop)
318
319
        QObject::disconnect(view, nullptr, q, nullptr);
        QObject::disconnect(view, nullptr, mSlideShow, nullptr);
320

321
        mDocumentViews.removeOne(view);
322
#ifdef KF5Activities_FOUND
323
        mActivityResources.remove(view);
324
#endif
325
        mDocumentViewContainer->deleteView(view);
326
327
328
329
330
331
332
333
334
    }

    void saveSplitterConfig()
    {
        if (mThumbnailBar->isVisible()) {
            GwenviewConfig::setThumbnailSplitterSizes(mThumbnailSplitter->sizes());
        }
    }

Aurélien Gâteau's avatar
Aurélien Gâteau committed
335
336
    DocumentView* currentView() const
    {
337
338
339
340
341
342
343
344
345
346
347
        return mDocumentViewController->view();
    }

    void setCurrentView(DocumentView* view)
    {
        DocumentView* oldView = currentView();
        if (view == oldView) {
            return;
        }
        if (oldView) {
            oldView->setCurrent(false);
348
#ifdef KF5Activities_FOUND
349
            Q_ASSERT(mActivityResources.contains(oldView));
350
            mActivityResources.value(oldView)->notifyFocusedOut();
351
#endif
352
353
354
355
356
357
        }
        view->setCurrent(true);
        mDocumentViewController->setView(view);
        mSynchronizer->setCurrentView(view);

        QModelIndex index = indexForView(view);
358
359
360
361
362
        if (index.isValid()) {
            // Index may be invalid when Gwenview is started as
            // `gwenview /foo/image.png` because in this situation it loads image.png
            // *before* listing /foo (because it matters less to the user)
            mThumbnailBar->selectionModel()->setCurrentIndex(index, QItemSelectionModel::Current);
363
        }
364
#ifdef KF5Activities_FOUND
365
        Q_ASSERT(mActivityResources.contains(view));
366
        mActivityResources.value(view)->notifyFocusedIn();
367
#endif
368
369
        QObject::connect(view, &DocumentView::currentToolChanged,
                         q, &ViewMainPage::updateFocus);
370
371
    }

Aurélien Gâteau's avatar
Aurélien Gâteau committed
372
373
    QModelIndex indexForView(DocumentView* view) const
    {
David Edmundson's avatar
David Edmundson committed
374
        QUrl url = view->url();
375
        if (!url.isValid()) {
Laurent Montel's avatar
Laurent Montel committed
376
            qCWarning(GWENVIEW_APP_LOG) << "View does not display any document!";
377
378
379
            return QModelIndex();
        }

380
381
382
383
384
385
386
387
388
389
        SortedDirModel* dirModel = mGvCore->sortedDirModel();
        QModelIndex srcIndex = dirModel->indexForUrl(url);
        if (!mDirModelToBarModelProxyMapper) {
            // Delay the initialization of the mapper to its first use because
            // mThumbnailBar->model() is not set after ViewMainPage ctor is
            // done.
            const_cast<ViewMainPagePrivate*>(this)->mDirModelToBarModelProxyMapper = new KModelIndexProxyMapper(dirModel, mThumbnailBar->model(), q);
        }
        QModelIndex index = mDirModelToBarModelProxyMapper->mapLeftToRight(srcIndex);
        return index;
390
    }
Aurélien Gâteau's avatar
Aurélien Gâteau committed
391
392
393

    void applyPalette(bool fullScreenMode)
    {
394
        mDocumentViewContainer->applyPalette(mGvCore->palette(fullScreenMode ? GvCore::FullScreenViewPalette : GvCore::NormalViewPalette));
Aurélien Gâteau's avatar
Aurélien Gâteau committed
395
396
        setupThumbnailBarStyleSheet();
    }
397
398
399
400
401
402
403
404

    void updateDocumentCountLabel()
    {
        const int current = mThumbnailBar->currentIndex().row() + 1;  // zero-based
        const int total = mThumbnailBar->model()->rowCount();
        const QString text = i18nc("@info:status %1 current document index, %2 total documents", "%1 of %2", current, total);
        mDocumentCountLabel->setText(text);
    }
405
406
};

Aurélien Gâteau's avatar
Aurélien Gâteau committed
407
ViewMainPage::ViewMainPage(QWidget* parent, SlideShow* slideShow, KActionCollection* actionCollection, GvCore* gvCore)
408
: QWidget(parent)
409
, d(new ViewMainPagePrivate)
410
{
411
    d->q = this;
412
    d->mDirModelToBarModelProxyMapper = nullptr; // Initialized later
413
414
    d->mSlideShow = slideShow;
    d->mActionCollection = actionCollection;
Aurélien Gâteau's avatar
Aurélien Gâteau committed
415
    d->mGvCore = gvCore;
416
417
    d->mCompareMode = false;

418
419
    QShortcut* enterKeyShortcut = new QShortcut(Qt::Key_Return, this);
    connect(enterKeyShortcut, &QShortcut::activated, this, &ViewMainPage::slotEnterPressed);
420

421
    d->setupWidgets();
422
423
424

    KActionCategory* view = new KActionCategory(i18nc("@title actions category - means actions changing smth in interface", "View"), actionCollection);

425
    d->mToggleThumbnailBarAction = view->add<KToggleAction>(QStringLiteral("toggle_thumbnailbar"));
426
    d->mToggleThumbnailBarAction->setText(i18n("Thumbnail Bar"));
Laurent Montel's avatar
Laurent Montel committed
427
    d->mToggleThumbnailBarAction->setIcon(QIcon::fromTheme(QStringLiteral("folder-image")));
428
    actionCollection->setDefaultShortcut(d->mToggleThumbnailBarAction, Qt::CTRL | Qt::Key_B);
Laurent Montel's avatar
Laurent Montel committed
429
    connect(d->mToggleThumbnailBarAction, &KToggleAction::triggered, this, &ViewMainPage::setThumbnailBarVisibility);
430
431
432
433
    d->mToggleThumbnailBarButton->setDefaultAction(d->mToggleThumbnailBarAction);

    d->mSynchronizeAction = view->add<KToggleAction>("synchronize_views");
    d->mSynchronizeAction->setText(i18n("Synchronize"));
434
    actionCollection->setDefaultShortcut(d->mSynchronizeAction, Qt::CTRL | Qt::Key_Y);
Laurent Montel's avatar
Laurent Montel committed
435
436
    connect(d->mSynchronizeAction, &QAction::toggled,
            d->mSynchronizer, &DocumentViewSynchronizer::setActive);
437
    // Ensure mSynchronizeAction and mSynchronizeCheckBox are in sync
Laurent Montel's avatar
Laurent Montel committed
438
439
440
441
    connect(d->mSynchronizeAction, &QAction::toggled,
            d->mSynchronizeCheckBox, &QAbstractButton::setChecked);
    connect(d->mSynchronizeCheckBox, &QAbstractButton::toggled,
            d->mSynchronizeAction, &QAction::setChecked);
442

443
444
445
446
447
448
    // Connections for the document count
    connect(d->mThumbnailBar, &ThumbnailBarView::rowsInsertedSignal,
            this, &ViewMainPage::slotDirModelItemsAddedOrRemoved);
    connect(d->mThumbnailBar, &ThumbnailBarView::rowsRemovedSignal,
            this, &ViewMainPage::slotDirModelItemsAddedOrRemoved);

449
    installEventFilter(this);
450
451
}

452
ViewMainPage::~ViewMainPage()
453
454
{
    delete d;
455
456
}

457
void ViewMainPage::loadConfig()
458
{
459
    d->applyPalette(window()->isFullScreen());
Aurélien Gâteau's avatar
Aurélien Gâteau committed
460

461
    // FIXME: Not symmetric with saveConfig(). Check if it matters.
462
    for (DocumentView * view : qAsConst(d->mDocumentViews)) {
463
464
465
466
467
468
469
        view->loadAdapterConfig();
    }

    Qt::Orientation orientation = GwenviewConfig::thumbnailBarOrientation();
    d->mThumbnailSplitter->setOrientation(orientation == Qt::Horizontal ? Qt::Vertical : Qt::Horizontal);
    d->mThumbnailBar->setOrientation(orientation);
    d->setupThumbnailBarStyleSheet();
470
471
    d->mThumbnailBar->setVisible(GwenviewConfig::thumbnailBarIsVisible());
    d->mToggleThumbnailBarAction->setChecked(GwenviewConfig::thumbnailBarIsVisible());
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490

    int oldRowCount = d->mThumbnailBar->rowCount();
    int newRowCount = GwenviewConfig::thumbnailBarRowCount();
    if (oldRowCount != newRowCount) {
        d->mThumbnailBar->setUpdatesEnabled(false);
        int gridSize = d->mThumbnailBar->gridSize().width();

        d->mThumbnailBar->setRowCount(newRowCount);

        // Adjust splitter to ensure thumbnail size remains the same
        int delta = (newRowCount - oldRowCount) * gridSize;
        QList<int> sizes = d->mThumbnailSplitter->sizes();
        Q_ASSERT(sizes.count() == 2);
        sizes[0] -= delta;
        sizes[1] += delta;
        d->mThumbnailSplitter->setSizes(sizes);

        d->mThumbnailBar->setUpdatesEnabled(true);
    }
491

492
    d->mZoomMode = GwenviewConfig::zoomMode();
493
494
}

495
void ViewMainPage::saveConfig()
496
497
498
{
    d->saveSplitterConfig();
    GwenviewConfig::setThumbnailBarIsVisible(d->mToggleThumbnailBarAction->isChecked());
499
500
}

501
void ViewMainPage::setThumbnailBarVisibility(bool visible)
502
503
504
{
    d->saveSplitterConfig();
    d->mThumbnailBar->setVisible(visible);
505
506
}

507
int ViewMainPage::statusBarHeight() const
508
509
{
    return d->mStatusBarContainer->height();
510
511
}

Valerio Pilo's avatar
Valerio Pilo committed
512
513
514
515
516
void ViewMainPage::setStatusBarVisible(bool visible)
{
    d->mStatusBarContainer->setVisible(visible);
}

517
void ViewMainPage::setFullScreenMode(bool fullScreenMode)
518
519
{
    if (fullScreenMode) {
520
        d->mThumbnailBar->setVisible(false);
521
    } else {
522
        d->mThumbnailBar->setVisible(d->mToggleThumbnailBarAction->isChecked());
523
    }
Aurélien Gâteau's avatar
Aurélien Gâteau committed
524
    d->applyPalette(fullScreenMode);
525
    d->mToggleThumbnailBarAction->setEnabled(!fullScreenMode);
526
527
}

528
ThumbnailBarView* ViewMainPage::thumbnailBar() const
529
530
{
    return d->mThumbnailBar;
Aurélien Gâteau's avatar
Aurélien Gâteau committed
531
532
}

David Edmundson's avatar
David Edmundson committed
533
inline void addActionToMenu(QMenu* menu, KActionCollection* actionCollection, const char* name)
534
535
536
537
538
{
    QAction* action = actionCollection->action(name);
    if (action) {
        menu->addAction(action);
    }
Aurélien Gâteau's avatar
Aurélien Gâteau committed
539
540
}

541
void ViewMainPage::showContextMenu()
542
{
David Edmundson's avatar
David Edmundson committed
543
    QMenu menu(this);
544
545
546
547
548
549
550
551
552
553
    addActionToMenu(&menu, d->mActionCollection, "fullscreen");
    menu.addSeparator();
    addActionToMenu(&menu, d->mActionCollection, "go_previous");
    addActionToMenu(&menu, d->mActionCollection, "go_next");
    if (d->currentView()->canZoom()) {
        menu.addSeparator();
        addActionToMenu(&menu, d->mActionCollection, "view_actual_size");
        addActionToMenu(&menu, d->mActionCollection, "view_zoom_to_fit");
        addActionToMenu(&menu, d->mActionCollection, "view_zoom_in");
        addActionToMenu(&menu, d->mActionCollection, "view_zoom_out");
554
        addActionToMenu(&menu, d->mActionCollection, "view_toggle_birdeyeview");
555
556
557
558
559
560
561
562
563
564
565
566
    }
    if (d->mCompareMode) {
        menu.addSeparator();
        addActionToMenu(&menu, d->mActionCollection, "synchronize_views");
    }

    menu.addSeparator();
    addActionToMenu(&menu, d->mActionCollection, "file_copy_to");
    addActionToMenu(&menu, d->mActionCollection, "file_move_to");
    addActionToMenu(&menu, d->mActionCollection, "file_link_to");
    menu.addSeparator();
    addActionToMenu(&menu, d->mActionCollection, "file_open_with");
567
568
    addActionToMenu(&menu, d->mActionCollection, "file_open_containing_folder");
    
569
    menu.exec(QCursor::pos());
Aurélien Gâteau's avatar
Aurélien Gâteau committed
570
571
}

572
QSize ViewMainPage::sizeHint() const
573
574
{
    return QSize(400, 300);
575
576
}

577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
QSize ViewMainPage::minimumSizeHint() const {
    if (!layout()) {
        return QSize();
    }

    QSize minimumSize = layout()->minimumSize();
    if (window()->isFullScreen()) {
        // Check minimum width of the overlay fullscreen bar
        // since there is no layout link which could do this
        const FullScreenBar* fullScreenBar = findChild<FullScreenBar*>();
        if (fullScreenBar && fullScreenBar->layout()) {
            const int fullScreenBarWidth = fullScreenBar->layout()->minimumSize().width();
            if (fullScreenBarWidth > minimumSize.width()) {
                minimumSize.setWidth(fullScreenBarWidth);
            }
        }
    }
    return minimumSize;
}

David Edmundson's avatar
David Edmundson committed
597
QUrl ViewMainPage::url() const
598
{
David Edmundson's avatar
David Edmundson committed
599
    GV_RETURN_VALUE_IF_FAIL(d->currentView(), QUrl());
600
    return d->currentView()->url();
601
602
}

603
Document::Ptr ViewMainPage::currentDocument() const
604
605
606
607
608
{
    if (!d->currentView()) {
        LOG("!d->documentView()");
        return Document::Ptr();
    }
609

610
    return d->currentView()->document();
611
612
}

613
bool ViewMainPage::isEmpty() const
614
{
615
    return !currentDocument();
616
617
}

618
RasterImageView* ViewMainPage::imageView() const
619
620
{
    if (!d->currentView()) {
621
        return nullptr;
622
623
    }
    return d->currentView()->imageView();
624
625
}

626
DocumentView* ViewMainPage::documentView() const
627
628
{
    return d->currentView();
629
630
}

David Edmundson's avatar
David Edmundson committed
631
void ViewMainPage::openUrl(const QUrl &url)
632
{
David Edmundson's avatar
David Edmundson committed
633
    openUrls(QList<QUrl>() << url, url);
634
635
}

David Edmundson's avatar
David Edmundson committed
636
void ViewMainPage::openUrls(const QList<QUrl>& allUrls, const QUrl &currentUrl)
637
{
638
    DocumentView::Setup setup;
639

640
641
642
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
    QSet<QUrl> urls(allUrls.begin(), allUrls.end());
#else
David Edmundson's avatar
David Edmundson committed
643
    QSet<QUrl> urls = allUrls.toSet();
644
#endif
645
646
    d->mCompareMode = urls.count() > 1;

David Edmundson's avatar
David Edmundson committed
647
    typedef QMap<QUrl, DocumentView*> ViewForUrlMap;
648
649
    ViewForUrlMap viewForUrlMap;

650
651
652
    if (!d->mDocumentViews.isEmpty()) {
        d->mDocumentViewContainer->updateSetup(d->mDocumentViews.last());
    }
653
    if (d->mDocumentViews.isEmpty() || d->mZoomMode == ZoomMode::Autofit) {
654
        setup.valid = true;
655
        setup.zoomToFit = true;
656
657
    } else {
        setup = d->mDocumentViews.last()->setup();
658
    }
659
660
    // Destroy views which show urls we don't care about, remove from "urls" the
    // urls which already have a view.
661
    for (DocumentView * view : qAsConst(d->mDocumentViews)) {
David Edmundson's avatar
David Edmundson committed
662
        QUrl url = view->url();
663
664
665
        if (urls.contains(url)) {
            // view displays an url we must display, keep it
            urls.remove(url);
666
            viewForUrlMap.insert(url, view);
667
668
        } else {
            // view url is not interesting, drop it
669
            d->deleteDocumentView(view);
670
671
672
673
        }
    }

    // Create view for remaining urls
674
    for (const QUrl &url : qAsConst(urls)) {
675
        if (d->mDocumentViews.count() >= MaxViewCount) {
Laurent Montel's avatar
Laurent Montel committed
676
            qCWarning(GWENVIEW_APP_LOG) << "Too many documents to show";
677
678
679
            break;
        }
        DocumentView* view = d->createDocumentView();
680
        viewForUrlMap.insert(url, view);
681
682
    }

683
684
    // Set sortKey to match url order
    int sortKey = 0;
685
    for (const QUrl &url : allUrls) {
686
687
688
689
        viewForUrlMap[url]->setSortKey(sortKey);
        ++sortKey;
    }

690
691
692
693
694
    d->mDocumentViewContainer->updateLayout();

    // Load urls for new views. Do it only now because the view must have the
    // correct size before it starts loading its url. Do not do it later because
    // view->url() needs to be set for the next loop.
695
    ViewForUrlMap::ConstIterator
696
697
        it = viewForUrlMap.constBegin(),
        end = viewForUrlMap.constEnd();
698
    for (; it != end; ++it) {
David Edmundson's avatar
David Edmundson committed
699
        QUrl url = it.key();
700
        DocumentView* view = it.value();
701
        DocumentView::Setup savedSetup = d->mDocumentViewContainer->savedSetup(url);
702
        view->openUrl(url, d->mZoomMode == ZoomMode::Individual && savedSetup.valid ? savedSetup : setup);
703
#ifdef KF5Activities_FOUND
704
705
706
707
        if (GwenviewConfig::historyEnabled()) {
            d->mActivityResources.value(view)->setUri(url);
            d->mActivityResources.value(view)->setMimetype(MimeTypeUtils::urlMimeType(url));
        }
708
#endif
709
710
711
    }

    // Init views
712
    for (DocumentView * view : qAsConst(d->mDocumentViews)) {
713
714
715
716
717
718
719
720
721
722
723
724
725
726
        view->setCompareMode(d->mCompareMode);
        if (view->url() == currentUrl) {
            d->setCurrentView(view);
        } else {
            view->setCurrent(false);
        }
    }

    d->mSynchronizeCheckBox->setVisible(d->mCompareMode);
    if (d->mCompareMode) {
        d->mSynchronizer->setActive(d->mSynchronizeCheckBox->isChecked());
    } else {
        d->mSynchronizer->setActive(false);
    }
727
728
729

    d->updateDocumentCountLabel();
    d->mDocumentCountLabel->setVisible(!d->mCompareMode);
730
731
}

732
void ViewMainPage::reload()
733
{
734
735
736
737
738
    DocumentView *view = d->currentView();
    if (!view) {
        return;
    }
    Document::Ptr doc = view->document();
739
    if (!doc) {
Laurent Montel's avatar
Laurent Montel committed
740
        qCWarning(GWENVIEW_APP_LOG) << "!doc";
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
        return;
    }
    if (doc->isModified()) {
        KGuiItem cont = KStandardGuiItem::cont();
        cont.setText(i18nc("@action:button", "Discard Changes and Reload"));
        int answer = KMessageBox::warningContinueCancel(this,
                     i18nc("@info", "This image has been modified. Reloading it will discard all your changes."),
                     QString() /* caption */,
                     cont);
        if (answer != KMessageBox::Continue) {
            return;
        }
    }
    doc->reload();
    // Call openUrl again because DocumentView may need to switch to a new
    // adapter (for example because document was broken and it is not anymore)
757
    d->currentView()->openUrl(doc->url(), d->currentView()->setup());
Aurélien Gâteau's avatar
Aurélien Gâteau committed
758
759
}

760
void ViewMainPage::reset()
761
{
762
    d->mDocumentViewController->reset();
763
764
    d->mDocumentViewContainer->reset();
    d->mDocumentViews.clear();
765
766
}

767
void ViewMainPage::slotViewFocused(DocumentView* view)
768
769
{
    d->setCurrentView(view);
770
771
}

772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
void ViewMainPage::slotEnterPressed()
{
    DocumentView *view = d->currentView();
    if (view) {
        AbstractRasterImageViewTool *tool = view->currentTool();
        if (tool) {
            QKeyEvent event(QEvent::KeyPress, Qt::Key_Return, Qt::NoModifier);
            tool->keyPressEvent(&event);
            if (event.isAccepted()) {
                return;
            }
        }
    }
    emit goToBrowseModeRequested();
}

788
bool ViewMainPage::eventFilter(QObject* watched, QEvent* event)
789
{
790
    if (event->type() == QEvent::ShortcutOverride) {
791
792
        const int key = static_cast<QKeyEvent*>(event)->key();
        if (key == Qt::Key_Space || key == Qt::Key_Escape) {
793
794
795
796
            const DocumentView* view = d->currentView();
            if (view) {
                AbstractRasterImageViewTool* tool = view->currentTool();
                if (tool) {
797
                    QKeyEvent toolKeyEvent(QEvent::KeyPress, key, Qt::NoModifier);
798
799
800
801
802
                    tool->keyPressEvent(&toolKeyEvent);
                    if (toolKeyEvent.isAccepted()) {
                        event->accept();
                    }
                }
803
804
            }
        }
805
806
807
808
809
        // Leave fullscreen when viewing an image
        if (window()->isFullScreen() && key == Qt::Key_Escape) {
            d->mActionCollection->action("leave_fullscreen")->trigger();
            event->accept();
        }
810
    }
811
812
    
    return QWidget::eventFilter(watched, event);
813
814
}

815
void ViewMainPage::trashView(DocumentView* view)
816
{
David Edmundson's avatar
David Edmundson committed
817
    QUrl url = view->url();
818
    deselectView(view);
David Edmundson's avatar
David Edmundson committed
819
    FileOperations::trash(QList<QUrl>() << url, this);
820
821
}

822
void ViewMainPage::deselectView(DocumentView* view)
823
{
824
    DocumentView* newCurrentView = nullptr;
825
826
827
    if (view == d->currentView()) {
        // We need to find a new view to set as current
        int idx = d->mDocumentViews.indexOf(view);
Aurélien Gâteau's avatar
Aurélien Gâteau committed
828
829
830
831
832
        if (idx + 1 < d->mDocumentViews.count()) {
            newCurrentView = d->mDocumentViews.at(idx + 1);
        } else if (idx > 0) {
            newCurrentView = d->mDocumentViews.at(idx - 1);
        } else {
833
            GV_WARN_AND_RETURN("No view found to set as current");
834
835
836
837
838
839
840
841
842
843
        }
    }

    QModelIndex index = d->indexForView(view);
    QItemSelectionModel* selectionModel = d->mThumbnailBar->selectionModel();
    selectionModel->select(index, QItemSelectionModel::Deselect);

    if (newCurrentView) {
        d->setCurrentView(newCurrentView);
    }
844
845
}

846
QToolButton* ViewMainPage::toggleSideBarButton() const
847
848
{
    return d->mToggleSideBarButton;
849
850
}

851
void ViewMainPage::showMessageWidget(QGraphicsWidget* widget, Qt::Alignment align)
852
{
853
    d->mDocumentViewContainer->showMessageWidget(widget, align);
854
855
}

856
857
858
859
860
861
862
void ViewMainPage::updateFocus(const AbstractRasterImageViewTool* tool)
{
    if (!tool) {
        d->mDocumentViewContainer->setFocus();
    }
}

863
864
865
866
867
void ViewMainPage::slotDirModelItemsAddedOrRemoved()
{
    d->updateDocumentCountLabel();
}

868
} // namespace