foldernavigationwidget.cpp 31.5 KB
Newer Older
hjk's avatar
hjk committed
1
/****************************************************************************
con's avatar
con committed
2
**
3 4
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
con's avatar
con committed
5
**
hjk's avatar
hjk committed
6
** This file is part of Qt Creator.
con's avatar
con committed
7
**
hjk's avatar
hjk committed
8 9 10 11
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
12 13 14
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
15
**
16 17 18 19 20 21 22
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
con's avatar
con committed
23
**
hjk's avatar
hjk committed
24
****************************************************************************/
hjk's avatar
hjk committed
25

con's avatar
con committed
26
#include "foldernavigationwidget.h"
27
#include "projectexplorer.h"
28
#include "projectexplorerconstants.h"
29
#include "projectexplorericons.h"
30 31
#include "projectnodes.h"
#include "projecttree.h"
con's avatar
con committed
32

33
#include <coreplugin/actionmanager/actionmanager.h>
34
#include <coreplugin/actionmanager/command.h>
35
#include <coreplugin/diffservice.h>
36
#include <coreplugin/documentmanager.h>
con's avatar
con committed
37
#include <coreplugin/editormanager/editormanager.h>
38
#include <coreplugin/editormanager/ieditor.h>
39
#include <coreplugin/fileiconprovider.h>
Robert Loehning's avatar
Robert Loehning committed
40
#include <coreplugin/fileutils.h>
41 42 43
#include <coreplugin/icontext.h>
#include <coreplugin/icore.h>
#include <coreplugin/idocument.h>
44

45 46 47 48
#include <extensionsystem/pluginmanager.h>

#include <texteditor/textdocument.h>

49
#include <utils/algorithm.h>
50
#include <utils/filecrumblabel.h>
51
#include <utils/hostosinfo.h>
52
#include <utils/navigationtreeview.h>
53
#include <utils/qtcassert.h>
54
#include <utils/removefiledialog.h>
Ulf Hermann's avatar
Ulf Hermann committed
55
#include <utils/utilsicons.h>
con's avatar
con committed
56

57 58
#include <QAction>
#include <QApplication>
59
#include <QComboBox>
60 61 62 63
#include <QContextMenuEvent>
#include <QDir>
#include <QFileInfo>
#include <QFileSystemModel>
64
#include <QHeaderView>
65
#include <QMenu>
66
#include <QMessageBox>
67
#include <QScrollBar>
68
#include <QSize>
69
#include <QTimer>
70
#include <QToolButton>
71
#include <QVBoxLayout>
con's avatar
con committed
72

73 74
const int PATH_ROLE = Qt::UserRole;
const int ID_ROLE = Qt::UserRole + 1;
75
const int SORT_ROLE = Qt::UserRole + 2;
76

77
const char PROJECTSDIRECTORYROOT_ID[] = "A.Projects";
78
const char C_FOLDERNAVIGATIONWIDGET[] = "ProjectExplorer.FolderNavigationWidget";
79

con's avatar
con committed
80
namespace ProjectExplorer {
hjk's avatar
hjk committed
81 82
namespace Internal {

83
static FolderNavigationWidgetFactory *m_instance = nullptr;
84

85
QVector<FolderNavigationWidgetFactory::RootDirectory>
86
    FolderNavigationWidgetFactory::m_rootDirectories;
87

88 89 90 91 92 93 94 95

static QWidget *createHLine()
{
    auto widget = new QFrame;
    widget->setFrameStyle(QFrame::Plain | QFrame::HLine);
    return widget;
}

96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
// Call delayLayoutOnce to delay reporting the new heightForWidget by the double-click interval.
// Call setScrollBarOnce to set a scroll bar's value once during layouting (where heightForWidget
// is called).
class DelayedFileCrumbLabel : public Utils::FileCrumbLabel
{
public:
    DelayedFileCrumbLabel(QWidget *parent) : Utils::FileCrumbLabel(parent) {}

    int immediateHeightForWidth(int w) const;
    int heightForWidth(int w) const final;
    void delayLayoutOnce();
    void setScrollBarOnce(QScrollBar *bar, int value);

private:
    void setScrollBarOnce() const;

    QPointer<QScrollBar> m_bar;
    int m_barValue = 0;
    bool m_delaying = false;
};

117
// FolderNavigationModel: Shows path as tooltip.
118 119 120
class FolderNavigationModel : public QFileSystemModel
{
public:
121
    explicit FolderNavigationModel(QObject *parent = nullptr);
122 123 124 125
    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const final;
    Qt::DropActions supportedDragActions() const final;
    Qt::ItemFlags flags(const QModelIndex &index) const final;
    bool setData(const QModelIndex &index, const QVariant &value, int role) final;
126 127
};

128 129
FolderNavigationModel::FolderNavigationModel(QObject *parent) : QFileSystemModel(parent)
{ }
130 131 132 133 134 135 136 137 138

QVariant FolderNavigationModel::data(const QModelIndex &index, int role) const
{
    if (role == Qt::ToolTipRole)
        return QDir::toNativeSeparators(QDir::cleanPath(filePath(index)));
    else
        return QFileSystemModel::data(index, role);
}

139 140 141 142 143
Qt::DropActions FolderNavigationModel::supportedDragActions() const
{
    return Qt::MoveAction;
}

144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164
Qt::ItemFlags FolderNavigationModel::flags(const QModelIndex &index) const
{
    if (index.isValid() && !fileInfo(index).isRoot())
        return QFileSystemModel::flags(index) | Qt::ItemIsEditable;
    return QFileSystemModel::flags(index);
}

static QVector<FolderNode *> renamableFolderNodes(const Utils::FileName &before,
                                                  const Utils::FileName &after)
{
    QVector<FolderNode *> folderNodes;
    ProjectTree::forEachNode([&](Node *node) {
        if (node->nodeType() == NodeType::File && node->filePath() == before
                && node->parentFolderNode()
                && node->parentFolderNode()->renameFile(before.toString(), after.toString())) {
            folderNodes.append(node->parentFolderNode());
        }
    });
    return folderNodes;
}

165 166 167 168 169 170 171 172
static QStringList projectNames(const QVector<FolderNode *> &folders)
{
    const QStringList names = Utils::transform<QList>(folders, [](FolderNode *n) {
        return n->managingProject()->filePath().fileName();
    });
    return Utils::filteredUnique(names);
}

173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197
bool FolderNavigationModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
    QTC_ASSERT(index.isValid() && parent(index).isValid() && index.column() == 0
                   && role == Qt::EditRole && value.canConvert<QString>(),
               return false);
    const QString afterFileName = value.toString();
    const QString beforeFilePath = filePath(index);
    const QString parentPath = filePath(parent(index));
    const QString afterFilePath = parentPath + '/' + afterFileName;
    if (beforeFilePath == afterFilePath)
        return false;
    // need to rename through file system model, which takes care of not changing our selection
    const bool success = QFileSystemModel::setData(index, value, role);
    // for files we can do more than just rename on disk, for directories the user is on his/her own
    if (success && fileInfo(index).isFile()) {
        Core::DocumentManager::renamedFile(beforeFilePath, afterFilePath);
        const QVector<FolderNode *> folderNodes
            = renamableFolderNodes(Utils::FileName::fromString(beforeFilePath),
                                   Utils::FileName::fromString(afterFilePath));
        QVector<FolderNode *> failedNodes;
        for (FolderNode *folder : folderNodes) {
            if (!folder->canRenameFile(beforeFilePath, afterFilePath))
                failedNodes.append(folder);
        }
        if (!failedNodes.isEmpty()) {
198
            const QString projects = projectNames(failedNodes).join(", ");
199 200 201 202 203 204 205 206 207 208 209 210 211 212
            const QString errorMessage
                = tr("The file \"%1\" was renamed to \"%2\", "
                     "but the following projects could not be automatically changed: %3")
                      .arg(beforeFilePath, afterFilePath, projects);
            QTimer::singleShot(0, Core::ICore::instance(), [errorMessage] {
                QMessageBox::warning(Core::ICore::dialogParent(),
                                     ProjectExplorerPlugin::tr("Project Editing Failed"),
                                     errorMessage);
            });
        }
    }
    return success;
}

213 214 215 216 217 218 219
static void showOnlyFirstColumn(QTreeView *view)
{
    const int columnCount = view->header()->count();
    for (int i = 1; i < columnCount; ++i)
        view->setColumnHidden(i, true);
}

220 221 222 223 224 225 226 227 228 229 230 231 232
static bool isChildOf(const QModelIndex &index, const QModelIndex &parent)
{
    if (index == parent)
        return true;
    QModelIndex current = index;
    while (current.isValid()) {
        current = current.parent();
        if (current == parent)
            return true;
    }
    return false;
}

con's avatar
con committed
233
/*!
234 235 236
    \class FolderNavigationWidget

    Shows a file system tree, with the root directory selectable from a dropdown.
con's avatar
con committed
237

238 239
    \internal
*/
240
FolderNavigationWidget::FolderNavigationWidget(QWidget *parent) : QWidget(parent),
241
    m_listView(new Utils::NavigationTreeView(this)),
242 243
    m_fileSystemModel(new FolderNavigationModel(this)),
    m_filterHiddenFilesAction(new QAction(tr("Show Hidden Files"), this)),
244
    m_toggleSync(new QToolButton(this)),
245
    m_rootSelector(new QComboBox),
246
    m_crumbLabel(new DelayedFileCrumbLabel(this))
con's avatar
con committed
247
{
248 249 250 251 252
    m_context = new Core::IContext(this);
    m_context->setContext(Core::Context(C_FOLDERNAVIGATIONWIDGET));
    m_context->setWidget(this);
    Core::ICore::addContextObject(m_context);

253 254
    setBackgroundRole(QPalette::Base);
    setAutoFillBackground(true);
255
    m_fileSystemModel->setResolveSymlinks(false);
256
    m_fileSystemModel->setIconProvider(Core::FileIconProvider::iconProvider());
257
    QDir::Filters filters = QDir::AllEntries | QDir::NoDotAndDotDot;
258 259
    if (Utils::HostOsInfo::isWindowsHost()) // Symlinked directories can cause file watcher warnings on Win32.
        filters |= QDir::NoSymLinks;
260
    m_fileSystemModel->setFilter(filters);
261
    m_fileSystemModel->setRootPath(QString());
262 263
    m_filterHiddenFilesAction->setCheckable(true);
    setHiddenFilesFilter(false);
264
    m_listView->setIconSize(QSize(16,16));
265
    m_listView->setModel(m_fileSystemModel);
266
    m_listView->setEditTriggers(QAbstractItemView::NoEditTriggers);
267 268
    m_listView->setDragEnabled(true);
    m_listView->setDragDropMode(QAbstractItemView::DragOnly);
269
    showOnlyFirstColumn(m_listView);
270
    setFocusProxy(m_listView);
con's avatar
con committed
271

272 273 274 275 276 277
    auto selectorWidget = new QWidget(this);
    auto selectorLayout = new QVBoxLayout(selectorWidget);
    selectorWidget->setLayout(selectorLayout);
    selectorLayout->setContentsMargins(0, 0, 0, 0);
    selectorLayout->addWidget(m_rootSelector);

278 279 280 281 282 283
    auto crumbLayout = new QVBoxLayout;
    crumbLayout->setSpacing(0);
    crumbLayout->setContentsMargins(4, 4, 4, 4);
    crumbLayout->addWidget(m_crumbLabel);
    m_crumbLabel->setAlignment(Qt::AlignLeft | Qt::AlignTop);

284
    auto layout = new QVBoxLayout();
285
    layout->addWidget(selectorWidget);
286 287
    layout->addLayout(crumbLayout);
    layout->addWidget(createHLine());
288
    layout->addWidget(m_listView);
con's avatar
con committed
289 290 291 292
    layout->setSpacing(0);
    layout->setContentsMargins(0, 0, 0, 0);
    setLayout(layout);

Ulf Hermann's avatar
Ulf Hermann committed
293
    m_toggleSync->setIcon(Utils::Icons::LINK.icon());
294 295 296
    m_toggleSync->setCheckable(true);
    m_toggleSync->setToolTip(tr("Synchronize with Editor"));

con's avatar
con committed
297
    // connections
298
    connect(m_listView, &QAbstractItemView::activated,
299
            this, [this](const QModelIndex &index) { openItem(index); });
300 301
    // use QueuedConnection for updating crumble path, because that can scroll, which doesn't
    // work well when done directly in currentChanged (the wrong item can get highlighted)
302 303 304
    connect(m_listView->selectionModel(),
            &QItemSelectionModel::currentChanged,
            this,
305 306
            &FolderNavigationWidget::setCrumblePath,
            Qt::QueuedConnection);
307 308 309 310 311 312 313 314 315 316 317
    connect(m_crumbLabel, &Utils::FileCrumbLabel::pathClicked, [this](const Utils::FileName &path) {
        const QModelIndex rootIndex = m_listView->rootIndex();
        const QModelIndex fileIndex = m_fileSystemModel->index(path.toString());
        if (!isChildOf(fileIndex, rootIndex))
            selectBestRootForFile(path);
        selectFile(path);
    });
    connect(m_filterHiddenFilesAction,
            &QAction::toggled,
            this,
            &FolderNavigationWidget::setHiddenFilesFilter);
318 319
    connect(m_toggleSync, &QAbstractButton::clicked,
            this, &FolderNavigationWidget::toggleAutoSynchronization);
320 321 322 323 324 325 326
    connect(m_rootSelector,
            static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
            this,
            [this](int index) {
                const auto directory = m_rootSelector->itemData(index).value<Utils::FileName>();
                m_rootSelector->setToolTip(directory.toString());
                setRootDirectory(directory);
327 328 329 330
                const QModelIndex rootIndex = m_listView->rootIndex();
                const QModelIndex fileIndex = m_listView->currentIndex();
                if (!isChildOf(fileIndex, rootIndex))
                    selectFile(directory);
331
            });
332 333

    setAutoSynchronization(true);
con's avatar
con committed
334 335
}

336 337 338 339 340
FolderNavigationWidget::~FolderNavigationWidget()
{
    Core::ICore::removeContextObject(m_context);
}

con's avatar
con committed
341 342 343 344 345
void FolderNavigationWidget::toggleAutoSynchronization()
{
    setAutoSynchronization(!m_autoSync);
}

346 347 348 349 350 351 352 353 354
static bool itemLessThan(QComboBox *combo,
                         int index,
                         const FolderNavigationWidgetFactory::RootDirectory &directory)
{
    return combo->itemData(index, SORT_ROLE).toInt() < directory.sortValue
           || (combo->itemData(index, SORT_ROLE).toInt() == directory.sortValue
               && combo->itemData(index, Qt::DisplayRole).toString() < directory.displayName);
}

355
void FolderNavigationWidget::insertRootDirectory(
356
    const FolderNavigationWidgetFactory::RootDirectory &directory)
357
{
358 359 360 361 362 363
    // Find existing. Do not remove yet, to not mess up the current selection.
    int previousIndex = 0;
    while (previousIndex < m_rootSelector->count()
           && m_rootSelector->itemData(previousIndex, ID_ROLE).toString() != directory.id)
        ++previousIndex;
    // Insert sorted.
364
    int index = 0;
365
    while (index < m_rootSelector->count() && itemLessThan(m_rootSelector, index, directory))
366
        ++index;
367 368 369
    m_rootSelector->insertItem(index, directory.displayName);
    if (index <= previousIndex) // item was inserted, update previousIndex
        ++previousIndex;
370 371
    m_rootSelector->setItemData(index, qVariantFromValue(directory.path), PATH_ROLE);
    m_rootSelector->setItemData(index, directory.id, ID_ROLE);
372
    m_rootSelector->setItemData(index, directory.sortValue, SORT_ROLE);
373
    m_rootSelector->setItemData(index, directory.path.toUserOutput(), Qt::ToolTipRole);
374
    m_rootSelector->setItemIcon(index, directory.icon);
375 376 377 378
    if (m_rootSelector->currentIndex() == previousIndex)
        m_rootSelector->setCurrentIndex(index);
    if (previousIndex < m_rootSelector->count())
        m_rootSelector->removeItem(previousIndex);
379 380 381 382
    if (m_autoSync) // we might find a better root for current selection now
        setCurrentEditor(Core::EditorManager::currentEditor());
}

383
void FolderNavigationWidget::removeRootDirectory(const QString &id)
384 385
{
    for (int i = 0; i < m_rootSelector->count(); ++i) {
386
        if (m_rootSelector->itemData(i, ID_ROLE).toString() == id) {
387 388 389 390 391 392 393 394
            m_rootSelector->removeItem(i);
            break;
        }
    }
    if (m_autoSync) // we might need to find a new root for current selection
        setCurrentEditor(Core::EditorManager::currentEditor());
}

395 396 397 398 399 400 401
void FolderNavigationWidget::editCurrentItem()
{
    const QModelIndex current = m_listView->currentIndex();
    if (m_fileSystemModel->flags(current) & Qt::ItemIsEditable)
        m_listView->edit(current);
}

402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446
static QVector<FolderNode *> removableFolderNodes(const Utils::FileName &filePath)
{
    QVector<FolderNode *> folderNodes;
    ProjectTree::forEachNode([&](Node *node) {
        if (node->nodeType() == NodeType::File && node->filePath() == filePath
                && node->parentFolderNode()
                && node->parentFolderNode()->supportsAction(RemoveFile, node)) {
            folderNodes.append(node->parentFolderNode());
        }
    });
    return folderNodes;
}

void FolderNavigationWidget::removeCurrentItem()
{
    const QModelIndex current = m_listView->currentIndex();
    if (!current.isValid() || m_fileSystemModel->isDir(current))
        return;
    const QString filePath = m_fileSystemModel->filePath(current);
    Utils::RemoveFileDialog dialog(filePath, Core::ICore::dialogParent());
    dialog.setDeleteFileVisible(false);
    if (dialog.exec() == QDialog::Accepted) {
        const QVector<FolderNode *> folderNodes = removableFolderNodes(
            Utils::FileName::fromString(filePath));
        const QVector<FolderNode *> failedNodes = Utils::filtered(folderNodes,
                                                                  [filePath](FolderNode *folder) {
                                                                      return !folder->removeFiles(
                                                                          {filePath});
                                                                  });
        Core::FileChangeBlocker changeGuard(filePath);
        Core::FileUtils::removeFile(filePath, true /*delete from disk*/);
        if (!failedNodes.isEmpty()) {
            const QString projects = projectNames(failedNodes).join(", ");
            const QString errorMessage
                = tr("The following projects failed to automatically remove the file: %1")
                      .arg(projects);
            QTimer::singleShot(0, Core::ICore::instance(), [errorMessage] {
                QMessageBox::warning(Core::ICore::dialogParent(),
                                     ProjectExplorerPlugin::tr("Project Editing Failed"),
                                     errorMessage);
            });
        }
    }
}

con's avatar
con committed
447 448 449 450 451 452 453
bool FolderNavigationWidget::autoSynchronization() const
{
    return m_autoSync;
}

void FolderNavigationWidget::setAutoSynchronization(bool sync)
{
454
    m_toggleSync->setChecked(sync);
con's avatar
con committed
455 456 457 458 459 460
    if (sync == m_autoSync)
        return;

    m_autoSync = sync;

    if (m_autoSync) {
461
        connect(Core::EditorManager::instance(), &Core::EditorManager::currentEditorChanged,
462 463
                this, &FolderNavigationWidget::setCurrentEditor);
        setCurrentEditor(Core::EditorManager::currentEditor());
con's avatar
con committed
464
    } else {
465
        disconnect(Core::EditorManager::instance(), &Core::EditorManager::currentEditorChanged,
466
                this, &FolderNavigationWidget::setCurrentEditor);
con's avatar
con committed
467 468 469
    }
}

470
void FolderNavigationWidget::setCurrentEditor(Core::IEditor *editor)
con's avatar
con committed
471
{
472
    if (!editor || editor->document()->filePath().isEmpty() || editor->document()->isTemporary())
473
        return;
474
    const Utils::FileName filePath = editor->document()->filePath();
475 476 477 478 479 480
    selectBestRootForFile(filePath);
    selectFile(filePath);
}

void FolderNavigationWidget::selectBestRootForFile(const Utils::FileName &filePath)
{
481 482
    const int bestRootIndex = bestRootForFile(filePath);
    m_rootSelector->setCurrentIndex(bestRootIndex);
483 484
}

485
void FolderNavigationWidget::selectFile(const Utils::FileName &filePath)
486
{
487
    const QModelIndex fileIndex = m_fileSystemModel->index(filePath.toString());
488
    if (fileIndex.isValid() || filePath.isEmpty() /* Computer root */) {
489 490 491 492 493 494 495
        // TODO This only scrolls to the right position if all directory contents are loaded.
        // Unfortunately listening to directoryLoaded was still not enough (there might also
        // be some delayed sorting involved?).
        // Use magic timer for scrolling.
        m_listView->setCurrentIndex(fileIndex);
        QTimer::singleShot(200, this, [this, filePath] {
            const QModelIndex fileIndex = m_fileSystemModel->index(filePath.toString());
496 497 498 499 500 501
            if (fileIndex == m_listView->rootIndex()) {
                m_listView->horizontalScrollBar()->setValue(0);
                m_listView->verticalScrollBar()->setValue(0);
            } else {
                m_listView->scrollTo(fileIndex);
            }
502
        });
503
    }
con's avatar
con committed
504 505
}

506
void FolderNavigationWidget::setRootDirectory(const Utils::FileName &directory)
con's avatar
con committed
507
{
508 509
    const QModelIndex index = m_fileSystemModel->setRootPath(directory.toString());
    m_listView->setRootIndex(index);
510 511
}

512
int FolderNavigationWidget::bestRootForFile(const Utils::FileName &filePath)
513
{
514 515 516 517 518 519 520 521 522 523
    int index = 0; // Computer is default
    int commonLength = 0;
    for (int i = 1; i < m_rootSelector->count(); ++i) {
        const auto root = m_rootSelector->itemData(i).value<Utils::FileName>();
        if (filePath.isChildOf(root) && root.length() > commonLength) {
            index = i;
            commonLength = root.length();
        }
    }
    return index;
524 525
}

526
void FolderNavigationWidget::openItem(const QModelIndex &index)
527
{
528 529 530 531
    QTC_ASSERT(index.isValid(), return);
    // signal "activate" is also sent when double-clicking folders
    // but we don't want to do anything in that case
    if (m_fileSystemModel->isDir(index))
532
        return;
533
    const QString path = m_fileSystemModel->filePath(index);
534 535 536
    Core::EditorManager::openEditor(path);
}

537
QStringList FolderNavigationWidget::projectsInDirectory(const QModelIndex &index) const
538
{
539
    QTC_ASSERT(index.isValid() && m_fileSystemModel->isDir(index), return {});
540 541
    const QFileInfo fi = m_fileSystemModel->fileInfo(index);
    if (!fi.isReadable() || !fi.isExecutable())
542
        return {};
543 544
    const QString path = m_fileSystemModel->filePath(index);
    // Try to find project files in directory and open those.
545 546 547 548 549 550
    return FolderNavigationWidget::projectFilesInDirectory(path);
}

void FolderNavigationWidget::openProjectsInDirectory(const QModelIndex &index)
{
    const QStringList projectFiles = projectsInDirectory(index);
551 552
    if (!projectFiles.isEmpty())
        Core::ICore::instance()->openFiles(projectFiles);
553 554
}

555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575
void FolderNavigationWidget::setCrumblePath(const QModelIndex &index, const QModelIndex &)
{
    const int width = m_crumbLabel->width();
    const int previousHeight = m_crumbLabel->immediateHeightForWidth(width);
    m_crumbLabel->setPath(Utils::FileName::fromString(m_fileSystemModel->filePath(index)));
    const int currentHeight = m_crumbLabel->immediateHeightForWidth(width);
    const int diff = currentHeight - previousHeight;
    if (diff != 0 && m_crumbLabel->isVisible()) {
        // try to fix scroll position, otherwise delay layouting
        QScrollBar *bar = m_listView->verticalScrollBar();
        const int newBarValue = bar ? bar->value() + diff : 0;
        if (bar && bar->minimum() <= newBarValue && bar->maximum() >= newBarValue) {
            // we need to set the scroll bar when the layout request from the crumble path is
            // handled, otherwise it will flicker
            m_crumbLabel->setScrollBarOnce(bar, newBarValue);
        } else {
            m_crumbLabel->delayLayoutOnce();
        }
    }
}

576 577 578 579
void FolderNavigationWidget::contextMenuEvent(QContextMenuEvent *ev)
{
    QMenu menu;
    // Open current item
580
    const QModelIndex current = m_listView->currentIndex();
581
    const bool hasCurrentItem = current.isValid();
582 583
    QAction *actionOpenFile = nullptr;
    QAction *actionOpenProjects = nullptr;
584
    QAction *actionOpenAsProject = nullptr;
585
    const bool isDir = m_fileSystemModel->isDir(current);
586 587 588
    const Utils::FileName filePath = hasCurrentItem ? Utils::FileName::fromString(
                                                          m_fileSystemModel->filePath(current))
                                                    : Utils::FileName();
589 590
    if (hasCurrentItem) {
        const QString fileName = m_fileSystemModel->fileName(current);
591
        if (isDir) {
592
            actionOpenProjects = menu.addAction(tr("Open Project in \"%1\"").arg(fileName));
593 594
            if (projectsInDirectory(current).isEmpty())
                actionOpenProjects->setEnabled(false);
595
        } else {
596
            actionOpenFile = menu.addAction(tr("Open \"%1\"").arg(fileName));
597 598 599
            if (ProjectExplorerPlugin::isProjectFile(Utils::FileName::fromString(fileName)))
                actionOpenAsProject = menu.addAction(tr("Open Project \"%1\"").arg(fileName));
        }
600
    }
601 602 603 604 605

    // we need dummy DocumentModel::Entry with absolute file path in it
    // to get EditorManager::addNativeDirAndOpenWithActions() working
    Core::DocumentModel::Entry fakeEntry;
    Core::IDocument document;
606
    document.setFilePath(filePath);
607 608 609
    fakeEntry.document = &document;
    Core::EditorManager::addNativeDirAndOpenWithActions(&menu, &fakeEntry);

610
    if (hasCurrentItem) {
611 612
        if (!isDir)
            menu.addAction(Core::ActionManager::command(Constants::REMOVEFILE)->action());
613 614 615
        if (m_fileSystemModel->flags(current) & Qt::ItemIsEditable)
            menu.addAction(Core::ActionManager::command(Constants::RENAMEFILE)->action());
        if (!isDir && Core::DiffService::instance()) {
616 617 618 619 620 621 622
            menu.addAction(
                TextEditor::TextDocument::createDiffAgainstCurrentFileAction(&menu, [filePath]() {
                    return filePath;
                }));
        }
    }

623 624 625 626 627
    QAction *action = menu.exec(ev->globalPos());
    if (!action)
        return;

    ev->accept();
628
    if (action == actionOpenFile)
629
        openItem(current);
630 631
    else if (action == actionOpenAsProject)
        ProjectExplorerPlugin::openProject(filePath.toString());
632 633
    else if (action == actionOpenProjects)
        openProjectsInDirectory(current);
634 635
}

636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651
void FolderNavigationWidget::setHiddenFilesFilter(bool filter)
{
    QDir::Filters filters = m_fileSystemModel->filter();
    if (filter)
        filters |= QDir::Hidden;
    else
        filters &= ~(QDir::Hidden);
    m_fileSystemModel->setFilter(filters);
    m_filterHiddenFilesAction->setChecked(filter);
}

bool FolderNavigationWidget::hiddenFilesFilter() const
{
    return m_filterHiddenFilesAction->isChecked();
}

652 653 654 655 656 657 658 659 660
QStringList FolderNavigationWidget::projectFilesInDirectory(const QString &path)
{
    QDir dir(path);
    QStringList projectFiles;
    foreach (const QFileInfo &i, dir.entryInfoList(ProjectExplorerPlugin::projectFileGlobs(), QDir::Files))
        projectFiles.append(i.absoluteFilePath());
    return projectFiles;
}

661
// --------------------FolderNavigationWidgetFactory
hjk's avatar
hjk committed
662
FolderNavigationWidgetFactory::FolderNavigationWidgetFactory()
con's avatar
con committed
663
{
664
    m_instance = this;
665 666 667 668
    setDisplayName(tr("File System"));
    setPriority(400);
    setId("File System");
    setActivationSequence(QKeySequence(Core::UseMacShortcuts ? tr("Meta+Y") : tr("Alt+Y")));
669 670 671
    insertRootDirectory({QLatin1String("A.Computer"),
                         0 /*sortValue*/,
                         FolderNavigationWidget::tr("Computer"),
672 673
                         Utils::FileName(),
                         Icons::DESKTOP_DEVICE_SMALL.icon()});
674
    insertRootDirectory({QLatin1String("A.Home"),
675
                         10 /*sortValue*/,
676
                         FolderNavigationWidget::tr("Home"),
677 678
                         Utils::FileName::fromString(QDir::homePath()),
                         Utils::Icons::HOME.icon()});
679 680 681 682 683
    updateProjectsDirectoryRoot();
    connect(Core::DocumentManager::instance(),
            &Core::DocumentManager::projectsDirectoryChanged,
            this,
            &FolderNavigationWidgetFactory::updateProjectsDirectoryRoot);
684
    registerActions();
con's avatar
con committed
685 686 687 688
}

Core::NavigationView FolderNavigationWidgetFactory::createWidget()
{
689
    auto fnw = new FolderNavigationWidget;
690
    for (const RootDirectory &root : m_rootDirectories)
691
        fnw->insertRootDirectory(root);
692 693 694
    connect(this,
            &FolderNavigationWidgetFactory::rootDirectoryAdded,
            fnw,
695
            &FolderNavigationWidget::insertRootDirectory);
696 697 698 699 700 701
    connect(this,
            &FolderNavigationWidgetFactory::rootDirectoryRemoved,
            fnw,
            &FolderNavigationWidget::removeRootDirectory);

    Core::NavigationView n;
702
    n.widget = fnw;
703
    auto filter = new QToolButton;
Ulf Hermann's avatar
Ulf Hermann committed
704
    filter->setIcon(Utils::Icons::FILTER.icon());
705 706 707
    filter->setToolTip(tr("Filter Files"));
    filter->setPopupMode(QToolButton::InstantPopup);
    filter->setProperty("noArrow", true);
708
    auto filterMenu = new QMenu(filter);
709 710 711
    filterMenu->addAction(fnw->m_filterHiddenFilesAction);
    filter->setMenu(filterMenu);
    n.dockToolBarWidgets << filter << fnw->m_toggleSync;
con's avatar
con committed
712 713 714
    return n;
}

Serhii Moroz's avatar
Serhii Moroz committed
715
void FolderNavigationWidgetFactory::saveSettings(QSettings *settings, int position, QWidget *widget)
716
{
717
    auto fnw = qobject_cast<FolderNavigationWidget *>(widget);
718 719 720 721 722 723
    QTC_ASSERT(fnw, return);
    const QString baseKey = QLatin1String("FolderNavigationWidget.") + QString::number(position);
    settings->setValue(baseKey + QLatin1String(".HiddenFilesFilter"), fnw->hiddenFilesFilter());
    settings->setValue(baseKey + QLatin1String(".SyncWithEditor"), fnw->autoSynchronization());
}

Serhii Moroz's avatar
Serhii Moroz committed
724
void FolderNavigationWidgetFactory::restoreSettings(QSettings *settings, int position, QWidget *widget)
725
{
726
    auto fnw = qobject_cast<FolderNavigationWidget *>(widget);
727 728 729 730 731
    QTC_ASSERT(fnw, return);
    const QString baseKey = QLatin1String("FolderNavigationWidget.") + QString::number(position);
    fnw->setHiddenFilesFilter(settings->value(baseKey + QLatin1String(".HiddenFilesFilter"), false).toBool());
    fnw->setAutoSynchronization(settings->value(baseKey +  QLatin1String(".SyncWithEditor"), true).toBool());
}
732

733
void FolderNavigationWidgetFactory::insertRootDirectory(const RootDirectory &directory)
734
{
735 736 737
    const int index = rootIndex(directory.id);
    if (index < 0)
        m_rootDirectories.append(directory);
738 739
    else
        m_rootDirectories[index] = directory;
740
    emit m_instance->rootDirectoryAdded(directory);
741 742
}

743
void FolderNavigationWidgetFactory::removeRootDirectory(const QString &id)
744
{
745
    const int index = rootIndex(id);
746
    QTC_ASSERT(index >= 0, return );
747
    m_rootDirectories.removeAt(index);
748
    emit m_instance->rootDirectoryRemoved(id);
749 750
}

751 752 753 754 755 756 757 758 759
int FolderNavigationWidgetFactory::rootIndex(const QString &id)
{
    return Utils::indexOf(m_rootDirectories,
                          [id](const RootDirectory &entry) { return entry.id == id; });
}

void FolderNavigationWidgetFactory::updateProjectsDirectoryRoot()
{
    insertRootDirectory({QLatin1String(PROJECTSDIRECTORYROOT_ID),
760
                         20 /*sortValue*/,
761
                         FolderNavigationWidget::tr("Projects"),
762 763
                         Core::DocumentManager::projectsDirectory(),
                         Utils::Icons::PROJECT.icon()});
764 765
}

766 767 768 769 770
static FolderNavigationWidget *currentFolderNavigationWidget()
{
    return qobject_cast<FolderNavigationWidget *>(Core::ICore::currentContextWidget());
}

771 772 773
void FolderNavigationWidgetFactory::registerActions()
{
    Core::Context context(C_FOLDERNAVIGATIONWIDGET);
774

775 776 777
    auto rename = new QAction(this);
    Core::ActionManager::registerAction(rename, Constants::RENAMEFILE, context);
    connect(rename, &QAction::triggered, Core::ICore::instance(), [] {
778
        if (auto navWidget = currentFolderNavigationWidget())
779 780
            navWidget->editCurrentItem();
    });
781 782 783 784 785 786 787

    auto remove = new QAction(this);
    Core::ActionManager::registerAction(remove, Constants::REMOVEFILE, context);
    connect(remove, &QAction::triggered, Core::ICore::instance(), [] {
        if (auto navWidget = currentFolderNavigationWidget())
            navWidget->removeCurrentItem();
    });
788 789
}

790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832
int DelayedFileCrumbLabel::immediateHeightForWidth(int w) const
{
    return Utils::FileCrumbLabel::heightForWidth(w);
}

int DelayedFileCrumbLabel::heightForWidth(int w) const
{
    static QHash<int, int> oldHeight;
    setScrollBarOnce();
    int newHeight = Utils::FileCrumbLabel::heightForWidth(w);
    if (!m_delaying || !oldHeight.contains(w)) {
        oldHeight.insert(w, newHeight);
    } else if (oldHeight.value(w) != newHeight){
        auto that = const_cast<DelayedFileCrumbLabel *>(this);
        QTimer::singleShot(QApplication::doubleClickInterval(), that, [that, w, newHeight] {
            oldHeight.insert(w, newHeight);
            that->m_delaying = false;
            that->updateGeometry();
        });
    }
    return oldHeight.value(w);
}

void DelayedFileCrumbLabel::delayLayoutOnce()
{
    m_delaying = true;
}

void DelayedFileCrumbLabel::setScrollBarOnce(QScrollBar *bar, int value)
{
    m_bar = bar;
    m_barValue = value;
}

void DelayedFileCrumbLabel::setScrollBarOnce() const
{
    if (!m_bar)
        return;
    auto that = const_cast<DelayedFileCrumbLabel *>(this);
    that->m_bar->setValue(m_barValue);
    that->m_bar.clear();
}

833 834
} // namespace Internal
} // namespace ProjectExplorer