/**************************************************************************** ** ** Copyright (C) 2016 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of Qt Creator. ** ** 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 ** 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. ** ** 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. ** ****************************************************************************/ #include "foldernavigationwidget.h" #include "projectexplorer.h" #include "projectexplorerconstants.h" #include "projectexplorericons.h" #include "projectnodes.h" #include "projecttree.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include const int PATH_ROLE = Qt::UserRole; const int ID_ROLE = Qt::UserRole + 1; const int SORT_ROLE = Qt::UserRole + 2; const char PROJECTSDIRECTORYROOT_ID[] = "A.Projects"; const char C_FOLDERNAVIGATIONWIDGET[] = "ProjectExplorer.FolderNavigationWidget"; const char kSettingsBase[] = "FolderNavigationWidget."; const char kHiddenFilesKey[] = ".HiddenFilesFilter"; const char kSyncKey[] = ".SyncWithEditor"; const char kShowBreadCrumbs[] = ".ShowBreadCrumbs"; const char kSyncRootWithEditor[] = ".SyncRootWithEditor"; namespace ProjectExplorer { namespace Internal { static FolderNavigationWidgetFactory *m_instance = nullptr; QVector FolderNavigationWidgetFactory::m_rootDirectories; static QWidget *createHLine() { auto widget = new QFrame; widget->setFrameStyle(QFrame::Plain | QFrame::HLine); return widget; } // 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 m_bar; int m_barValue = 0; bool m_delaying = false; }; // FolderNavigationModel: Shows path as tooltip. class FolderNavigationModel : public QFileSystemModel { public: enum Roles { IsFolderRole = Qt::UserRole + 50 // leave some gap for the custom roles in QFileSystemModel }; explicit FolderNavigationModel(QObject *parent = nullptr); 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; }; // Sorts folders on top if wanted class FolderSortProxyModel : public QSortFilterProxyModel { public: FolderSortProxyModel(QObject *parent = nullptr); protected: bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const override; }; FolderSortProxyModel::FolderSortProxyModel(QObject *parent) : QSortFilterProxyModel(parent) { } bool FolderSortProxyModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const { const QAbstractItemModel *src = sourceModel(); if (sortRole() == FolderNavigationModel::IsFolderRole) { const bool leftIsFolder = src->data(source_left, FolderNavigationModel::IsFolderRole) .toBool(); const bool rightIsFolder = src->data(source_right, FolderNavigationModel::IsFolderRole) .toBool(); if (leftIsFolder != rightIsFolder) return leftIsFolder; } const QString leftName = src->data(source_left, QFileSystemModel::FileNameRole).toString(); const QString rightName = src->data(source_right, QFileSystemModel::FileNameRole).toString(); return Utils::FilePath::fromString(leftName) < Utils::FilePath::fromString(rightName); } FolderNavigationModel::FolderNavigationModel(QObject *parent) : QFileSystemModel(parent) { } QVariant FolderNavigationModel::data(const QModelIndex &index, int role) const { if (role == Qt::ToolTipRole) return QDir::toNativeSeparators(QDir::cleanPath(filePath(index))); else if (role == IsFolderRole) return isDir(index); else return QFileSystemModel::data(index, role); } Qt::DropActions FolderNavigationModel::supportedDragActions() const { return Qt::MoveAction; } 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 renamableFolderNodes(const Utils::FilePath &before, const Utils::FilePath &after) { QVector folderNodes; ProjectTree::forEachNode([&](Node *node) { if (node->asFileNode() && node->filePath() == before && node->parentFolderNode() && node->parentFolderNode()->canRenameFile(before.toString(), after.toString())) { folderNodes.append(node->parentFolderNode()); } }); return folderNodes; } static QStringList projectNames(const QVector &folders) { const QStringList names = Utils::transform(folders, [](FolderNode *n) { return n->managingProject()->filePath().fileName(); }); return Utils::filteredUnique(names); } 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(), 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 folderNodes = renamableFolderNodes(Utils::FilePath::fromString(beforeFilePath), Utils::FilePath::fromString(afterFilePath)); QVector failedNodes; for (FolderNode *folder : folderNodes) { if (!folder->renameFile(beforeFilePath, afterFilePath)) failedNodes.append(folder); } if (!failedNodes.isEmpty()) { const QString projects = projectNames(failedNodes).join(", "); const QString errorMessage = FolderNavigationWidget::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; } static void showOnlyFirstColumn(QTreeView *view) { const int columnCount = view->header()->count(); for (int i = 1; i < columnCount; ++i) view->setColumnHidden(i, true); } 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; } /*! \class FolderNavigationWidget Shows a file system tree, with the root directory selectable from a dropdown. \internal */ FolderNavigationWidget::FolderNavigationWidget(QWidget *parent) : QWidget(parent), m_listView(new Utils::NavigationTreeView(this)), m_fileSystemModel(new FolderNavigationModel(this)), m_sortProxyModel(new FolderSortProxyModel(m_fileSystemModel)), m_filterHiddenFilesAction(new QAction(tr("Show Hidden Files"), this)), m_showBreadCrumbsAction(new QAction(tr("Show Bread Crumbs"), this)), m_showFoldersOnTopAction(new QAction(tr("Show Folders on Top"), this)), m_toggleSync(new QToolButton(this)), m_toggleRootSync(new QToolButton(this)), m_rootSelector(new QComboBox), m_crumbContainer(new QWidget(this)), m_crumbLabel(new DelayedFileCrumbLabel(this)) { auto context = new Core::IContext(this); context->setContext(Core::Context(C_FOLDERNAVIGATIONWIDGET)); context->setWidget(this); Core::ICore::addContextObject(context); setBackgroundRole(QPalette::Base); setAutoFillBackground(true); m_sortProxyModel->setSourceModel(m_fileSystemModel); m_sortProxyModel->setSortRole(FolderNavigationModel::IsFolderRole); m_sortProxyModel->sort(0); m_fileSystemModel->setResolveSymlinks(false); m_fileSystemModel->setIconProvider(Core::FileIconProvider::iconProvider()); QDir::Filters filters = QDir::AllEntries | QDir::NoDotAndDotDot; if (Utils::HostOsInfo::isWindowsHost()) // Symlinked directories can cause file watcher warnings on Win32. filters |= QDir::NoSymLinks; m_fileSystemModel->setFilter(filters); m_fileSystemModel->setRootPath(QString()); m_filterHiddenFilesAction->setCheckable(true); setHiddenFilesFilter(false); m_showBreadCrumbsAction->setCheckable(true); setShowBreadCrumbs(true); m_showFoldersOnTopAction->setCheckable(true); setShowFoldersOnTop(true); m_listView->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); m_listView->setIconSize(QSize(16,16)); m_listView->setModel(m_sortProxyModel); m_listView->setEditTriggers(QAbstractItemView::NoEditTriggers); m_listView->setDragEnabled(true); m_listView->setDragDropMode(QAbstractItemView::DragOnly); showOnlyFirstColumn(m_listView); setFocusProxy(m_listView); auto selectorWidget = new Utils::StyledBar(this); selectorWidget->setLightColored(true); auto selectorLayout = new QHBoxLayout(selectorWidget); selectorWidget->setLayout(selectorLayout); selectorLayout->setSpacing(0); selectorLayout->setContentsMargins(0, 0, 0, 0); selectorLayout->addWidget(m_rootSelector, 10); auto crumbContainerLayout = new QVBoxLayout; crumbContainerLayout->setSpacing(0); crumbContainerLayout->setContentsMargins(0, 0, 0, 0); m_crumbContainer->setLayout(crumbContainerLayout); auto crumbLayout = new QVBoxLayout; crumbLayout->setSpacing(0); crumbLayout->setContentsMargins(4, 4, 4, 4); crumbLayout->addWidget(m_crumbLabel); crumbContainerLayout->addLayout(crumbLayout); crumbContainerLayout->addWidget(createHLine()); m_crumbLabel->setAlignment(Qt::AlignLeft | Qt::AlignTop); auto layout = new QVBoxLayout(); layout->addWidget(selectorWidget); layout->addWidget(m_crumbContainer); layout->addWidget(m_listView); layout->setSpacing(0); layout->setContentsMargins(0, 0, 0, 0); setLayout(layout); m_toggleSync->setIcon(Utils::Icons::LINK_TOOLBAR.icon()); m_toggleSync->setCheckable(true); m_toggleSync->setToolTip(tr("Synchronize with Editor")); m_toggleRootSync->setIcon(Utils::Icons::LINK.icon()); m_toggleRootSync->setCheckable(true); m_toggleRootSync->setToolTip(tr("Synchronize Root Directory with Editor")); selectorLayout->addWidget(m_toggleRootSync); // connections connect(Core::EditorManager::instance(), &Core::EditorManager::currentEditorChanged, this, &FolderNavigationWidget::handleCurrentEditorChanged); connect(m_listView, &QAbstractItemView::activated, this, [this](const QModelIndex &index) { openItem(m_sortProxyModel->mapToSource(index)); }); // Delay updating crumble path by event loop cylce, because that can scroll, which doesn't // work well when done directly in currentChanged (the wrong item can get highlighted). // We cannot use Qt::QueuedConnection directly, because the QModelIndex could get invalidated // in the meantime, so use a queued invokeMethod instead. connect(m_listView->selectionModel(), &QItemSelectionModel::currentChanged, this, [this](const QModelIndex &index) { const QModelIndex sourceIndex = m_sortProxyModel->mapToSource(index); const auto filePath = Utils::FilePath::fromString( m_fileSystemModel->filePath(sourceIndex)); // QTimer::singleShot only posts directly onto the event loop if you use the SLOT("...") // notation, so using a singleShot with a lambda would flicker // QTimer::singleShot(0, this, [this, filePath]() { setCrumblePath(filePath); }); QMetaObject::invokeMethod(this, "setCrumblePath", Qt::QueuedConnection, Q_ARG(Utils::FilePath, filePath)); }); connect(m_crumbLabel, &Utils::FileCrumbLabel::pathClicked, [this](const Utils::FilePath &path) { const QModelIndex rootIndex = m_sortProxyModel->mapToSource(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); connect(m_showBreadCrumbsAction, &QAction::toggled, this, &FolderNavigationWidget::setShowBreadCrumbs); connect(m_showFoldersOnTopAction, &QAction::toggled, this, &FolderNavigationWidget::setShowFoldersOnTop); connect(m_toggleSync, &QAbstractButton::clicked, this, &FolderNavigationWidget::toggleAutoSynchronization); connect(m_toggleRootSync, &QAbstractButton::clicked, this, [this]() { setRootAutoSynchronization(!m_rootAutoSync); }); connect(m_rootSelector, QOverload::of(&QComboBox::currentIndexChanged), this, [this](int index) { const auto directory = m_rootSelector->itemData(index).value(); m_rootSelector->setToolTip(directory.toString()); setRootDirectory(directory); const QModelIndex rootIndex = m_sortProxyModel->mapToSource(m_listView->rootIndex()); const QModelIndex fileIndex = m_sortProxyModel->mapToSource(m_listView->currentIndex()); if (!isChildOf(fileIndex, rootIndex)) selectFile(directory); }); setAutoSynchronization(true); setRootAutoSynchronization(true); } void FolderNavigationWidget::toggleAutoSynchronization() { setAutoSynchronization(!m_autoSync); } void FolderNavigationWidget::setShowBreadCrumbs(bool show) { m_showBreadCrumbsAction->setChecked(show); m_crumbContainer->setVisible(show); } void FolderNavigationWidget::setShowFoldersOnTop(bool onTop) { m_showFoldersOnTopAction->setChecked(onTop); m_sortProxyModel->setSortRole(onTop ? int(FolderNavigationModel::IsFolderRole) : int(QFileSystemModel::FileNameRole)); } 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); } void FolderNavigationWidget::insertRootDirectory( const FolderNavigationWidgetFactory::RootDirectory &directory) { // 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. int index = 0; while (index < m_rootSelector->count() && itemLessThan(m_rootSelector, index, directory)) ++index; m_rootSelector->insertItem(index, directory.displayName); if (index <= previousIndex) // item was inserted, update previousIndex ++previousIndex; m_rootSelector->setItemData(index, QVariant::fromValue(directory.path), PATH_ROLE); m_rootSelector->setItemData(index, directory.id, ID_ROLE); m_rootSelector->setItemData(index, directory.sortValue, SORT_ROLE); m_rootSelector->setItemData(index, directory.path.toUserOutput(), Qt::ToolTipRole); m_rootSelector->setItemIcon(index, directory.icon); if (m_rootSelector->currentIndex() == previousIndex) m_rootSelector->setCurrentIndex(index); if (previousIndex < m_rootSelector->count()) m_rootSelector->removeItem(previousIndex); if (m_autoSync) // we might find a better root for current selection now handleCurrentEditorChanged(Core::EditorManager::currentEditor()); } void FolderNavigationWidget::removeRootDirectory(const QString &id) { for (int i = 0; i < m_rootSelector->count(); ++i) { if (m_rootSelector->itemData(i, ID_ROLE).toString() == id) { m_rootSelector->removeItem(i); break; } } if (m_autoSync) // we might need to find a new root for current selection handleCurrentEditorChanged(Core::EditorManager::currentEditor()); } void FolderNavigationWidget::addNewItem() { const QModelIndex current = m_sortProxyModel->mapToSource(m_listView->currentIndex()); if (!current.isValid()) return; const auto filePath = Utils::FilePath::fromString(m_fileSystemModel->filePath(current)); const Utils::FilePath path = filePath.isDir() ? filePath : filePath.parentDir(); Core::ICore::showNewItemDialog(ProjectExplorerPlugin::tr("New File", "Title of dialog"), Utils::filtered(Core::IWizardFactory::allWizardFactories(), Utils::equal(&Core::IWizardFactory::kind, Core::IWizardFactory::FileWizard)), path.toString()); } void FolderNavigationWidget::editCurrentItem() { const QModelIndex current = m_listView->currentIndex(); if (m_listView->model()->flags(current) & Qt::ItemIsEditable) m_listView->edit(current); } static QVector removableFolderNodes(const Utils::FilePath &filePath) { QVector folderNodes; ProjectTree::forEachNode([&](Node *node) { if (node->asFileNode() && node->filePath() == filePath && node->parentFolderNode() && node->parentFolderNode()->supportsAction(RemoveFile, node)) { folderNodes.append(node->parentFolderNode()); } }); return folderNodes; } void FolderNavigationWidget::removeCurrentItem() { const QModelIndex current = m_sortProxyModel->mapToSource(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 folderNodes = removableFolderNodes( Utils::FilePath::fromString(filePath)); const QVector failedNodes = Utils::filtered(folderNodes, [filePath](FolderNode *folder) { return folder->removeFiles({filePath}) != RemovedFilesFromProject::Ok; }); 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); }); } } } bool FolderNavigationWidget::autoSynchronization() const { return m_autoSync; } void FolderNavigationWidget::setAutoSynchronization(bool sync) { m_toggleSync->setChecked(sync); m_toggleRootSync->setEnabled(sync); m_toggleRootSync->setChecked(sync ? m_rootAutoSync : false); if (sync == m_autoSync) return; m_autoSync = sync; if (m_autoSync) handleCurrentEditorChanged(Core::EditorManager::currentEditor()); } void FolderNavigationWidget::setRootAutoSynchronization(bool sync) { m_toggleRootSync->setChecked(sync); if (sync == m_rootAutoSync) return; m_rootAutoSync = sync; if (m_rootAutoSync) handleCurrentEditorChanged(Core::EditorManager::currentEditor()); } void FolderNavigationWidget::handleCurrentEditorChanged(Core::IEditor *editor) { if (!m_autoSync || !editor || editor->document()->filePath().isEmpty() || editor->document()->isTemporary()) return; const Utils::FilePath filePath = editor->document()->filePath(); if (m_rootAutoSync) selectBestRootForFile(filePath); selectFile(filePath); } void FolderNavigationWidget::selectBestRootForFile(const Utils::FilePath &filePath) { const int bestRootIndex = bestRootForFile(filePath); m_rootSelector->setCurrentIndex(bestRootIndex); } void FolderNavigationWidget::selectFile(const Utils::FilePath &filePath) { const QModelIndex fileIndex = m_sortProxyModel->mapFromSource( m_fileSystemModel->index(filePath.toString())); if (fileIndex.isValid() || filePath.isEmpty() /* Computer root */) { // 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_sortProxyModel->mapFromSource( m_fileSystemModel->index(filePath.toString())); if (fileIndex == m_listView->rootIndex()) { m_listView->horizontalScrollBar()->setValue(0); m_listView->verticalScrollBar()->setValue(0); } else { m_listView->scrollTo(fileIndex); } setCrumblePath(filePath); }); } } void FolderNavigationWidget::setRootDirectory(const Utils::FilePath &directory) { const QModelIndex index = m_sortProxyModel->mapFromSource( m_fileSystemModel->setRootPath(directory.toString())); m_listView->setRootIndex(index); } int FolderNavigationWidget::bestRootForFile(const Utils::FilePath &filePath) { 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(); if (filePath.isChildOf(root) && root.toString().size() > commonLength) { index = i; commonLength = root.toString().size(); } } return index; } void FolderNavigationWidget::openItem(const QModelIndex &index) { 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)) return; const QString path = m_fileSystemModel->filePath(index); Core::EditorManager::openEditor(path); } QStringList FolderNavigationWidget::projectsInDirectory(const QModelIndex &index) const { QTC_ASSERT(index.isValid() && m_fileSystemModel->isDir(index), return {}); const QFileInfo fi = m_fileSystemModel->fileInfo(index); if (!fi.isReadable() || !fi.isExecutable()) return {}; const QString path = m_fileSystemModel->filePath(index); // Try to find project files in directory and open those. return FolderNavigationWidget::projectFilesInDirectory(path); } void FolderNavigationWidget::openProjectsInDirectory(const QModelIndex &index) { const QStringList projectFiles = projectsInDirectory(index); if (!projectFiles.isEmpty()) Core::ICore::instance()->openFiles(projectFiles); } void FolderNavigationWidget::createNewFolder(const QModelIndex &parent) { static const QString baseName = tr("New Folder"); // find non-existing name const QDir dir(m_fileSystemModel->filePath(parent)); const QSet existingItems = Utils::transform(dir.entryList({baseName + '*'}, QDir::AllEntries), [](const QString &entry) { return Utils::FilePath::fromString(entry); }); const Utils::FilePath name = Utils::makeUniquelyNumbered(Utils::FilePath::fromString(baseName), existingItems); // create directory and edit const QModelIndex index = m_sortProxyModel->mapFromSource( m_fileSystemModel->mkdir(parent, name.toString())); if (!index.isValid()) return; m_listView->setCurrentIndex(index); m_listView->edit(index); } void FolderNavigationWidget::setCrumblePath(const Utils::FilePath &filePath) { const QModelIndex index = m_fileSystemModel->index(filePath.toString()); const int width = m_crumbLabel->width(); const int previousHeight = m_crumbLabel->immediateHeightForWidth(width); m_crumbLabel->setPath(filePath); 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; const QRect currentItemRect = m_listView->visualRect(index); const int currentItemVStart = currentItemRect.y(); const int currentItemVEnd = currentItemVStart + currentItemRect.height(); const bool currentItemStillVisibleAsBefore = (diff < 0 || currentItemVStart > diff || currentItemVEnd <= 0); if (bar && bar->minimum() <= newBarValue && bar->maximum() >= newBarValue && currentItemStillVisibleAsBefore) { // 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(); } } } void FolderNavigationWidget::contextMenuEvent(QContextMenuEvent *ev) { QMenu menu; // Open current item const QModelIndex current = m_sortProxyModel->mapToSource(m_listView->currentIndex()); const bool hasCurrentItem = current.isValid(); QAction *actionOpenFile = nullptr; QAction *actionOpenProjects = nullptr; QAction *actionOpenAsProject = nullptr; QAction *newFolder = nullptr; const bool isDir = m_fileSystemModel->isDir(current); const Utils::FilePath filePath = hasCurrentItem ? Utils::FilePath::fromString( m_fileSystemModel->filePath(current)) : Utils::FilePath(); if (hasCurrentItem) { const QString fileName = m_fileSystemModel->fileName(current); if (isDir) { actionOpenProjects = menu.addAction(tr("Open Project in \"%1\"").arg(fileName)); if (projectsInDirectory(current).isEmpty()) actionOpenProjects->setEnabled(false); } else { actionOpenFile = menu.addAction(tr("Open \"%1\"").arg(fileName)); if (ProjectExplorerPlugin::isProjectFile(Utils::FilePath::fromString(fileName))) actionOpenAsProject = menu.addAction(tr("Open Project \"%1\"").arg(fileName)); } } // we need dummy DocumentModel::Entry with absolute file path in it // to get EditorManager::addNativeDirAndOpenWithActions() working Core::DocumentModel::Entry fakeEntry; Core::IDocument document; document.setFilePath(filePath); fakeEntry.document = &document; Core::EditorManager::addNativeDirAndOpenWithActions(&menu, &fakeEntry); if (hasCurrentItem) { menu.addAction(Core::ActionManager::command(Constants::ADDNEWFILE)->action()); if (!isDir) menu.addAction(Core::ActionManager::command(Constants::REMOVEFILE)->action()); if (m_fileSystemModel->flags(current) & Qt::ItemIsEditable) menu.addAction(Core::ActionManager::command(Constants::RENAMEFILE)->action()); newFolder = menu.addAction(tr("New Folder")); if (!isDir && Core::DiffService::instance()) { menu.addAction( TextEditor::TextDocument::createDiffAgainstCurrentFileAction(&menu, [filePath]() { return filePath; })); } } menu.addSeparator(); QAction * const collapseAllAction = menu.addAction(ProjectExplorerPlugin::tr("Collapse All")); QAction *action = menu.exec(ev->globalPos()); if (!action) return; ev->accept(); if (action == actionOpenFile) { openItem(current); } else if (action == actionOpenAsProject) { ProjectExplorerPlugin::openProject(filePath.toString()); } else if (action == actionOpenProjects) openProjectsInDirectory(current); else if (action == newFolder) { if (isDir) createNewFolder(current); else createNewFolder(current.parent()); } else if (action == collapseAllAction) { m_listView->collapseAll(); } } bool FolderNavigationWidget::rootAutoSynchronization() const { return m_rootAutoSync; } 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(); } bool FolderNavigationWidget::isShowingBreadCrumbs() const { return m_showBreadCrumbsAction->isChecked(); } bool FolderNavigationWidget::isShowingFoldersOnTop() const { return m_showFoldersOnTopAction->isChecked(); } 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; } // --------------------FolderNavigationWidgetFactory FolderNavigationWidgetFactory::FolderNavigationWidgetFactory() { m_instance = this; setDisplayName(tr("File System")); setPriority(400); setId("File System"); setActivationSequence(QKeySequence(Core::useMacShortcuts ? tr("Meta+Y") : tr("Alt+Y"))); insertRootDirectory({QLatin1String("A.Computer"), 0 /*sortValue*/, FolderNavigationWidget::tr("Computer"), Utils::FilePath(), Icons::DESKTOP_DEVICE_SMALL.icon()}); insertRootDirectory({QLatin1String("A.Home"), 10 /*sortValue*/, FolderNavigationWidget::tr("Home"), Utils::FilePath::fromString(QDir::homePath()), Utils::Icons::HOME.icon()}); updateProjectsDirectoryRoot(); connect(Core::DocumentManager::instance(), &Core::DocumentManager::projectsDirectoryChanged, this, &FolderNavigationWidgetFactory::updateProjectsDirectoryRoot); registerActions(); } Core::NavigationView FolderNavigationWidgetFactory::createWidget() { auto fnw = new FolderNavigationWidget; for (const RootDirectory &root : m_rootDirectories) fnw->insertRootDirectory(root); connect(this, &FolderNavigationWidgetFactory::rootDirectoryAdded, fnw, &FolderNavigationWidget::insertRootDirectory); connect(this, &FolderNavigationWidgetFactory::rootDirectoryRemoved, fnw, &FolderNavigationWidget::removeRootDirectory); Core::NavigationView n; n.widget = fnw; auto filter = new QToolButton; filter->setIcon(Utils::Icons::FILTER.icon()); filter->setToolTip(tr("Options")); filter->setPopupMode(QToolButton::InstantPopup); filter->setProperty("noArrow", true); auto filterMenu = new QMenu(filter); filterMenu->addAction(fnw->m_filterHiddenFilesAction); filterMenu->addAction(fnw->m_showBreadCrumbsAction); filterMenu->addAction(fnw->m_showFoldersOnTopAction); filter->setMenu(filterMenu); n.dockToolBarWidgets << filter << fnw->m_toggleSync; return n; } void FolderNavigationWidgetFactory::saveSettings(QSettings *settings, int position, QWidget *widget) { auto fnw = qobject_cast(widget); QTC_ASSERT(fnw, return); const QString base = kSettingsBase + QString::number(position); settings->setValue(base + kHiddenFilesKey, fnw->hiddenFilesFilter()); settings->setValue(base + kSyncKey, fnw->autoSynchronization()); settings->setValue(base + kShowBreadCrumbs, fnw->isShowingBreadCrumbs()); settings->setValue(base + kSyncRootWithEditor, fnw->rootAutoSynchronization()); } void FolderNavigationWidgetFactory::restoreSettings(QSettings *settings, int position, QWidget *widget) { auto fnw = qobject_cast(widget); QTC_ASSERT(fnw, return); const QString base = kSettingsBase + QString::number(position); fnw->setHiddenFilesFilter(settings->value(base + kHiddenFilesKey, false).toBool()); fnw->setAutoSynchronization(settings->value(base + kSyncKey, true).toBool()); fnw->setShowBreadCrumbs(settings->value(base + kShowBreadCrumbs, true).toBool()); fnw->setRootAutoSynchronization(settings->value(base + kSyncRootWithEditor, true).toBool()); } void FolderNavigationWidgetFactory::insertRootDirectory(const RootDirectory &directory) { const int index = rootIndex(directory.id); if (index < 0) m_rootDirectories.append(directory); else m_rootDirectories[index] = directory; emit m_instance->rootDirectoryAdded(directory); } void FolderNavigationWidgetFactory::removeRootDirectory(const QString &id) { const int index = rootIndex(id); QTC_ASSERT(index >= 0, return ); m_rootDirectories.removeAt(index); emit m_instance->rootDirectoryRemoved(id); } 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), 20 /*sortValue*/, FolderNavigationWidget::tr("Projects"), Core::DocumentManager::projectsDirectory(), Utils::Icons::PROJECT.icon()}); } static FolderNavigationWidget *currentFolderNavigationWidget() { return qobject_cast(Core::ICore::currentContextWidget()); } void FolderNavigationWidgetFactory::registerActions() { Core::Context context(C_FOLDERNAVIGATIONWIDGET); auto add = new QAction(tr("Add New..."), this); Core::ActionManager::registerAction(add, Constants::ADDNEWFILE, context); connect(add, &QAction::triggered, Core::ICore::instance(), [] { if (auto navWidget = currentFolderNavigationWidget()) navWidget->addNewItem(); }); auto rename = new QAction(tr("Rename..."), this); Core::ActionManager::registerAction(rename, Constants::RENAMEFILE, context); connect(rename, &QAction::triggered, Core::ICore::instance(), [] { if (auto navWidget = currentFolderNavigationWidget()) navWidget->editCurrentItem(); }); auto remove = new QAction(tr("Remove..."), this); Core::ActionManager::registerAction(remove, Constants::REMOVEFILE, context); connect(remove, &QAction::triggered, Core::ICore::instance(), [] { if (auto navWidget = currentFolderNavigationWidget()) navWidget->removeCurrentItem(); }); } int DelayedFileCrumbLabel::immediateHeightForWidth(int w) const { return Utils::FileCrumbLabel::heightForWidth(w); } int DelayedFileCrumbLabel::heightForWidth(int w) const { static const int doubleDefaultInterval = 800; static QHash 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(this); QTimer::singleShot(std::max(2 * QApplication::doubleClickInterval(), doubleDefaultInterval), 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(this); that->m_bar->setValue(m_barValue); that->m_bar.clear(); } } // namespace Internal } // namespace ProjectExplorer