diff --git a/project/abstractfilemanagerplugin.cpp b/project/abstractfilemanagerplugin.cpp index 0f0372f6c..6a0cd889a 100644 --- a/project/abstractfilemanagerplugin.cpp +++ b/project/abstractfilemanagerplugin.cpp @@ -1,662 +1,662 @@ /*************************************************************************** * This file is part of KDevelop * * Copyright 2010-2012 Milian Wolff * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU Library General Public License as * * published by the Free Software Foundation; either version 2 of the * * License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU Library General Public * * License along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #include "abstractfilemanagerplugin.h" #include "filemanagerlistjob.h" #include "projectmodel.h" #include "helper.h" #include #include #include #include #include #include #include #include #include #include #include "projectfiltermanager.h" #include "debug.h" #define ifDebug(x) using namespace KDevelop; //BEGIN Helper namespace { /** * Returns the parent folder item for a given item or the project root item if there is no parent. */ ProjectFolderItem* getParentFolder(ProjectBaseItem* item) { if ( item->parent() ) { return static_cast(item->parent()); } else { return item->project()->projectItem(); } } } //END Helper //BEGIN Private struct AbstractFileManagerPlugin::Private { explicit Private(AbstractFileManagerPlugin* qq) : q(qq) { } AbstractFileManagerPlugin* q; /** * The just returned must be started in one way or another for this method * to have any affect. The job will then auto-delete itself upon completion. */ - KIO::Job* eventuallyReadFolder( ProjectFolderItem* item ) Q_REQUIRED_RESULT; + Q_REQUIRED_RESULT KIO::Job* eventuallyReadFolder(ProjectFolderItem* item); void addJobItems(FileManagerListJob* job, ProjectFolderItem* baseItem, const KIO::UDSEntryList& entries); void deleted(const QString &path); void created(const QString &path); void projectClosing(IProject* project); void jobFinished(KJob* job); /// Stops watching the given folder for changes, only useful for local files. void stopWatcher(ProjectFolderItem* folder); /// Continues watching the given folder for changes. void continueWatcher(ProjectFolderItem* folder); /// Common renaming function. bool rename(ProjectBaseItem* item, const Path& newPath); void removeFolder(ProjectFolderItem* folder); QHash m_watchers; QHash > m_projectJobs; QVector m_stoppedFolders; ProjectFilterManager m_filters; }; void AbstractFileManagerPlugin::Private::projectClosing(IProject* project) { if ( m_projectJobs.contains(project) ) { // make sure the import job does not live longer than the project // see also addLotsOfFiles test foreach( FileManagerListJob* job, m_projectJobs[project] ) { qCDebug(FILEMANAGER) << "killing project job:" << job; job->abort(); } m_projectJobs.remove(project); } delete m_watchers.take(project); m_filters.remove(project); } KIO::Job* AbstractFileManagerPlugin::Private::eventuallyReadFolder( ProjectFolderItem* item ) { FileManagerListJob* listJob = new FileManagerListJob( item ); m_projectJobs[ item->project() ] << listJob; qCDebug(FILEMANAGER) << "adding job" << listJob << item << item->path() << "for project" << item->project(); q->connect( listJob, &FileManagerListJob::finished, q, [&] (KJob* job) { jobFinished(job); } ); q->connect( listJob, &FileManagerListJob::entries, q, [&] (FileManagerListJob* job, ProjectFolderItem* baseItem, const KIO::UDSEntryList& entries) { addJobItems(job, baseItem, entries); } ); return listJob; } void AbstractFileManagerPlugin::Private::jobFinished(KJob* job) { FileManagerListJob* gmlJob = qobject_cast(job); if (gmlJob) { ifDebug(qCDebug(FILEMANAGER) << job << gmlJob << gmlJob->item();) m_projectJobs[ gmlJob->item()->project() ].removeOne( gmlJob ); } else { // job emitted its finished signal from its destructor // ensure we don't keep a dangling point in our list foreach (auto jobs, m_projectJobs) { if (jobs.removeOne(reinterpret_cast(job))) { break; } } } } void AbstractFileManagerPlugin::Private::addJobItems(FileManagerListJob* job, ProjectFolderItem* baseItem, const KIO::UDSEntryList& entries) { if ( entries.empty() ) { return; } qCDebug(FILEMANAGER) << "reading entries of" << baseItem->path(); // build lists of valid files and folders with paths relative to the project folder Path::List files; Path::List folders; foreach ( const KIO::UDSEntry& entry, entries ) { QString name = entry.stringValue( KIO::UDSEntry::UDS_NAME ); if (name == QLatin1String(".") || name == QLatin1String("..")) { continue; } Path path(baseItem->path(), name); if ( !q->isValid( path, entry.isDir(), baseItem->project() ) ) { continue; } else { if ( entry.isDir() ) { if( entry.isLink() ) { const Path linkedPath = baseItem->path().cd(entry.stringValue( KIO::UDSEntry::UDS_LINK_DEST )); // make sure we don't end in an infinite loop if( linkedPath.isParentOf( baseItem->project()->path() ) || baseItem->project()->path().isParentOf( linkedPath ) || linkedPath == baseItem->project()->path() ) { continue; } } folders << path; } else { files << path; } } } ifDebug(qCDebug(FILEMANAGER) << "valid folders:" << folders;) ifDebug(qCDebug(FILEMANAGER) << "valid files:" << files;) // remove obsolete rows for ( int j = 0; j < baseItem->rowCount(); ++j ) { if ( ProjectFolderItem* f = baseItem->child(j)->folder() ) { // check if this is still a valid folder int index = folders.indexOf( f->path() ); if ( index == -1 ) { // folder got removed or is now invalid removeFolder(f); --j; } else { // this folder already exists in the view folders.remove( index ); // no need to add this item, but we still want to recurse into it job->addSubDir( f ); emit q->reloadedFolderItem( f ); } } else if ( ProjectFileItem* f = baseItem->child(j)->file() ) { // check if this is still a valid file int index = files.indexOf( f->path() ); if ( index == -1 ) { // file got removed or is now invalid ifDebug(qCDebug(FILEMANAGER) << "removing file:" << f << f->path();) baseItem->removeRow( j ); --j; } else { // this file already exists in the view files.remove( index ); emit q->reloadedFileItem( f ); } } } // add new rows foreach ( const Path& path, files ) { ProjectFileItem* file = q->createFileItem( baseItem->project(), path, baseItem ); if (file) { emit q->fileAdded( file ); } } foreach ( const Path& path, folders ) { ProjectFolderItem* folder = q->createFolderItem( baseItem->project(), path, baseItem ); if (folder) { emit q->folderAdded( folder ); job->addSubDir( folder ); } } } void AbstractFileManagerPlugin::Private::created(const QString &path_) { qCDebug(FILEMANAGER) << "created:" << path_; QFileInfo info(path_); ///FIXME: share memory with parent const Path path(path_); const IndexedString indexedPath(path.pathOrUrl()); const IndexedString indexedParent(path.parent().pathOrUrl()); QHashIterator it(m_watchers); while (it.hasNext()) { const auto p = it.next().key(); if ( !p->projectItem()->model() ) { // not yet finished with loading // FIXME: how should this be handled? see unit test continue; } if ( !q->isValid(path, info.isDir(), p) ) { continue; } if ( info.isDir() ) { bool found = false; foreach ( ProjectFolderItem* folder, p->foldersForPath(indexedPath) ) { // exists already in this project, happens e.g. when we restart the dirwatcher // or if we delete and remove folders consecutively https://bugs.kde.org/show_bug.cgi?id=260741 qCDebug(FILEMANAGER) << "force reload of" << path << folder; auto job = eventuallyReadFolder( folder ); job->start(); found = true; } if ( found ) { continue; } } else if (!p->filesForPath(indexedPath).isEmpty()) { // also gets triggered for kate's backup files continue; } foreach ( ProjectFolderItem* parentItem, p->foldersForPath(indexedParent) ) { if ( info.isDir() ) { ProjectFolderItem* folder = q->createFolderItem( p, path, parentItem ); if (folder) { emit q->folderAdded( folder ); auto job = eventuallyReadFolder( folder ); job->start(); } } else { ProjectFileItem* file = q->createFileItem( p, path, parentItem ); if (file) { emit q->fileAdded( file ); } } } } } void AbstractFileManagerPlugin::Private::deleted(const QString &path_) { if ( QFile::exists(path_) ) { // stopDirScan... return; } // ensure that the path is not inside a stopped folder foreach(const QString& folder, m_stoppedFolders) { if (path_.startsWith(folder)) { return; } } qCDebug(FILEMANAGER) << "deleted:" << path_; const Path path(QUrl::fromLocalFile(path_)); const IndexedString indexed(path.pathOrUrl()); QHashIterator it(m_watchers); while (it.hasNext()) { const auto p = it.next().key(); if (path == p->path()) { KMessageBox::error(qApp->activeWindow(), i18n("The base folder of project %1" " got deleted or moved outside of KDevelop.\n" "The project has to be closed.", p->name()), i18n("Project Folder Deleted") ); ICore::self()->projectController()->closeProject(p); continue; } if ( !p->projectItem()->model() ) { // not yet finished with loading // FIXME: how should this be handled? see unit test continue; } foreach ( ProjectFolderItem* item, p->foldersForPath(indexed) ) { removeFolder(item); } foreach ( ProjectFileItem* item, p->filesForPath(indexed) ) { emit q->fileRemoved(item); ifDebug(qCDebug(FILEMANAGER) << "removing file" << item;) item->parent()->removeRow(item->row()); } } } bool AbstractFileManagerPlugin::Private::rename(ProjectBaseItem* item, const Path& newPath) { if ( !q->isValid(newPath, true, item->project()) ) { int cancel = KMessageBox::warningContinueCancel( qApp->activeWindow(), i18n("You tried to rename '%1' to '%2', but the latter is filtered and will be hidden.\n" "Do you want to continue?", item->text(), newPath.lastPathSegment()), QString(), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), QStringLiteral("GenericManagerRenameToFiltered") ); if ( cancel == KMessageBox::Cancel ) { return false; } } foreach ( ProjectFolderItem* parent, item->project()->foldersForPath(IndexedString(newPath.parent().pathOrUrl())) ) { if ( parent->folder() ) { stopWatcher(parent); const Path source = item->path(); bool success = renameUrl( item->project(), source.toUrl(), newPath.toUrl() ); if ( success ) { item->setPath( newPath ); item->parent()->takeRow( item->row() ); parent->appendRow( item ); if (item->file()) { emit q->fileRenamed(source, item->file()); } else { Q_ASSERT(item->folder()); emit q->folderRenamed(source, item->folder()); } } continueWatcher(parent); return success; } } return false; } void AbstractFileManagerPlugin::Private::stopWatcher(ProjectFolderItem* folder) { if ( !folder->path().isLocalFile() ) { return; } Q_ASSERT(m_watchers.contains(folder->project())); const QString path = folder->path().toLocalFile(); m_watchers[folder->project()]->stopDirScan(path); m_stoppedFolders.append(path); } void AbstractFileManagerPlugin::Private::continueWatcher(ProjectFolderItem* folder) { if ( !folder->path().isLocalFile() ) { return; } Q_ASSERT(m_watchers.contains(folder->project())); const QString path = folder->path().toLocalFile(); m_watchers[folder->project()]->restartDirScan(path); const int idx = m_stoppedFolders.indexOf(path); if (idx != -1) { m_stoppedFolders.remove(idx); } } bool isChildItem(ProjectBaseItem* parent, ProjectBaseItem* child) { do { if (child == parent) { return true; } child = child->parent(); } while(child); return false; } void AbstractFileManagerPlugin::Private::removeFolder(ProjectFolderItem* folder) { ifDebug(qCDebug(FILEMANAGER) << "removing folder:" << folder << folder->path();) foreach(FileManagerListJob* job, m_projectJobs[folder->project()]) { if (isChildItem(folder, job->item())) { qCDebug(FILEMANAGER) << "killing list job for removed folder" << job << folder->path(); job->abort(); Q_ASSERT(!m_projectJobs.value(folder->project()).contains(job)); } else { job->removeSubDir(folder); } } folder->parent()->removeRow( folder->row() ); } //END Private //BEGIN Plugin AbstractFileManagerPlugin::AbstractFileManagerPlugin( const QString& componentName, QObject *parent, const QVariantList & /*args*/ ) : IProjectFileManager(), IPlugin( componentName, parent ), d(new Private(this)) { connect(core()->projectController(), &IProjectController::projectClosing, this, [&] (IProject* project) { d->projectClosing(project); }); } AbstractFileManagerPlugin::~AbstractFileManagerPlugin() { delete d; } IProjectFileManager::Features AbstractFileManagerPlugin::features() const { return Features( Folders | Files ); } QList AbstractFileManagerPlugin::parse( ProjectFolderItem *item ) { // we are async, can't return anything here qCDebug(FILEMANAGER) << "note: parse will always return an empty list"; Q_UNUSED(item); return QList(); } ProjectFolderItem *AbstractFileManagerPlugin::import( IProject *project ) { ProjectFolderItem *projectRoot = createFolderItem( project, project->path(), nullptr ); emit folderAdded( projectRoot ); qCDebug(FILEMANAGER) << "imported new project" << project->name() << "at" << projectRoot->path(); ///TODO: check if this works for remote files when something gets changed through another KDE app if ( project->path().isLocalFile() ) { d->m_watchers[project] = new KDirWatch( project ); connect(d->m_watchers[project], &KDirWatch::created, this, [&] (const QString& path_) { d->created(path_); }); connect(d->m_watchers[project], &KDirWatch::deleted, this, [&] (const QString& path_) { d->deleted(path_); }); d->m_watchers[project]->addDir(project->path().toLocalFile(), KDirWatch::WatchSubDirs | KDirWatch:: WatchFiles ); } d->m_filters.add(project); return projectRoot; } KJob* AbstractFileManagerPlugin::createImportJob(ProjectFolderItem* item) { return d->eventuallyReadFolder(item); } bool AbstractFileManagerPlugin::reload( ProjectFolderItem* item ) { qCDebug(FILEMANAGER) << "reloading item" << item->path(); auto job = d->eventuallyReadFolder( item->folder() ); job->start(); return true; } ProjectFolderItem* AbstractFileManagerPlugin::addFolder( const Path& folder, ProjectFolderItem * parent ) { qCDebug(FILEMANAGER) << "adding folder" << folder << "to" << parent->path(); ProjectFolderItem* created = nullptr; d->stopWatcher(parent); if ( createFolder(folder.toUrl()) ) { created = createFolderItem( parent->project(), folder, parent ); if (created) { emit folderAdded(created); } } d->continueWatcher(parent); return created; } ProjectFileItem* AbstractFileManagerPlugin::addFile( const Path& file, ProjectFolderItem * parent ) { qCDebug(FILEMANAGER) << "adding file" << file << "to" << parent->path(); ProjectFileItem* created = nullptr; d->stopWatcher(parent); if ( createFile(file.toUrl()) ) { created = createFileItem( parent->project(), file, parent ); if (created) { emit fileAdded(created); } } d->continueWatcher(parent); return created; } bool AbstractFileManagerPlugin::renameFolder(ProjectFolderItem* folder, const Path& newPath) { qCDebug(FILEMANAGER) << "trying to rename a folder:" << folder->path() << newPath; return d->rename(folder, newPath); } bool AbstractFileManagerPlugin::renameFile(ProjectFileItem* file, const Path& newPath) { qCDebug(FILEMANAGER) << "trying to rename a file:" << file->path() << newPath; return d->rename(file, newPath); } bool AbstractFileManagerPlugin::removeFilesAndFolders(const QList &items) { bool success = true; foreach(ProjectBaseItem* item, items) { Q_ASSERT(item->folder() || item->file()); ProjectFolderItem* parent = getParentFolder(item); d->stopWatcher(parent); success &= removeUrl(parent->project(), item->path().toUrl(), true); if ( success ) { if (item->file()) { emit fileRemoved(item->file()); } else { Q_ASSERT(item->folder()); emit folderRemoved(item->folder()); } item->parent()->removeRow( item->row() ); } d->continueWatcher(parent); if ( !success ) break; } return success; } bool AbstractFileManagerPlugin::moveFilesAndFolders(const QList< ProjectBaseItem* >& items, ProjectFolderItem* newParent) { bool success = true; foreach(ProjectBaseItem* item, items) { Q_ASSERT(item->folder() || item->file()); ProjectFolderItem* oldParent = getParentFolder(item); d->stopWatcher(oldParent); d->stopWatcher(newParent); const Path oldPath = item->path(); const Path newPath(newParent->path(), item->baseName()); success &= renameUrl(oldParent->project(), oldPath.toUrl(), newPath. toUrl()); if ( success ) { if (item->file()) { emit fileRemoved(item->file()); } else { emit folderRemoved(item->folder()); } oldParent->removeRow( item->row() ); KIO::Job *readJob = d->eventuallyReadFolder(newParent); // reload first level synchronously, deeper levels will run async // this is required for code that expects the new item to exist after // this method finished readJob->exec(); } d->continueWatcher(oldParent); d->continueWatcher(newParent); if ( !success ) break; } return success; } bool AbstractFileManagerPlugin::copyFilesAndFolders(const Path::List& items, ProjectFolderItem* newParent) { bool success = true; foreach(const Path& item, items) { d->stopWatcher(newParent); success &= copyUrl(newParent->project(), item.toUrl(), newParent->path().toUrl()); if ( success ) { KIO::Job *readJob = d->eventuallyReadFolder(newParent); // reload first level synchronously, deeper levels will run async // this is required for code that expects the new item to exist after // this method finished readJob->exec(); } d->continueWatcher(newParent); if ( !success ) break; } return success; } bool AbstractFileManagerPlugin::isValid( const Path& path, const bool isFolder, IProject* project ) const { return d->m_filters.isValid( path, isFolder, project ); } ProjectFileItem* AbstractFileManagerPlugin::createFileItem( IProject* project, const Path& path, ProjectBaseItem* parent ) { return new ProjectFileItem( project, path, parent ); } ProjectFolderItem* AbstractFileManagerPlugin::createFolderItem( IProject* project, const Path& path, ProjectBaseItem* parent ) { return new ProjectFolderItem( project, path, parent ); } KDirWatch* AbstractFileManagerPlugin::projectWatcher( IProject* project ) const { return d->m_watchers.value( project, nullptr ); } //END Plugin #include "moc_abstractfilemanagerplugin.cpp" diff --git a/sublime/container.cpp b/sublime/container.cpp index 7d6494509..f736db01b 100644 --- a/sublime/container.cpp +++ b/sublime/container.cpp @@ -1,689 +1,726 @@ /*************************************************************************** * Copyright 2006-2009 Alexander Dymo * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU Library General Public License as * * published by the Free Software Foundation; either version 2 of the * * License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU Library General Public * * License along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #include "container.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "view.h" #include "urldocument.h" #include namespace Sublime { // struct ContainerPrivate class ContainerTabBar : public QTabBar { Q_OBJECT public: explicit ContainerTabBar(Container* container) : QTabBar(container), m_container(container) { if (QApplication::style()->objectName() == QLatin1String("macintosh")) { static QPointer qTabBarStyle = QStyleFactory::create(QStringLiteral("fusion")); if (qTabBarStyle) { setStyle(qTabBarStyle); } } // configure the QTabBar style so it behaves as appropriately as possible, // even if we end up using the native Macintosh style because the user's // Qt doesn't have the Fusion style installed. setDocumentMode(true); setUsesScrollButtons(true); setElideMode(Qt::ElideNone); installEventFilter(this); } bool event(QEvent* ev) override { if(ev->type() == QEvent::ToolTip) { ev->accept(); QHelpEvent* helpEvent = static_cast(ev); int tab = tabAt(helpEvent->pos()); if(tab != -1) { m_container->showTooltipForTab(tab); } return true; } return QTabBar::event(ev); } void mousePressEvent(QMouseEvent* event) override { if (event->button() == Qt::MidButton) { // just close on midbutton, drag can still be done with left mouse button int tab = tabAt(event->pos()); if (tab != -1) { emit tabCloseRequested(tab); } return; } QTabBar::mousePressEvent(event); } bool eventFilter(QObject* obj, QEvent* event) override { if (obj != this) { return QObject::eventFilter(obj, event); } // TODO Qt6: Move to mouseDoubleClickEvent when fixme in qttabbar.cpp is resolved // see "fixme Qt 6: move to mouseDoubleClickEvent(), here for BC reasons." in qtabbar.cpp if (event->type() == QEvent::MouseButtonDblClick) { // block tabBarDoubleClicked signals with RMB, see https://bugs.kde.org/show_bug.cgi?id=356016 auto mouseEvent = static_cast(event); if (mouseEvent->button() == Qt::MidButton) { return true; } } return QObject::eventFilter(obj, event); } Q_SIGNALS: void newTabRequested(); private: Container* m_container; }; bool sortViews(const View* const lhs, const View* const rhs) { return lhs->document()->title().compare(rhs->document()->title(), Qt::CaseInsensitive) < 0; } +#ifdef Q_OS_MACOS +// only one of these per process: +static QMenu* currentDockMenu = nullptr; +#endif + struct ContainerPrivate { QBoxLayout* layout; QHash viewForWidget; ContainerTabBar *tabBar; QStackedWidget *stack; KSqueezedTextLabel *fileNameCorner; QLabel *fileStatus; KSqueezedTextLabel *statusCorner; QPointer leftCornerWidget; QToolButton* documentListButton; QMenu* documentListMenu; QHash documentListActionForView; /** * Updates the context menu which is shown when * the document list button in the tab bar is clicked. * * It shall build a popup menu which contains all currently * enabled views using the title their document provides. */ void updateDocumentListPopupMenu() { qDeleteAll(documentListActionForView); documentListActionForView.clear(); documentListMenu->clear(); // create a lexicographically sorted list QVector views; views.reserve(viewForWidget.size()); foreach(View* view, viewForWidget){ views << view; } std::sort(views.begin(), views.end(), sortViews); for (int i = 0; i < views.size(); ++i) { View *view = views.at(i); QString visibleEntryTitle; // if filename is not unique, prepend containing directory if ((i < views.size() - 1 && view->document()->title() == views.at(i + 1)->document()->title()) || (i > 0 && view->document()->title() == views.at(i - 1)->document()->title()) ) { auto urlDoc = qobject_cast(view->document()); if (!urlDoc) { visibleEntryTitle = view->document()->title(); } else { auto url = urlDoc->url().toString(); int secondOffset = url.lastIndexOf('/'); secondOffset = url.lastIndexOf('/', secondOffset - 1); visibleEntryTitle = url.right(url.length() - url.lastIndexOf('/', secondOffset) - 1); } } else { visibleEntryTitle = view->document()->title(); } QAction* action = documentListMenu->addAction(visibleEntryTitle); action->setData(QVariant::fromValue(view)); documentListActionForView[view] = action; action->setIcon(view->document()->icon()); ///FIXME: push this code somehow into shell, such that we can access the project model for /// icons and also get a neat, short path like the document switcher. } + + setAsDockMenu(); + } + + void setAsDockMenu() + { +#ifdef Q_OS_MACOS + if (documentListMenu != currentDockMenu) { + documentListMenu->setAsDockMenu(); + currentDockMenu = documentListMenu; + } +#endif + } + + ~ContainerPrivate() + { +#ifdef Q_OS_MACOS + if (documentListMenu == currentDockMenu) { + QMenu().setAsDockMenu(); + currentDockMenu = nullptr; + } +#endif } }; class UnderlinedLabel: public KSqueezedTextLabel { Q_OBJECT public: explicit UnderlinedLabel(QTabBar *tabBar, QWidget* parent = nullptr) :KSqueezedTextLabel(parent), m_tabBar(tabBar) { } protected: void paintEvent(QPaintEvent *ev) override { #ifndef Q_OS_OSX // getting the underlining right on OS X is tricky; omitting // the underlining attracts the eye less than not getting it // exactly right. if (m_tabBar->isVisible() && m_tabBar->count() > 0) { QStylePainter p(this); QStyleOptionTabBarBase optTabBase; optTabBase.init(m_tabBar); optTabBase.shape = m_tabBar->shape(); optTabBase.tabBarRect = m_tabBar->rect(); optTabBase.tabBarRect.moveRight(0); QStyleOptionTab tabOverlap; tabOverlap.shape = m_tabBar->shape(); int overlap = style()->pixelMetric(QStyle::PM_TabBarBaseOverlap, &tabOverlap, m_tabBar); if( overlap > 0 ) { QRect rect; rect.setRect(0, height()-overlap, width(), overlap); optTabBase.rect = rect; } if( m_tabBar->drawBase() ) { p.drawPrimitive(QStyle::PE_FrameTabBarBase, optTabBase); } } #endif KSqueezedTextLabel::paintEvent(ev); } QTabBar *m_tabBar; }; class StatusLabel: public UnderlinedLabel { Q_OBJECT public: explicit StatusLabel(QTabBar *tabBar, QWidget* parent = nullptr): UnderlinedLabel(tabBar, parent) { setAlignment(Qt::AlignRight | Qt::AlignVCenter); setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Fixed); } QSize minimumSizeHint() const override { QRect rect = style()->itemTextRect(fontMetrics(), QRect(), Qt::AlignRight, true, i18n("Line: 00000 Col: 000")); rect.setHeight(m_tabBar->height()); return rect.size(); } }; // class Container Container::Container(QWidget *parent) :QWidget(parent), d(new ContainerPrivate()) { KAcceleratorManager::setNoAccel(this); QBoxLayout *l = new QBoxLayout(QBoxLayout::TopToBottom, this); l->setMargin(0); l->setSpacing(0); d->layout = new QBoxLayout(QBoxLayout::LeftToRight); d->layout->setMargin(0); d->layout->setSpacing(0); d->documentListMenu = new QMenu(this); d->documentListButton = new QToolButton(this); d->documentListButton->setIcon(QIcon::fromTheme(QStringLiteral("format-list-unordered"))); d->documentListButton->setMenu(d->documentListMenu); +#ifdef Q_OS_MACOS + // for maintaining the Dock menu: + setFocusPolicy(Qt::StrongFocus); +#endif d->documentListButton->setPopupMode(QToolButton::InstantPopup); d->documentListButton->setAutoRaise(true); d->documentListButton->setToolTip(i18n("Show sorted list of opened documents")); d->documentListButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred); d->layout->addWidget(d->documentListButton); d->tabBar = new ContainerTabBar(this); d->tabBar->setContextMenuPolicy(Qt::CustomContextMenu); d->layout->addWidget(d->tabBar); d->fileStatus = new QLabel( this ); d->fileStatus->setFixedSize( QSize( 16, 16 ) ); d->layout->addWidget(d->fileStatus); d->fileNameCorner = new UnderlinedLabel(d->tabBar, this); d->layout->addWidget(d->fileNameCorner); d->statusCorner = new StatusLabel(d->tabBar, this); d->layout->addWidget(d->statusCorner); l->addLayout(d->layout); d->stack = new QStackedWidget(this); l->addWidget(d->stack); connect(d->tabBar, &ContainerTabBar::currentChanged, this, &Container::widgetActivated); connect(d->tabBar, &ContainerTabBar::tabCloseRequested, this, static_cast(&Container::requestClose)); connect(d->tabBar, &ContainerTabBar::newTabRequested, this, &Container::newTabRequested); connect(d->tabBar, &ContainerTabBar::tabMoved, this, &Container::tabMoved); connect(d->tabBar, &ContainerTabBar::customContextMenuRequested, this, &Container::contextMenu); connect(d->tabBar, &ContainerTabBar::tabBarDoubleClicked, this, &Container::doubleClickTriggered); connect(d->documentListMenu, &QMenu::triggered, this, &Container::documentListActionTriggered); setTabBarHidden(!configTabBarVisible()); d->tabBar->setTabsClosable(true); d->tabBar->setMovable(true); d->tabBar->setExpanding(false); d->tabBar->setSelectionBehaviorOnRemove(QTabBar::SelectPreviousTab); } bool Container::configTabBarVisible() { KConfigGroup group = KSharedConfig::openConfig()->group("UiSettings"); return group.readEntry("TabBarVisibility", 1); } void Container::setLeftCornerWidget(QWidget* widget) { if(d->leftCornerWidget.data() == widget) { if(d->leftCornerWidget) d->leftCornerWidget.data()->setParent(nullptr); }else{ delete d->leftCornerWidget.data(); d->leftCornerWidget.clear(); } d->leftCornerWidget = widget; if(!widget) return; widget->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred); d->layout->insertWidget(0, widget); widget->show(); } Container::~Container() { delete d; } QList Container::views() const { return d->viewForWidget.values(); } void Container::requestClose(int idx) { emit requestClose(widget(idx)); } void Container::widgetActivated(int idx) { if (idx < 0) return; if (QWidget* w = d->stack->widget(idx)) { Sublime::View* view = d->viewForWidget.value(w); if(view) emit activateView(view); } } void Container::addWidget(View *view, int position) { QWidget *w = view->widget(this); int idx = 0; if (position != -1) { idx = d->stack->insertWidget(position, w); } else idx = d->stack->addWidget(w); d->tabBar->insertTab(idx, view->document()->statusIcon(), view->document()->title()); Q_ASSERT(view); d->viewForWidget[w] = view; // Update document list context menu. This has to be called before // setCurrentWidget, because we call the status icon and title update slots // already, which in turn need the document list menu to be setup. d->updateDocumentListPopupMenu(); setCurrentWidget(d->stack->currentWidget()); // This fixes a strange layouting bug, that could be reproduced like this: Open a few files in KDevelop, activate the rightmost tab. // Then temporarily switch to another area, and then switch back. After that, the tab-bar was gone. // The problem could only be fixed by closing/opening another view. d->tabBar->setMinimumHeight(d->tabBar->sizeHint().height()); connect(view, &View::statusChanged, this, &Container::statusChanged); connect(view->document(), &Document::statusIconChanged, this, &Container::statusIconChanged); connect(view->document(), &Document::titleChanged, this, &Container::documentTitleChanged); } void Container::statusChanged(Sublime::View* view) { d->statusCorner->setText(view->viewStatus()); } void Container::statusIconChanged(Document* doc) { QHashIterator it = d->viewForWidget; while (it.hasNext()) { if (it.next().value()->document() == doc) { d->fileStatus->setPixmap( doc->statusIcon().pixmap( QSize( 16,16 ) ) ); int tabIndex = d->stack->indexOf(it.key()); if (tabIndex != -1) { d->tabBar->setTabIcon(tabIndex, doc->statusIcon()); } // Update the document title's menu associated action // using the View* index map Q_ASSERT(d->documentListActionForView.contains(it.value())); d->documentListActionForView[it.value()]->setIcon(doc->icon()); break; } } } void Container::documentTitleChanged(Sublime::Document* doc) { QHashIterator it = d->viewForWidget; while (it.hasNext()) { Sublime::View* view = it.next().value(); if (view->document() == doc) { if (currentView() == view) { d->fileNameCorner->setText( doc->title(Document::Extended) + i18n(" (Press Ctrl+Tab to switch)") ); } int tabIndex = d->stack->indexOf(it.key()); if (tabIndex != -1) { d->tabBar->setTabText(tabIndex, doc->title()); } break; } } // Update document list popup title d->updateDocumentListPopupMenu(); } int Container::count() const { return d->stack->count(); } QWidget* Container::currentWidget() const { return d->stack->currentWidget(); } void Container::setCurrentWidget(QWidget* w) { d->stack->setCurrentWidget(w); //prevent from emitting activateView() signal on tabbar active tab change //this function is called from MainWindow::activateView() //which does the activation without any additional signals { QSignalBlocker blocker(d->tabBar); d->tabBar->setCurrentIndex(d->stack->indexOf(w)); } if (View* view = viewForWidget(w)) { statusChanged(view); if (!d->tabBar->isVisible()) { // repaint icon and document title only in tabbar-less mode // tabbar will do repainting for us statusIconChanged( view->document() ); documentTitleChanged( view->document() ); } } } QWidget* Container::widget(int i) const { return d->stack->widget(i); } int Container::indexOf(QWidget* w) const { return d->stack->indexOf(w); } void Container::removeWidget(QWidget *w) { if (w) { int widgetIdx = d->stack->indexOf(w); d->stack->removeWidget(w); d->tabBar->removeTab(widgetIdx); if (d->tabBar->currentIndex() != -1 && !d->tabBar->isVisible()) { // repaint icon and document title only in tabbar-less mode // tabbar will do repainting for us View* view = currentView(); if( view ) { statusIconChanged( view->document() ); documentTitleChanged( view->document() ); } } View* view = d->viewForWidget.take(w); if (view) { disconnect(view->document(), &Document::titleChanged, this, &Container::documentTitleChanged); disconnect(view->document(), &Document::statusIconChanged, this, &Container::statusIconChanged); disconnect(view, &View::statusChanged, this, &Container::statusChanged); // Update document list context menu Q_ASSERT(d->documentListActionForView.contains(view)); delete d->documentListActionForView.take(view); } } } bool Container::hasWidget(QWidget *w) { return d->stack->indexOf(w) != -1; } View *Container::viewForWidget(QWidget *w) const { return d->viewForWidget.value(w); } void Container::setTabBarHidden(bool hide) { if (hide) { d->tabBar->hide(); d->fileNameCorner->show(); d->fileStatus->show(); } else { d->fileNameCorner->hide(); d->fileStatus->hide(); d->tabBar->show(); } View* v = currentView(); if (v) { documentTitleChanged(v->document()); } } void Container::resetTabColors(const QColor& color) { for (int i = 0; i < count(); i++){ d->tabBar->setTabTextColor(i, color); } } void Container::setTabColor(const View* view, const QColor& color) { for (int i = 0; i < count(); i++){ if (view == viewForWidget(widget(i))) { d->tabBar->setTabTextColor(i, color); } } } void Container::setTabColors(const QHash& colors) { for (int i = 0; i < count(); i++) { auto view = viewForWidget(widget(i)); auto color = colors[view]; if (color.isValid()) { d->tabBar->setTabTextColor(i, color); } } } void Container::tabMoved(int from, int to) { QWidget *w = d->stack->widget(from); d->stack->removeWidget(w); d->stack->insertWidget(to, w); d->viewForWidget[w]->notifyPositionChanged(to); } void Container::contextMenu( const QPoint& pos ) { QWidget* senderWidget = qobject_cast(sender()); Q_ASSERT(senderWidget); int currentTab = d->tabBar->tabAt(pos); QPointer menu = new QMenu(senderWidget); Sublime::View* view = viewForWidget(widget(currentTab)); emit tabContextMenuRequested(view, menu); menu->addSeparator(); QAction* copyPathAction = nullptr; QAction* closeTabAction = nullptr; QAction* closeOtherTabsAction = nullptr; if (view) { copyPathAction = menu->addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy Filename")); menu->addSeparator(); closeTabAction = menu->addAction(QIcon::fromTheme(QStringLiteral("document-close")), i18n("Close File")); closeOtherTabsAction = menu->addAction(QIcon::fromTheme(QStringLiteral("document-close")), i18n("Close Other Files")); } QAction* closeAllTabsAction = menu->addAction( QIcon::fromTheme(QStringLiteral("document-close")), i18n( "Close All Files" ) ); QAction* triggered = menu->exec(senderWidget->mapToGlobal(pos)); delete menu.data(); if (triggered) { if ( triggered == closeTabAction ) { requestClose(currentTab); } else if ( triggered == closeOtherTabsAction ) { // activate the remaining tab widgetActivated(currentTab); // first get the widgets to be closed since otherwise the indices will be wrong QList otherTabs; for ( int i = 0; i < count(); ++i ) { if ( i != currentTab ) { otherTabs << widget(i); } } // finally close other tabs foreach( QWidget* tab, otherTabs ) { requestClose(tab); } } else if ( triggered == closeAllTabsAction ) { // activate last tab widgetActivated(count() - 1); // close all for ( int i = 0; i < count(); ++i ) { requestClose(widget(i)); } } else if( triggered == copyPathAction ) { auto view = viewForWidget( widget( currentTab ) ); auto urlDocument = qobject_cast( view->document() ); if( urlDocument ) { QApplication::clipboard()->setText(urlDocument->url().toDisplayString(QUrl::PreferLocalFile)); } } // else the action was handled by someone else } } void Container::showTooltipForTab(int tab) { emit tabToolTipRequested(viewForWidget(widget(tab)), this, tab); } bool Container::isCurrentTab(int tab) const { return d->tabBar->currentIndex() == tab; } QRect Container::tabRect(int tab) const { return d->tabBar->tabRect(tab).translated(d->tabBar->mapToGlobal(QPoint(0, 0))); } void Container::doubleClickTriggered(int tab) { if (tab == -1) { emit newTabRequested(); } else { emit tabDoubleClicked(viewForWidget(widget(tab))); } } void Container::documentListActionTriggered(QAction* action) { Sublime::View* view = action->data().value< Sublime::View* >(); Q_ASSERT(view); QWidget* widget = d->viewForWidget.key(view); Q_ASSERT(widget); setCurrentWidget(widget); } Sublime::View* Container::currentView() const { return d->viewForWidget.value(widget( d->tabBar->currentIndex() )); } +void Container::focusInEvent(QFocusEvent* event) +{ + d->setAsDockMenu(); + QWidget::focusInEvent(event); +} + } #include "container.moc" diff --git a/sublime/container.h b/sublime/container.h index e85afd159..c065359e0 100644 --- a/sublime/container.h +++ b/sublime/container.h @@ -1,117 +1,120 @@ /*************************************************************************** * Copyright 2006-2007 Alexander Dymo * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU Library General Public License as * * published by the Free Software Foundation; either version 2 of the * * License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU Library General Public * * License along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #ifndef KDEVPLATFORM_SUBLIMECONTAINER_H #define KDEVPLATFORM_SUBLIMECONTAINER_H #include #include "sublimeexport.h" class QMenu; class QPaintEvent; namespace Sublime { class View; class Document; /** @short Container for the widgets. This container is placed inside mainwindow splitters to show widgets for views in the area. */ class KDEVPLATFORMSUBLIME_EXPORT Container: public QWidget { Q_OBJECT public: explicit Container(QWidget *parent = nullptr); ~Container() override; /**Adds the widget for given @p view to the container.*/ void addWidget(Sublime::View* view, int position = -1); /**Removes the widget from the container.*/ void removeWidget(QWidget *w); /** @return true if widget is placed inside this container.*/ bool hasWidget(QWidget *w); QList views() const; int count() const; QWidget *currentWidget() const; void setCurrentWidget(QWidget *w); QWidget *widget(int i) const; int indexOf(QWidget *w) const; View *viewForWidget(QWidget *w) const; void setTabBarHidden(bool hide); void setTabColor(const View* view, const QColor& color); void setTabColors(const QHash& colors); void resetTabColors(const QColor& color); /** Adds a corner widget to the left of this containers tab-bar. To remove it again, just delete it. * The ownership otherwise goes to the container. */ void setLeftCornerWidget(QWidget* widget); void showTooltipForTab(int tab); bool isCurrentTab(int tab) const; /// @return Rect in global position of the tab identified by index @p tab QRect tabRect(int tab) const; static bool configTabBarVisible(); Q_SIGNALS: void activateView(Sublime::View* view); void requestClose(QWidget *w); /** * This signal is emitted whenever the users double clicks on the free * space next to the tab bar. Typically, a new document should be * created. */ void newTabRequested(); void tabContextMenuRequested(Sublime::View* view, QMenu* menu); /** * @p view The view represented by the tab that was hovered * @p Container The tab container that triggered the event * @p idx The index of the tab that was hovered */ void tabToolTipRequested(Sublime::View* view, Sublime::Container* container, int idx); void tabDoubleClicked(Sublime::View* view); private Q_SLOTS: void widgetActivated(int idx); void documentTitleChanged(Sublime::Document* doc); void statusIconChanged(Sublime::Document*); void statusChanged(Sublime::View *view); void requestClose(int idx); void tabMoved(int from, int to); void contextMenu(const QPoint&); void doubleClickTriggered(int tab); void documentListActionTriggered(QAction*); private: Sublime::View* currentView() const; struct ContainerPrivate * const d; + +protected: + void focusInEvent(QFocusEvent*) override; }; } #endif