mainWindow.cpp 16.6 KB
Newer Older
1
/***********************************************************************
2
3
4
* SPDX-FileCopyrightText: 2003-2004 Max Howell <max.howell@methylblue.com>
* SPDX-FileCopyrightText: 2008-2009 Martin Sandsmark <martin.sandsmark@kde.org>
* SPDX-FileCopyrightText: 2017 Harald Sitter <sitter@kde.org>
5
*
6
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
7
***********************************************************************/
8
9

#include "mainWindow.h"
10
#include "historyAction.h"
Harald Sitter's avatar
Harald Sitter committed
11
12
13
14
15
16
17
18
19
#include "Config.h"
#include "define.h"
#include "fileTree.h"
#include "progressBox.h"
#include "radialMap/widget.h"
#include "scan.h"
#include "settingsDialog.h"
#include "summaryWidget.h"

Harald Sitter's avatar
Harald Sitter committed
20
21
22
#include <cstdlib>            //std::exit()
#include <iostream>

Harald Sitter's avatar
Harald Sitter committed
23
#include <KActionCollection>
Harald Sitter's avatar
Harald Sitter committed
24
25
26
27
#include <KConfigGroup>
#include <KEditToolBar>     //for editToolbar dialog
#include <KHistoryComboBox>
#include <KIO/Global> // upUrl
Harald Sitter's avatar
Harald Sitter committed
28
#include <KLocalizedString>
Harald Sitter's avatar
Harald Sitter committed
29
30
31
32
33
#include <KMessageBox>  //::start()
#include <KShell>
#include <KShortcutsDialog>
#include <KStandardAction>
#include <KUrlCompletion>   //locationbar
Harald Sitter's avatar
Harald Sitter committed
34

Harald Sitter's avatar
Harald Sitter committed
35
#include <QApplication>     //setupActions()
Harald Sitter's avatar
Harald Sitter committed
36
#include <QDir>
Harald Sitter's avatar
Harald Sitter committed
37
#include <QFileDialog>
Harald Sitter's avatar
Harald Sitter committed
38
#include <QScrollArea>
Harald Sitter's avatar
Harald Sitter committed
39
#include <QStatusBar>
Laurent Montel's avatar
Laurent Montel committed
40
#include <QLineEdit>
Harald Sitter's avatar
Harald Sitter committed
41

42
43
namespace Filelight {

44
MainWindow::MainWindow()
Harald Sitter's avatar
Harald Sitter committed
45
    : KXmlGuiWindow()
46
    , m_histories(nullptr)
Harald Sitter's avatar
Harald Sitter committed
47
48
49
    , m_summary(nullptr)
    , m_map(nullptr)
    , m_started(false)
50
{
Harald Sitter's avatar
Harald Sitter committed
51
52
53
54
    Config::read();

    QScrollArea *scrollArea = new QScrollArea(this);
    scrollArea->setWidgetResizable(true);
55
    setCentralWidget(scrollArea);
Harald Sitter's avatar
Harald Sitter committed
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89

    QWidget *partWidget = new QWidget(scrollArea);
    scrollArea->setWidget(partWidget);

    partWidget->setBackgroundRole(QPalette::Base);
    partWidget->setAutoFillBackground(true);

    m_layout = new QGridLayout();
    partWidget->setLayout(m_layout);

    m_manager = new ScanManager(partWidget);

    m_map = new RadialMap::Widget(partWidget);
    m_layout->addWidget(m_map);

    // FIXME: drop stupid nullptr argument
    m_stateWidget = new ProgressBox(statusBar(), this, m_manager);
    m_layout->addWidget(m_stateWidget);
    m_stateWidget->hide();

    m_numberOfFiles = new QLabel();
    statusBar()->addPermanentWidget(m_numberOfFiles);

    KStandardAction::zoomIn(m_map, &RadialMap::Widget::zoomIn, actionCollection());
    KStandardAction::zoomOut(m_map, &RadialMap::Widget::zoomOut, actionCollection());
    KStandardAction::preferences(this, &MainWindow::configFilelight, actionCollection());

    connect(m_map, &RadialMap::Widget::folderCreated, this, &MainWindow::completed);
    connect(m_map, &RadialMap::Widget::folderCreated, this, &MainWindow::mapChanged);
    connect(m_map, &RadialMap::Widget::activated, this, &MainWindow::updateURL);

    // TODO make better system
    connect(m_map, &RadialMap::Widget::giveMeTreeFor, this, &MainWindow::updateURL);
    connect(m_map, &RadialMap::Widget::giveMeTreeFor, this, &MainWindow::openUrl);
90
    connect(m_map, &RadialMap::Widget::rescanRequested, this, &MainWindow::rescanSingleDir);
Harald Sitter's avatar
Harald Sitter committed
91
92
93
94

    connect(m_manager, &ScanManager::completed, this, &MainWindow::folderScanCompleted);
    connect(m_manager, &ScanManager::aboutToEmptyCache, m_map, &RadialMap::Widget::invalidate);

95
96
    setStandardToolBarMenuEnabled(true);
    setupActions();
97
    createGUI(QStringLiteral("filelightui.rc"));
98

99
    stateChanged(QStringLiteral("scan_failed")); //bah! doesn't affect the parts' actions, should I add them to the actionCollection here?
100

Harald Sitter's avatar
Harald Sitter committed
101
102
103
104
    connect(this, &MainWindow::started, this, &MainWindow::scanStarted);
    connect(this, &MainWindow::completed, this, &MainWindow::scanCompleted);
    connect(this, &MainWindow::canceled, this, &MainWindow::scanFailed);
    connect(this, &MainWindow::canceled, m_histories, &HistoryCollection::stop);
105

106
    const KConfigGroup config = KSharedConfig::openStateConfig()->group("general");
107
    m_combo->setHistoryItems(config.readPathEntry("comboHistory", QStringList()));
108

109
    setAutoSaveSettings(QStringLiteral("window"));
Harald Sitter's avatar
Harald Sitter committed
110

111
    QTimer::singleShot(0, this, &MainWindow::postInit);
112
113
}

Harald Sitter's avatar
Harald Sitter committed
114
115
116
117
118
void MainWindow::scan(const QUrl &u)
{
    slotScanUrl(u);
}

119
void MainWindow::setupActions() //singleton function
120
121
122
{
    KActionCollection *const ac = actionCollection();

123
124
125
126
127
128
    m_combo = new KHistoryComboBox(this);
    m_combo->setCompletionObject(new KUrlCompletion(KUrlCompletion::DirCompletion));
    m_combo->setAutoDeleteCompletionObject(true);
    m_combo->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed));
    m_combo->setDuplicatesEnabled(false);

129
    KStandardAction::open(this, SLOT(slotScanFolder()), ac);
130
    KStandardAction::save(this, SLOT(slotSaveSvg()), ac)->setEnabled(false);
131
132
133
134
135
    KStandardAction::quit(this, SLOT(close()), ac);
    KStandardAction::up(this, SLOT(slotUp()), ac);
    KStandardAction::configureToolbars(this, SLOT(configToolbars()), ac);
    KStandardAction::keyBindings(this, SLOT(configKeys()), ac);

Harald Sitter's avatar
Harald Sitter committed
136
    QAction *action;
137

138
    action = ac->addAction(QStringLiteral("scan_home"), this, &MainWindow::slotScanHomeFolder);
139
    action->setText(i18n("Scan &Home Folder"));
140
    action->setIcon(QIcon::fromTheme(QStringLiteral("user-home")));
141
    ac->setDefaultShortcut(action, QKeySequence(Qt::CTRL | Qt::Key_Home));
142

143
    action = ac->addAction(QStringLiteral("scan_root"), this, &MainWindow::slotScanRootFolder);
144
    action->setText(i18n("Scan &Root Folder"));
145
    action->setIcon(QIcon::fromTheme(QStringLiteral("folder-red")));
146

147
    action = ac->addAction(QStringLiteral("scan_rescan"), this, &MainWindow::rescan);
148
    action->setText(i18n("Rescan"));
149
    action->setIcon(QIcon::fromTheme(QStringLiteral("view-refresh")));
150
    ac->setDefaultShortcut(action, QKeySequence::Refresh);
151
152


153
    action = ac->addAction(QStringLiteral("scan_stop"), this, &MainWindow::slotAbortScan);
154
    action->setText(i18n("Stop"));
155
    action->setIcon(QIcon::fromTheme(QStringLiteral("process-stop")));
156
    ac->setDefaultShortcut(action, Qt::Key_Escape);
157

158
159
160
    action = ac->addAction(QStringLiteral("go"), m_combo, [this]() {
        m_combo->returnPressed(QString());
    });
161
    action->setText(i18n("Go"));
162
    action->setIcon(QIcon::fromTheme(QStringLiteral("go-jump-locationbar")));
163

164
    action = ac->addAction(QStringLiteral("scan_folder"), this, &MainWindow::slotScanFolder);
165
    action->setText(i18n("Scan Folder"));
166
    action->setIcon(QIcon::fromTheme(QStringLiteral("folder")));
167

168
    QWidgetAction *locationAction = ac->add<QWidgetAction>(QStringLiteral("location_bar"), nullptr, nullptr);
169
    locationAction->setText(i18n("Location Bar"));
170
    locationAction->setDefaultWidget(m_combo);
171

Nate Graham's avatar
Nate Graham committed
172
173
174
175
    action = ac->action(QStringLiteral("file_save"));
    action->setText(i18n("Save as SVG..."));
    action->setToolTip(i18n("Save the current view as an SVG file"));

176
    m_recentScans = new KRecentFilesAction(i18n("&Recent Scans"), ac);
177
178
179
180
    m_recentScans->setMaxItems(8);

    m_histories = new HistoryCollection(ac, this);

181
    m_recentScans->loadEntries(KSharedConfig::openStateConfig()->group("general"));
182

183
    connect(m_recentScans, &KRecentFilesAction::urlSelected, this, &MainWindow::slotScanUrl);
Laurent Montel's avatar
Laurent Montel committed
184
    connect(m_combo, qOverload<const QString &>(&KHistoryComboBox::returnPressed), this, &MainWindow::slotComboScan);
185
    connect(m_histories, &HistoryCollection::activated, this, &MainWindow::slotScanUrl);
186
187
}

188
void MainWindow::closeEvent(QCloseEvent *event)
189
{
190
    KConfigGroup config = KSharedConfig::openStateConfig()->group("general");
191

192
193
194
    m_recentScans->saveEntries(config);
    config.writePathEntry("comboHistory", m_combo->historyItems());
    config.sync();
195

196
    KXmlGuiWindow::closeEvent(event);
197
198
}

199
void MainWindow::configToolbars() //slot
200
{
201
    KEditToolBar dialog(factory(), this);
202

203
    if (dialog.exec()) //krazy:exclude=crashy
204
    {
205
        createGUI(QStringLiteral("filelightui.rc"));
206
        applyMainWindowSettings(KSharedConfig::openConfig()->group("window"));
207
208
209
    }
}

210
void MainWindow::configKeys() //slot
211
{
212
    KShortcutsDialog::showDialog(actionCollection(), KShortcutsEditor::LetterShortcutsAllowed, this);
213
214
}

215
void MainWindow::slotScanFolder()
216
{
217
    slotScanUrl(QFileDialog::getExistingDirectoryUrl(this, i18n("Select Folder to Scan"), url()));
218
219
}

220
221
void MainWindow::slotScanHomeFolder()
{
222
    slotScanPath(QDir::homePath());
223
}
224

225
226
void MainWindow::slotSaveSvg()
{
227
228
229
    const QString path = QFileDialog::getSaveFileName(
        this, i18nc("@title:window", "Save as SVG"), QString(),
        i18nc("filedialog type filter", "SVG Files (*.svg);;All Files(*)"));
230
231
232
233
234
235
236
    if (path.isEmpty()) {
        return;
    }

    m_map->saveSvg(path);
}

237
238
void MainWindow::slotScanRootFolder()
{
239
    slotScanPath(QDir::rootPath());
240
}
241
242
243

void MainWindow::slotUp()
{
244
    slotScanUrl(KIO::upUrl(url()));
245
}
246

247
void MainWindow::slotComboScan()
248
{
249
250
    QString path = m_combo->lineEdit()->text();

251
    QUrl url = QUrl::fromUserInput(path);
252
253

    if (url.isRelative())
254
        path = QLatin1String("~/") + path; // KUrlCompletion completes relative to ~, not CWD
255
256

    path = KShell::tildeExpand(path);
257

258
259
    if (slotScanPath(path))
        m_combo->addToHistory(path);
260
261
}

262
bool MainWindow::slotScanPath(const QString &path)
263
{
264
    return slotScanUrl(QUrl::fromUserInput(path));
265
266
}

267
bool MainWindow::slotScanUrl(const QUrl &url)
268
{
269
    const QUrl oldUrl = this->url();
270

271
    if (openUrl(url))
272
    {
273
        m_histories->push(oldUrl);
274
        return true;
275
    }
276
277
    else
        return false;
278
279
}

280
void MainWindow::slotAbortScan()
281
{
282
    if (closeUrl()) action("scan_stop")->setEnabled(false);
283
284
}

285
void MainWindow::scanStarted()
286
{
287
    stateChanged(QStringLiteral("scan_started"));
288
289
290
    m_combo->clearFocus();
}

291
void MainWindow::scanFailed()
292
{
293
    stateChanged(QStringLiteral("scan_failed"));
294
295
    action("go_up")->setStatusTip(QString());
    action("go_up")->setToolTip(QString());
296
297
298
    m_combo->lineEdit()->clear();
}

299
void MainWindow::scanCompleted()
300
{
301
    const QUrl url = this->url();
302

303
    stateChanged(QStringLiteral("scan_complete"));
304

305
306
    action("file_save")->setEnabled(true);

307
    m_combo->lineEdit()->setText(prettyUrl(url));
308

309
    if (url.toLocalFile() == QLatin1String("/")) {
310
311
312
313
314
        action("go_up")->setEnabled(false);
        action("go_up")->setStatusTip(QString());
        action("go_up")->setToolTip(QString());
    }
    else {
315
        action("go_up")->setEnabled(true);
316
317
        action("go_up")->setStatusTip(KIO::upUrl(url).path());
        action("go_up")->setToolTip(KIO::upUrl(url).path());
318
319
    }

320
    m_recentScans->addUrl(url); //FIXME doesn't set the tick
321
322
}

323
void MainWindow::urlAboutToChange()
324
{
325
326
    //called when part's URL is about to change internally
    //the part will then create the Map and emit completed()
327

328
    m_histories->push(url());
329
330
331
332
333
334
}

/**********************************************
  SESSION MANAGEMENT
 **********************************************/

335
void MainWindow::saveProperties(KConfigGroup &configgroup) //virtual
336
{
337
338
    if (!m_histories) return;

339
    m_histories->save(configgroup);
340
    configgroup.writeEntry("currentMap", url().path());
341
342
}

343
void MainWindow::readProperties(const KConfigGroup &configgroup) //virtual
344
{
345
346
    m_histories->restore(configgroup);
    slotScanPath(configgroup.group("General").readEntry("currentMap", QString()));
347
348
}

349
void MainWindow::postInit()
Harald Sitter's avatar
Harald Sitter committed
350
351
352
353
354
355
356
357
{
    if (url().isEmpty()) //if url is not empty openUrl() has been called immediately after ctor, which happens
    {
        m_map->hide();
        showSummary();

        //FIXME KXMLGUI is b0rked, it should allow us to set this
        //BEFORE createGUI is called but it doesn't
Laurent Montel's avatar
Laurent Montel committed
358
        stateChanged(QStringLiteral("scan_failed"));
Harald Sitter's avatar
Harald Sitter committed
359
360
361
    }
}

362
bool MainWindow::openUrl(const QUrl &u)
Harald Sitter's avatar
Harald Sitter committed
363
364
365
366
367
368
369
370
371
{

    //TODO everyone hates dialogs, instead render the text in big fonts on the Map
    //TODO should have an empty QUrl until scan is confirmed successful
    //TODO probably should set caption to QString::null while map is unusable

#define KMSG(s) KMessageBox::information(widget(), s)

    QUrl uri = u.adjusted(QUrl::NormalizePathSegments);
372
    const QString localPath = uri.toLocalFile();
373
    const bool isLocal = uri.isLocalFile();
Harald Sitter's avatar
Harald Sitter committed
374
375
376

    if (uri.isEmpty())
    {
Yuri Chornoivan's avatar
Yuri Chornoivan committed
377
        //do nothing, chances are the user accidentally pressed ENTER
Harald Sitter's avatar
Harald Sitter committed
378
379
380
381
382
    }
    else if (!uri.isValid())
    {
        KMSG(i18n("The entered URL cannot be parsed; it is invalid."));
    }
383
    else if (isLocal && !QDir::isAbsolutePath(localPath))
Harald Sitter's avatar
Harald Sitter committed
384
    {
385
        KMSG(i18n("Filelight only accepts absolute paths, eg. /%1", localPath));
Harald Sitter's avatar
Harald Sitter committed
386
    }
387
    else if (isLocal && !QDir(localPath).exists())
Harald Sitter's avatar
Harald Sitter committed
388
    {
389
        KMSG(i18n("Folder not found: %1", localPath));
Harald Sitter's avatar
Harald Sitter committed
390
    }
391
    else if (isLocal && !QDir(localPath).isReadable())
Harald Sitter's avatar
Harald Sitter committed
392
    {
393
        KMSG(i18n("Unable to enter: %1\nYou do not have access rights to this location.", localPath));
Harald Sitter's avatar
Harald Sitter committed
394
395
396
397
    }
    else
    {
        //we don't want to be using the summary screen anymore
398
        if (m_summary != nullptr)
Harald Sitter's avatar
Harald Sitter committed
399
400
401
402
403
            m_summary->hide();

        m_stateWidget->show();
        m_layout->addWidget(m_stateWidget);

404
405
406
407
408
        const bool success = start(uri);
        if (success) {
            setUrl(uri);
        }
        return success;
Harald Sitter's avatar
Harald Sitter committed
409
410
411
412
413
    }

    return false;
}

414
bool MainWindow::closeUrl()
Harald Sitter's avatar
Harald Sitter committed
415
416
417
418
419
420
421
422
423
424
425
426
{
    if (m_manager->abort())
        statusBar()->showMessage(i18n("Aborting Scan..."));

    m_map->hide();
    m_stateWidget->hide();

    showSummary();

    return true;
}

427
428
QString MainWindow::prettyUrl(const QUrl &url) const {
    return url.isLocalFile() ? QDir::toNativeSeparators(url.toLocalFile()) : url.toString();
Harald Sitter's avatar
Harald Sitter committed
429
430
}

431
void MainWindow::updateURL(const QUrl &u)
Harald Sitter's avatar
Harald Sitter committed
432
433
434
{
    if (m_manager->running())
        m_manager->abort();
435

Harald Sitter's avatar
Harald Sitter committed
436
    if (u == url())
437
        m_manager->emptyCache(); //same as rescan()
Harald Sitter's avatar
Harald Sitter committed
438
439
440
441
442

    //do this last, or it breaks Konqi location bar
    setUrl(u);
}

443
444
445
446
447
448
449
450
451
452
453
454
void MainWindow::rescanSingleDir(const QUrl &dirUrl)
{
    if (m_manager->running()) {
        m_manager->abort();
    }

    m_manager->invalidateCacheFor(dirUrl);
    m_map->hide();
    m_stateWidget->show();
    start(url());
}

Harald Sitter's avatar
Harald Sitter committed
455
456
457
458
459
460
461
462
463
464
QUrl MainWindow::url() const
{
    return m_url;
}

void MainWindow::setUrl(const QUrl &url)
{
    m_url = url;
}

465
void MainWindow::configFilelight()
Harald Sitter's avatar
Harald Sitter committed
466
467
468
469
470
471
{
    SettingsDialog *dialog = new SettingsDialog(widget());

    connect(dialog, &SettingsDialog::canvasIsDirty, m_map, &RadialMap::Widget::refresh);
    connect(dialog, &SettingsDialog::mapIsInvalid, m_manager, &ScanManager::emptyCache);

472
473
474
475
    if (m_summary) {
        connect(dialog, &SettingsDialog::canvasIsDirty, m_summary, &SummaryWidget::canvasDirtied);
    }

Harald Sitter's avatar
Harald Sitter committed
476
477
478
    dialog->show(); //deletes itself
}

479
bool MainWindow::start(const QUrl &url)
Harald Sitter's avatar
Harald Sitter committed
480
481
{
    if (!m_started) {
482
483
        connect(m_map, &RadialMap::Widget::mouseHover,
                [&](const QString &msg) { statusBar()->showMessage(msg); });
Harald Sitter's avatar
Harald Sitter committed
484
485
486
487
488
489
490
491
492
493
        connect(m_map, &RadialMap::Widget::folderCreated, statusBar(), &QStatusBar::clearMessage);
        m_started = true;
    }

    if (m_manager->running())
        m_manager->abort();

    m_numberOfFiles->setText(QString());

    if (m_manager->start(url)) {
494
        const QString s = i18n("Scanning: %1", prettyUrl(url));
Laurent Montel's avatar
Laurent Montel committed
495
        stateChanged(QStringLiteral("scan_started"));
Laurent Montel's avatar
Laurent Montel committed
496
497
        Q_EMIT started(); //as a MainWindow, we have to do this
        Q_EMIT setWindowCaption(s);
Harald Sitter's avatar
Harald Sitter committed
498
499
500
501
502
503
504
505
506
507
508
        statusBar()->showMessage(s);
        m_map->hide();
        m_map->invalidate(); //to maintain ui consistency


        return true;
    }

    return false;
}

509
void MainWindow::rescan()
Harald Sitter's avatar
Harald Sitter committed
510
511
512
{
    if (m_summary && !m_summary->isHidden()) {
        delete m_summary;
513
        m_summary = nullptr;
Harald Sitter's avatar
Harald Sitter committed
514
515
516
517
518
519
520
521
522
523
        showSummary();
        return;
    }

    m_manager->emptyCache(); //causes canvas to invalidate
    m_map->hide();
    m_stateWidget->show();
    start(url());
}

524
void MainWindow::folderScanCompleted(Folder *tree)
Harald Sitter's avatar
Harald Sitter committed
525
526
527
528
529
530
531
532
{
    if (tree) {
        statusBar()->showMessage(i18n("Scan completed, generating map..."));

        m_stateWidget->hide();
        m_map->show();
        m_map->create(tree);

Laurent Montel's avatar
Laurent Montel committed
533
        stateChanged(QStringLiteral("scan_complete"));
Harald Sitter's avatar
Harald Sitter committed
534
535
    }
    else {
Laurent Montel's avatar
Laurent Montel committed
536
        stateChanged(QStringLiteral("scan_failed"));
537
        Q_EMIT canceled(i18n("Scan failed: %1", prettyUrl(url())));
Laurent Montel's avatar
Laurent Montel committed
538
        Q_EMIT setWindowCaption(QString());
Harald Sitter's avatar
Harald Sitter committed
539
540
541
542
543
544
545
546
547
548
549
550

        statusBar()->clearMessage();

        m_map->hide();
        m_stateWidget->hide();

        showSummary();

        setUrl(QUrl());
    }
}

551
void MainWindow::mapChanged(const Folder *tree)
Harald Sitter's avatar
Harald Sitter committed
552
553
554
{
    //IMPORTANT -> url() has already been set

555
    Q_EMIT setWindowCaption(prettyUrl(url()));
Harald Sitter's avatar
Harald Sitter committed
556
557

    const int fileCount = tree->children();
558
    const QString text = (fileCount == 0) ?
Harald Sitter's avatar
Harald Sitter committed
559
560
561
562
563
564
        i18n("No files.") :
        i18np("1 file", "%1 files",fileCount);

    m_numberOfFiles->setText(text);
}

565
void MainWindow::showSummary()
Harald Sitter's avatar
Harald Sitter committed
566
{
567
    if (m_summary == nullptr) {
Harald Sitter's avatar
Harald Sitter committed
568
        m_summary = new SummaryWidget(widget());
569
        m_summary->setObjectName(QStringLiteral("summaryWidget"));
Harald Sitter's avatar
Harald Sitter committed
570
571
572
573
574
575
576
577
        connect(m_summary, &SummaryWidget::activated, this, &MainWindow::openUrl);
        m_summary->show();
        m_layout->addWidget(m_summary);
    }
    else m_summary->show();
}

} //namespace Filelight