documentationview.cpp 12.9 KB
Newer Older
1
2
/*
   Copyright 2009 Aleix Pol Gonzalez <aleixpol@kde.org>
3
   Copyright 2010 Benjamin Port <port.benjamin@gmail.com>
Dāvis Mosāns's avatar
Dāvis Mosāns committed
4

5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
   This library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Library General Public
   License version 2 as published by the Free Software Foundation.

   This library 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
   Library General Public License for more details.

   You should have received a copy of the GNU Library General Public License
   along with this library; see the file COPYING.LIB.  If not, write to
   the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
   Boston, MA 02110-1301, USA.
*/

#include "documentationview.h"
Kevin Funk's avatar
Kevin Funk committed
21

22
#include <QWidgetAction>
23
#include <QAction>
Kevin Funk's avatar
Kevin Funk committed
24
#include <QIcon>
25
#include <QVBoxLayout>
26
27
#include <QComboBox>
#include <QCompleter>
Anton Anikin's avatar
Anton Anikin committed
28
#include <QAbstractItemView>
Kevin Funk's avatar
Kevin Funk committed
29
#include <QLineEdit>
30
#include <QShortcut>
31

32
#include <KLocalizedString>
Kevin Funk's avatar
Kevin Funk committed
33

34
#include <interfaces/icore.h>
35
#include <interfaces/idocumentationprovider.h>
36
#include <interfaces/idocumentationproviderprovider.h>
37
#include <interfaces/idocumentationcontroller.h>
38
#include <interfaces/iplugincontroller.h>
39
#include "documentationfindwidget.h"
Dāvis Mosāns's avatar
Dāvis Mosāns committed
40
41
#include "debug.h"

42
using namespace KDevelop;
43

Milian Wolff's avatar
Milian Wolff committed
44
45
DocumentationView::DocumentationView(QWidget* parent, ProvidersModel* model)
    : QWidget(parent), mProvidersModel(model)
46
{
47
    setWindowIcon(QIcon::fromTheme(QStringLiteral("documentation"), windowIcon()));
48
49
    setWindowTitle(i18n("Documentation"));

50
    setLayout(new QVBoxLayout(this));
51
52
    layout()->setMargin(0);
    layout()->setSpacing(0);
Dāvis Mosāns's avatar
Dāvis Mosāns committed
53

54
55
    mFindDoc = new DocumentationFindWidget;
    mFindDoc->hide();
Dāvis Mosāns's avatar
Dāvis Mosāns committed
56

57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
    // insert placeholder widget at location of doc view
    layout()->addWidget(new QWidget(this));
    layout()->addWidget(mFindDoc);

    setupActions();

    mCurrent = mHistory.end();

    setFocusProxy(mIdentifiers);

    QMetaObject::invokeMethod(this, "initialize", Qt::QueuedConnection);
}

QList<QAction*> DocumentationView::contextMenuActions() const
{
72
73
    // TODO: also show providers
    return {mBack, mForward, mHomeAction, mSeparatorBeforeFind, mFind};
74
75
76
77
78
79
80
81
82
}

void DocumentationView::setupActions()
{
    // use custom QAction's with createWidget for mProviders and mIdentifiers
    mBack = new QAction(QIcon::fromTheme(QStringLiteral("go-previous")), i18n("Back"), this);
    mBack->setEnabled(false);
    connect(mBack, &QAction::triggered, this, &DocumentationView::browseBack);
    addAction(mBack);
Dāvis Mosāns's avatar
Dāvis Mosāns committed
83

84
85
86
87
88
89
    mForward = new QAction(QIcon::fromTheme(QStringLiteral("go-next")), i18n("Forward"), this);
    mForward->setEnabled(false);
    connect(mForward, &QAction::triggered, this, &DocumentationView::browseForward);
    addAction(mForward);

    mHomeAction = new QAction(QIcon::fromTheme(QStringLiteral("go-home")), i18n("Home"), this);
90
    mHomeAction->setEnabled(false);
91
92
93
94
    connect(mHomeAction, &QAction::triggered, this, &DocumentationView::showHome);
    addAction(mHomeAction);

    mProviders = new QComboBox(this);
95
96
97
    auto providersAction = new QWidgetAction(this);
    providersAction->setDefaultWidget(mProviders);
    addAction(providersAction);
98
99

    mIdentifiers = new QLineEdit(this);
100
    mIdentifiers->setEnabled(false);
Kevin Funk's avatar
Kevin Funk committed
101
    mIdentifiers->setClearButtonEnabled(true);
102
    mIdentifiers->setPlaceholderText(i18n("Search..."));
103
    mIdentifiers->setCompleter(new QCompleter(mIdentifiers));
104
105
//     mIdentifiers->completer()->setCompletionMode(QCompleter::UnfilteredPopupCompletion);
    mIdentifiers->completer()->setCaseSensitivity(Qt::CaseInsensitive);
106
107
108

    /* vertical size policy should be left to the style. */
    mIdentifiers->setSizePolicy(QSizePolicy::Expanding, mIdentifiers->sizePolicy().verticalPolicy());
Friedrich W. H. Kossebau's avatar
Friedrich W. H. Kossebau committed
109
    connect(mIdentifiers->completer(), QOverload<const QModelIndex&>::of(&QCompleter::activated),
110
            this, &DocumentationView::changedSelection);
Anton Anikin's avatar
Anton Anikin committed
111
    connect(mIdentifiers, &QLineEdit::returnPressed, this, &DocumentationView::returnPressed);
112
113
114
    auto identifiersAction = new QWidgetAction(this);
    identifiersAction->setDefaultWidget(mIdentifiers);
    addAction(identifiersAction);
115

116
117
118
119
120
121
    mSeparatorBeforeFind = new QAction(this);
    mSeparatorBeforeFind->setSeparator(true);
    addAction(mSeparatorBeforeFind);

    mFind = new QAction(QIcon::fromTheme(QStringLiteral("edit-find")), i18n("Find in Text..."), this);
    mFind->setToolTip(i18n("Find in text of current documentation page."));
122
    mFind->setEnabled(false);
123
124
125
    connect(mFind, &QAction::triggered, mFindDoc, &DocumentationFindWidget::startSearch);
    addAction(mFind);

126
127
128
    auto closeFindBarShortcut = new QShortcut(QKeySequence(Qt::Key_Escape), this);
    closeFindBarShortcut->setContext(Qt::WidgetWithChildrenShortcut);
    connect(closeFindBarShortcut, &QShortcut::activated, mFindDoc, &QWidget::hide);
129
130
131
132
133
}

void DocumentationView::initialize()
{
    mProviders->setModel(mProvidersModel);
Friedrich W. H. Kossebau's avatar
Friedrich W. H. Kossebau committed
134
    connect(mProviders, QOverload<int>::of(&QComboBox::activated), this, &DocumentationView::changedProvider);
135
    connect(mProvidersModel, &ProvidersModel::providersChanged, this, &DocumentationView::emptyHistory);
Dāvis Mosāns's avatar
Dāvis Mosāns committed
136

137
138
139
140
    const bool hasProviders = (mProviders->count() > 0);
    mHomeAction->setEnabled(hasProviders);
    mIdentifiers->setEnabled(hasProviders);
    if (hasProviders) {
141
        changedProvider(0);
Milian Wolff's avatar
Milian Wolff committed
142
    }
143
144
}

145
void DocumentationView::browseBack()
146
{
147
    --mCurrent;
Milian Wolff's avatar
Milian Wolff committed
148
    mBack->setEnabled(mCurrent != mHistory.begin());
149
    mForward->setEnabled(true);
Dāvis Mosāns's avatar
Dāvis Mosāns committed
150

151
    updateView();
152
153
154
155
}

void DocumentationView::browseForward()
{
156
    ++mCurrent;
Milian Wolff's avatar
Milian Wolff committed
157
    mForward->setEnabled(mCurrent+1 != mHistory.end());
158
    mBack->setEnabled(true);
Milian Wolff's avatar
Milian Wolff committed
159

160
161
162
    updateView();
}

163
164
void DocumentationView::showHome()
{
165
    auto prov = mProvidersModel->provider(mProviders->currentIndex());
Dāvis Mosāns's avatar
Dāvis Mosāns committed
166

167
168
169
    showDocumentation(prov->homePage());
}

Anton Anikin's avatar
Anton Anikin committed
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
void DocumentationView::returnPressed()
{
    // Exit if search text is empty. It's necessary because of empty
    // line edit text not leads to "empty" completer indexes.
    if (mIdentifiers->text().isEmpty())
        return;

    // Exit if completer popup has selected item - in this case 'Return'
    // key press emits QCompleter::activated signal which is already connected.
    if (mIdentifiers->completer()->popup()->currentIndex().isValid())
        return;

    // If user doesn't select any item in popup we will try to use the first row.
    if (mIdentifiers->completer()->setCurrentRow(0))
        changedSelection(mIdentifiers->completer()->currentIndex());
}

187
void DocumentationView::changedSelection(const QModelIndex& idx)
188
{
189
    if (idx.isValid()) {
190
191
192
193
194
195
        // Skip view update if user try to show already opened documentation
        mIdentifiers->setText(idx.data(Qt::DisplayRole).toString());
        if (mIdentifiers->text() == (*mCurrent)->name()) {
            return;
        }

196
197
198
        IDocumentationProvider* prov = mProvidersModel->provider(mProviders->currentIndex());
        auto doc = prov->documentationForIndex(idx);
        if (doc) {
199
            showDocumentation(doc);
200
        }
201
    }
202
203
}

204
void DocumentationView::showDocumentation(const IDocumentation::Ptr& doc)
205
{
Dāvis Mosāns's avatar
Dāvis Mosāns committed
206
207
    qCDebug(DOCUMENTATION) << "showing" << doc->name();

Milian Wolff's avatar
Milian Wolff committed
208
    mBack->setEnabled(!mHistory.isEmpty());
209
    mForward->setEnabled(false);
210
211
212
213
214
215
216
217
218

    // clear all history following the current item, unless we're already
    // at the end (otherwise this code crashes when history is empty, which
    // happens when addHistory is first called on startup to add the
    // homepage)
    if (mCurrent+1 < mHistory.end()) {
        mHistory.erase(mCurrent+1, mHistory.end());
    }

219
    mHistory.append(doc);
Milian Wolff's avatar
Milian Wolff committed
220
    mCurrent = mHistory.end()-1;
Milian Wolff's avatar
Milian Wolff committed
221
222
223
224
225
226
227

    // NOTE: we assume an existing widget was used to navigate somewhere
    //       but this history entry actually contains the new info for the
    //       title... this is ugly and should be refactored somehow
    if (mIdentifiers->completer()->model() == (*mCurrent)->provider()->indexModel()) {
        mIdentifiers->setText((*mCurrent)->name());
    }
228
229

    updateView();
230
}
231

232
233
234
void DocumentationView::emptyHistory()
{
    mHistory.clear();
Milian Wolff's avatar
Milian Wolff committed
235
    mCurrent = mHistory.end();
236
237
    mBack->setEnabled(false);
    mForward->setEnabled(false);
238
239
240
241
    const bool hasProviders = (mProviders->count() > 0);
    mHomeAction->setEnabled(hasProviders);
    mIdentifiers->setEnabled(hasProviders);
    if (hasProviders) {
242
        mProviders->setCurrentIndex(0);
243
        changedProvider(0);
244
245
    } else {
        updateView();
246
    }
247
248
}

249
void DocumentationView::updateView()
250
{
251
252
253
254
255
256
257
258
    if (mCurrent != mHistory.end()) {
        mProviders->setCurrentIndex(mProvidersModel->rowForProvider((*mCurrent)->provider()));
        mIdentifiers->completer()->setModel((*mCurrent)->provider()->indexModel());
        mIdentifiers->setText((*mCurrent)->name());
        mIdentifiers->completer()->setCompletionPrefix((*mCurrent)->name());
    } else {
        mIdentifiers->clear();
    }
Dāvis Mosāns's avatar
Dāvis Mosāns committed
259

260
    QLayoutItem* lastview = layout()->takeAt(0);
261
    Q_ASSERT(lastview);
Dāvis Mosāns's avatar
Dāvis Mosāns committed
262

Milian Wolff's avatar
Milian Wolff committed
263
    if (lastview->widget()->parent() == this) {
264
        lastview->widget()->deleteLater();
Milian Wolff's avatar
Milian Wolff committed
265
    }
Dāvis Mosāns's avatar
Dāvis Mosāns committed
266

267
    delete lastview;
Dāvis Mosāns's avatar
Dāvis Mosāns committed
268

269
    mFindDoc->setEnabled(false);
270
271
272
273
274
275
276
277
278
    QWidget* w;
    if (mCurrent != mHistory.end()) {
        w = (*mCurrent)->documentationWidget(mFindDoc, this);
        Q_ASSERT(w);
        QWidget::setTabOrder(mIdentifiers, w);
    } else {
        // placeholder widget at location of doc view
        w = new QWidget(this);
    }
Dāvis Mosāns's avatar
Dāvis Mosāns committed
279

280
    mFind->setEnabled(mFindDoc->isEnabled());
Milian Wolff's avatar
Milian Wolff committed
281
    if (!mFindDoc->isEnabled()) {
282
        mFindDoc->hide();
Milian Wolff's avatar
Milian Wolff committed
283
    }
Dāvis Mosāns's avatar
Dāvis Mosāns committed
284

285
    QLayoutItem* findWidget = layout()->takeAt(0);
286
    layout()->addWidget(w);
Milian Wolff's avatar
Milian Wolff committed
287
    layout()->addItem(findWidget);
288
289
290
291
}

void DocumentationView::changedProvider(int row)
{
292
293
    mIdentifiers->completer()->setModel(mProvidersModel->provider(row)->indexModel());
    mIdentifiers->clear();
Dāvis Mosāns's avatar
Dāvis Mosāns committed
294

295
    showHome();
296
}
297
298
299
300
301
302
303

////////////// ProvidersModel //////////////////

ProvidersModel::ProvidersModel(QObject* parent)
    : QAbstractListModel(parent)
    , mProviders(ICore::self()->documentationController()->documentationProviders())
{
304
    connect(ICore::self()->pluginController(), &IPluginController::unloadingPlugin, this, &ProvidersModel::unloaded);
305
306
    connect(ICore::self()->pluginController(), &IPluginController::pluginLoaded, this, &ProvidersModel::loaded);
    connect(ICore::self()->documentationController(), &IDocumentationController::providersChanged, this, &ProvidersModel::reloadProviders);
307
308
309
310
311
312
}

void ProvidersModel::reloadProviders()
{
    beginResetModel();
    mProviders = ICore::self()->documentationController()->documentationProviders();
313
314
315
316
317
318

    std::sort(mProviders.begin(), mProviders.end(),
              [](const KDevelop::IDocumentationProvider* a, const KDevelop::IDocumentationProvider* b) {
        return a->name() < b->name();
    });

319
    endResetModel();
320
    emit providersChanged();
321
322
323
324
}

QVariant ProvidersModel::data(const QModelIndex& index, int role) const
{
325
326
327
    if (index.row() >= mProviders.count() || index.row() < 0)
        return QVariant();

328
329
330
331
    QVariant ret;
    switch (role)
    {
    case Qt::DisplayRole:
Milian Wolff's avatar
Milian Wolff committed
332
        ret = provider(index.row())->name();
333
334
        break;
    case Qt::DecorationRole:
Milian Wolff's avatar
Milian Wolff committed
335
        ret = provider(index.row())->icon();
336
337
338
339
340
        break;
    }
    return ret;
}

341
void ProvidersModel::addProvider(IDocumentationProvider* provider)
342
{
343
    if (!provider || mProviders.contains(provider))
344
345
        return;

346
347
348
    int pos = 0;
    while (pos < mProviders.size() && mProviders[pos]->name() < provider->name())
        ++pos;
349

350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
    beginInsertRows(QModelIndex(), pos, pos);
    mProviders.insert(pos, provider);
    endInsertRows();

    emit providersChanged();
}

void ProvidersModel::removeProvider(IDocumentationProvider* provider)
{
    int pos;
    if (!provider || (pos = mProviders.indexOf(provider)) < 0)
        return;

    beginRemoveRows(QModelIndex(), pos, pos);
    mProviders.removeAt(pos);
    endRemoveRows();

    emit providersChanged();
368
369
}

Milian Wolff's avatar
Milian Wolff committed
370
void ProvidersModel::unloaded(IPlugin* plugin)
371
{
372
    removeProvider(plugin->extension<IDocumentationProvider>());
373

374
    auto* providerProvider = plugin->extension<IDocumentationProviderProvider>();
Milian Wolff's avatar
Milian Wolff committed
375
    if (providerProvider) {
376
377
        const auto providers = providerProvider->providers();
        for (IDocumentationProvider* provider : providers) {
378
            removeProvider(provider);
379
        }
380
    }
381
382
}

Milian Wolff's avatar
Milian Wolff committed
383
void ProvidersModel::loaded(IPlugin* plugin)
384
{
385
    addProvider(plugin->extension<IDocumentationProvider>());
386

387
    auto* providerProvider = plugin->extension<IDocumentationProviderProvider>();
Milian Wolff's avatar
Milian Wolff committed
388
    if (providerProvider) {
389
390
        const auto providers = providerProvider->providers();
        for (IDocumentationProvider* provider : providers) {
391
            addProvider(provider);
392
        }
393
    }
394
395
}

396
397
398
399
400
int ProvidersModel::rowCount(const QModelIndex& parent) const
{
    return parent.isValid() ? 0 : mProviders.count();
}

Milian Wolff's avatar
Milian Wolff committed
401
402
403
404
405
406
407
408
409
410
411
412
413
414
int ProvidersModel::rowForProvider(IDocumentationProvider* provider)
{
    return mProviders.indexOf(provider);
}

IDocumentationProvider* ProvidersModel::provider(int pos) const
{
    return mProviders[pos];
}

QList<IDocumentationProvider*> ProvidersModel::providers()
{
    return mProviders;
}