contextmanager.cpp 11.6 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 "contextmanager.h"
21

22
// Qt
23
#include <QItemSelectionModel>
24
#include <QTimer>
25
#include <QUndoGroup>
26

27
// KF
28
#include <KDirLister>
29
#include <kio_version.h>
30
#include <KProtocolManager>
Aurélien Gâteau's avatar
Aurélien Gâteau committed
31

32
// Local
33
#include <lib/document/documentfactory.h>
34
#include <lib/gvdebug.h>
35
#include <lib/gwenviewconfig.h>
Aurélien Gâteau's avatar
Aurélien Gâteau committed
36
#include <lib/semanticinfo/sorteddirmodel.h>
37

38 39
namespace Gwenview
{
40

Aurélien Gâteau's avatar
Aurélien Gâteau committed
41 42
struct ContextManagerPrivate
{
43 44
    SortedDirModel* mDirModel;
    QItemSelectionModel* mSelectionModel;
David Edmundson's avatar
David Edmundson committed
45 46
    QUrl mCurrentDirUrl;
    QUrl mCurrentUrl;
47

David Edmundson's avatar
David Edmundson committed
48
    QUrl mUrlToSelect;
rkflx's avatar
rkflx committed
49
    QUrl mTargetDirUrl;
50

51
    bool mSelectedFileItemListNeedsUpdate;
52 53
    using Signal = void (ContextManager::*)();
    QVector<Signal> mQueuedSignals;
54 55
    KFileItemList mSelectedFileItemList;

56
    bool mDirListerFinished = false;
57 58
    QTimer* mQueuedSignalsTimer;

59
    void queueSignal(Signal signal)
60
    {
61 62 63
        if (!mQueuedSignals.contains(signal)) {
            mQueuedSignals << signal;
        }
64 65 66 67 68 69 70 71 72
        mQueuedSignalsTimer->start();
    }

    void updateSelectedFileItemList()
    {
        if (!mSelectedFileItemListNeedsUpdate) {
            return;
        }
        mSelectedFileItemList.clear();
73 74
        const QItemSelection selection = mSelectionModel->selection();
        for (const QModelIndex & index : selection.indexes()) {
75
            mSelectedFileItemList << mDirModel->itemForIndex(index);
76 77 78 79 80 81
        }

        // At least add current url if it's valid (it may not be in
        // the list if we are viewing a non-browsable url, for example
        // using http protocol)
        if (mSelectedFileItemList.isEmpty() && mCurrentUrl.isValid()) {
Lukáš Tinkl's avatar
Lukáš Tinkl committed
82
            KFileItem item(mCurrentUrl);
83 84 85 86 87
            mSelectedFileItemList << item;
        }

        mSelectedFileItemListNeedsUpdate = false;
    }
88 89
};

90
ContextManager::ContextManager(SortedDirModel* dirModel, QObject* parent)
91
: QObject(parent)
92 93
, d(new ContextManagerPrivate)
{
94 95 96
    d->mQueuedSignalsTimer = new QTimer(this);
    d->mQueuedSignalsTimer->setInterval(100);
    d->mQueuedSignalsTimer->setSingleShot(true);
Laurent Montel's avatar
Laurent Montel committed
97
    connect(d->mQueuedSignalsTimer, &QTimer::timeout, this, &ContextManager::emitQueuedSignals);
98 99

    d->mDirModel = dirModel;
Laurent Montel's avatar
Laurent Montel committed
100
    connect(d->mDirModel, &SortedDirModel::dataChanged, this, &ContextManager::slotDirModelDataChanged);
101

102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
    /* HACK! In extended-selection mode, when the current index is removed,
     * QItemSelectionModel selects the previous index if there is one, if not it
     * selects the next index. This is not what we want: when the user removes
     * an image, he expects to go to the next one, not the previous one.
     *
     * To overcome this, we must connect to the mDirModel.rowsAboutToBeRemoved()
     * signal *before* QItemSelectionModel connects to it, so that our slot is
     * called before QItemSelectionModel slot. This allows us to pick a new
     * current index ourself, leaving QItemSelectionModel slot with nothing to
     * do.
     *
     * This is the reason ContextManager creates a QItemSelectionModel itself:
     * doing so ensures QItemSelectionModel cannot be connected to the
     * mDirModel.rowsAboutToBeRemoved() signal before us.
     */
Laurent Montel's avatar
Laurent Montel committed
117
    connect(d->mDirModel, &SortedDirModel::rowsAboutToBeRemoved, this, &ContextManager::slotRowsAboutToBeRemoved);
118

Laurent Montel's avatar
Laurent Montel committed
119
    connect(d->mDirModel, &SortedDirModel::rowsInserted, this, &ContextManager::slotRowsInserted);
120

121
#if KIO_VERSION < QT_VERSION_CHECK(5, 80, 0)
David Edmundson's avatar
David Edmundson committed
122 123
    connect(d->mDirModel->dirLister(), SIGNAL(redirection(QUrl)),
            SLOT(slotDirListerRedirection(QUrl)));
124 125 126 127 128
#else
    connect(d->mDirModel->dirLister(), QOverload<const QUrl &, const QUrl &>::of(&KDirLister::redirection), this, [this](const QUrl &, const QUrl &newUrl) {
        slotDirListerRedirection(newUrl);
    });
#endif
129

130
    connect(d->mDirModel->dirLister(), QOverload<>::of(&KDirLister::completed), this, &ContextManager::slotDirListerCompleted);
131

132 133
    d->mSelectionModel = new QItemSelectionModel(d->mDirModel);

Laurent Montel's avatar
Laurent Montel committed
134 135
    connect(d->mSelectionModel, &QItemSelectionModel::selectionChanged, this, &ContextManager::slotSelectionChanged);
    connect(d->mSelectionModel, &QItemSelectionModel::currentChanged, this, &ContextManager::slotCurrentChanged);
136 137

    d->mSelectedFileItemListNeedsUpdate = false;
138

Laurent Montel's avatar
Laurent Montel committed
139
    connect(DocumentFactory::instance(), &DocumentFactory::readyForDirListerStart, this, [this](const QUrl &urlReady) {
140 141
        setCurrentDirUrl(urlReady.adjusted(QUrl::RemoveFilename));
    });
142
}
143

144 145 146
ContextManager::~ContextManager()
{
    delete d;
147 148
}

149 150
void ContextManager::loadConfig()
{
rkflx's avatar
rkflx committed
151
    setTargetDirUrl(QUrl(GwenviewConfig::lastTargetDir()));
152 153 154 155
}

void ContextManager::saveConfig() const
{
rkflx's avatar
rkflx committed
156
    GwenviewConfig::setLastTargetDir(targetDirUrl().toString());
157 158
}

159 160 161 162 163
QItemSelectionModel* ContextManager::selectionModel() const
{
    return d->mSelectionModel;
}

David Edmundson's avatar
David Edmundson committed
164
void ContextManager::setCurrentUrl(const QUrl &currentUrl)
165 166 167 168
{
    if (d->mCurrentUrl == currentUrl) {
        return;
    }
169

170
    d->mCurrentUrl = currentUrl;
171 172 173 174 175 176
    if (!d->mCurrentUrl.isEmpty()) {
        Document::Ptr doc = DocumentFactory::instance()->load(currentUrl);
        QUndoGroup* undoGroup = DocumentFactory::instance()->undoGroup();
        undoGroup->addStack(doc->undoStack());
        undoGroup->setActiveStack(doc->undoStack());
    }
177

178
    d->mSelectedFileItemListNeedsUpdate = true;
Alexander Volkov's avatar
Alexander Volkov committed
179
    emit currentUrlChanged(currentUrl);
Aurélien Gâteau's avatar
Aurélien Gâteau committed
180 181
}

182 183 184 185
KFileItemList ContextManager::selectedFileItemList() const
{
    d->updateSelectedFileItemList();
    return d->mSelectedFileItemList;
186 187
}

188
void ContextManager::setCurrentDirUrl(const QUrl &_url)
189
{
190
    const QUrl url = _url.adjusted(QUrl::StripTrailingSlash);
David Edmundson's avatar
David Edmundson committed
191
    if (url == d->mCurrentDirUrl) {
192 193
        return;
    }
194 195 196

    if (url.isValid() && KProtocolManager::supportsListing(url)) {
        d->mCurrentDirUrl = url;
197
        d->mDirModel->dirLister()->openUrl(url);
198 199 200
        d->mDirListerFinished = false;
    } else {
        d->mCurrentDirUrl.clear();
Alexander Volkov's avatar
Alexander Volkov committed
201
        emit d->mDirModel->dirLister()->clear();
202
    }
203
    emit currentDirUrlChanged(d->mCurrentDirUrl);
204 205
}

David Edmundson's avatar
David Edmundson committed
206
QUrl ContextManager::currentDirUrl() const
207 208
{
    return d->mCurrentDirUrl;
209 210
}

David Edmundson's avatar
David Edmundson committed
211
QUrl ContextManager::currentUrl() const
212 213
{
    return d->mCurrentUrl;
214 215
}

216 217 218
SortedDirModel* ContextManager::dirModel() const
{
    return d->mDirModel;
219 220
}

221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245
void ContextManager::slotDirModelDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight)
{
    // Data change can happen in the following cases:
    // - items have been renamed
    // - item bytes have been modified
    // - item meta info has been retrieved or modified
    //
    // If a selected item is affected, schedule emission of a
    // selectionDataChanged() signal. Don't emit it directly to avoid spamming
    // the context items in case of a mass change.
    QModelIndexList selectionList = d->mSelectionModel->selectedIndexes();
    if (selectionList.isEmpty()) {
        return;
    }

    QModelIndexList changedList;
    for (int row = topLeft.row(); row <= bottomRight.row(); ++row) {
        changedList << d->mDirModel->index(row, 0);
    }

    QModelIndexList& shortList = selectionList;
    QModelIndexList& longList = changedList;
    if (shortList.length() > longList.length()) {
        qSwap(shortList, longList);
    }
246
    for (const QModelIndex & index : qAsConst(shortList)) {
247 248
        if (longList.contains(index)) {
            d->mSelectedFileItemListNeedsUpdate = true;
249
            d->queueSignal(&ContextManager::selectionDataChanged);
250 251 252
            return;
        }
    }
253 254
}

255 256 257
void ContextManager::slotSelectionChanged()
{
    d->mSelectedFileItemListNeedsUpdate = true;
258
    if (!d->mSelectionModel->hasSelection()) {
David Edmundson's avatar
David Edmundson committed
259
        setCurrentUrl(QUrl());
260
    }
261
    d->queueSignal(&ContextManager::selectionChanged);
262 263
}

264 265
void Gwenview::ContextManager::slotCurrentChanged(const QModelIndex& index)
{
David Edmundson's avatar
David Edmundson committed
266
    QUrl url = d->mDirModel->urlForIndex(index);
267
    setCurrentUrl(url);
268 269
}

270 271
void ContextManager::emitQueuedSignals()
{
272
    for (ContextManagerPrivate::Signal signal : qAsConst(d->mQueuedSignals)) {
273
        emit (this->*signal)();
274 275
    }
    d->mQueuedSignals.clear();
276 277
}

278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297
void Gwenview::ContextManager::slotRowsAboutToBeRemoved(const QModelIndex& /*parent*/, int start, int end)
{
    QModelIndex oldCurrent = d->mSelectionModel->currentIndex();
    if (oldCurrent.row() < start || oldCurrent.row() > end) {
        // currentIndex has not been removed
        return;
    }
    QModelIndex newCurrent;
    if (end + 1 < d->mDirModel->rowCount()) {
        newCurrent = d->mDirModel->index(end + 1, 0);
    } else if (start > 0) {
        newCurrent = d->mDirModel->index(start - 1, 0);
    } else {
        // No index we can select, nothing to do
        return;
    }
    d->mSelectionModel->select(oldCurrent, QItemSelectionModel::Deselect);
    d->mSelectionModel->setCurrentIndex(newCurrent, QItemSelectionModel::Select);
}

298 299 300 301 302
bool ContextManager::currentUrlIsRasterImage() const
{
    return MimeTypeUtils::urlKind(currentUrl()) == MimeTypeUtils::KIND_RASTER_IMAGE;
}

David Edmundson's avatar
David Edmundson committed
303
QUrl ContextManager::urlToSelect() const
304 305 306 307
{
    return d->mUrlToSelect;
}

David Edmundson's avatar
David Edmundson committed
308
void ContextManager::setUrlToSelect(const QUrl &url)
309 310 311
{
    GV_RETURN_IF_FAIL(url.isValid());
    d->mUrlToSelect = url;
312

313 314 315 316
    setCurrentUrl(url);
    selectUrlToSelect();
}

rkflx's avatar
rkflx committed
317
QUrl ContextManager::targetDirUrl() const
318
{
rkflx's avatar
rkflx committed
319
    return d->mTargetDirUrl;
320 321
}

rkflx's avatar
rkflx committed
322
void ContextManager::setTargetDirUrl(const QUrl &url)
323
{
324
    GV_RETURN_IF_FAIL(url.isEmpty() || url.isValid());
325 326
    d->mTargetDirUrl = GwenviewConfig::historyEnabled() ? url
                                                        : QUrl();
327 328
}

329 330 331 332 333 334 335 336 337 338
void ContextManager::slotRowsInserted()
{
    // We reach this method when rows have been inserted in the model, but views
    // may not have been updated yet and thus do not have the matching items.
    // Delay the selection of mUrlToSelect so that the view items exist.
    //
    // Without this, when Gwenview is started with an image as argument and the
    // thumbnail bar is visible, the image will not be selected in the thumbnail
    // bar.
    if (d->mUrlToSelect.isValid()) {
339
        QMetaObject::invokeMethod(this, &ContextManager::selectUrlToSelect, Qt::QueuedConnection);
340 341 342
    }
}

343 344
void ContextManager::selectUrlToSelect()
{
345 346 347 348 349 350
    // Because of the queued connection above we might be called several times in a row
    // In this case we don't want the warning below
    if (d->mUrlToSelect.isEmpty()) {
        return;
    }

351
    GV_RETURN_IF_FAIL(d->mUrlToSelect.isValid());
352 353 354
    QModelIndex index = d->mDirModel->indexForUrl(d->mUrlToSelect);
    if (index.isValid()) {
        d->mSelectionModel->setCurrentIndex(index, QItemSelectionModel::ClearAndSelect);
David Edmundson's avatar
David Edmundson committed
355
        d->mUrlToSelect = QUrl();
356 357 358 359 360 361
    } else if (d->mDirListerFinished) {
        // Desired URL cannot be found in the directory
        // Clear the selection to avoid dragging any local files into context
        // and manually set current URL
        d->mSelectionModel->clearSelection();
        setCurrentUrl(d->mUrlToSelect);
362 363 364
    }
}

David Edmundson's avatar
David Edmundson committed
365
void ContextManager::slotDirListerRedirection(const QUrl &newUrl)
366 367 368 369
{
    setCurrentDirUrl(newUrl);
}

370 371 372 373 374
void ContextManager::slotDirListerCompleted()
{
    d->mDirListerFinished = true;
}

375

376
} // namespace