diff --git a/shell/plugincontroller.cpp b/shell/plugincontroller.cpp index 62e1d27be..e14da5e26 100644 --- a/shell/plugincontroller.cpp +++ b/shell/plugincontroller.cpp @@ -1,792 +1,792 @@ /* This file is part of the KDE project Copyright 2004, 2007 Alexander Dymo Copyright 2006 Matt Rogers Based on code from Kopete Copyright (c) 2002-2003 Martijn Klingens This library 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 library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "plugincontroller.h" #include #include #include #include #include #include // TODO: remove once we no longer support old style plugins #include #include #include #include #include #include #include #include "core.h" #include "shellextension.h" #include "runcontroller.h" #include "debugcontroller.h" #include "documentationcontroller.h" #include "sourceformattercontroller.h" #include "projectcontroller.h" #include "ktexteditorpluginintegration.h" #include "debug.h" namespace { inline QString KEY_Plugins() { return QStringLiteral("Plugins"); } inline QString KEY_Suffix_Enabled() { return QStringLiteral("Enabled"); } inline QString KEY_LoadMode() { return QStringLiteral("X-KDevelop-LoadMode"); } inline QString KEY_Category() { return QStringLiteral("X-KDevelop-Category"); } inline QString KEY_Mode() { return QStringLiteral("X-KDevelop-Mode"); } inline QString KEY_Version() { return QStringLiteral("X-KDevelop-Version"); } inline QString KEY_Interfaces() { return QStringLiteral("X-KDevelop-Interfaces"); } inline QString KEY_Required() { return QStringLiteral("X-KDevelop-IRequired"); } inline QString KEY_Optional() { return QStringLiteral("X-KDevelop-IOptional"); } inline QString KEY_Global() { return QStringLiteral("Global"); } inline QString KEY_Project() { return QStringLiteral("Project"); } inline QString KEY_Gui() { return QStringLiteral("GUI"); } inline QString KEY_AlwaysOn() { return QStringLiteral("AlwaysOn"); } inline QString KEY_UserSelectable() { return QStringLiteral("UserSelectable"); } bool isUserSelectable( const KPluginMetaData& info ) { QString loadMode = info.value(KEY_LoadMode()); return loadMode.isEmpty() || loadMode == KEY_UserSelectable(); } bool isGlobalPlugin( const KPluginMetaData& info ) { return info.value(KEY_Category()) == KEY_Global(); } bool hasMandatoryProperties( const KPluginMetaData& info ) { QString mode = info.value(KEY_Mode()); if (mode.isEmpty()) { return false; } // when the plugin is installed into the versioned plugin path, it's good to go if (info.fileName().contains(QLatin1String("/kdevplatform/" QT_STRINGIFY(KDEVELOP_PLUGIN_VERSION) "/"))) { return true; } // the version property is only required when the plugin is not installed into the right directory QVariant version = info.rawData().value(KEY_Version()).toVariant(); if (version.isValid() && version.value() == KDEVELOP_PLUGIN_VERSION) { return true; } return false; } bool constraintsMatch( const KPluginMetaData& info, const QVariantMap& constraints) { for (auto it = constraints.begin(); it != constraints.end(); ++it) { const auto property = info.rawData().value(it.key()).toVariant(); if (!property.isValid()) { return false; } else if (property.canConvert()) { QSet values = property.toStringList().toSet(); QSet expected = it.value().toStringList().toSet(); if (!values.contains(expected)) { return false; } } else if (it.value() != property) { return false; } } return true; } struct Dependency { Dependency(const QString &dependency) : interface(dependency) { if (dependency.contains('@')) { const auto list = dependency.split('@', QString::SkipEmptyParts); if (list.size() == 2) { interface = list.at(0); pluginName = list.at(1); } } } QString interface; QString pluginName; }; } namespace KDevelop { class PluginControllerPrivate { public: QVector plugins; //map plugin infos to currently loaded plugins typedef QHash InfoToPluginMap; InfoToPluginMap loadedPlugins; // The plugin manager's mode. The mode is StartingUp until loadAllPlugins() // has finished loading the plugins, after which it is set to Running. // ShuttingDown and DoneShutdown are used during shutdown by the // async unloading of plugins. enum CleanupMode { Running /**< the plugin manager is running */, CleaningUp /**< the plugin manager is cleaning up for shutdown */, CleanupDone /**< the plugin manager has finished cleaning up */ }; CleanupMode cleanupMode; bool canUnload(const KPluginMetaData& plugin) { qCDebug(SHELL) << "checking can unload for:" << plugin.name() << plugin.value(KEY_LoadMode()); if (plugin.value(KEY_LoadMode()) == KEY_AlwaysOn()) { return false; } const QStringList interfaces = KPluginMetaData::readStringList(plugin.rawData(), KEY_Interfaces()); qCDebug(SHELL) << "checking dependencies:" << interfaces; foreach (const KPluginMetaData& info, loadedPlugins.keys()) { if (info.pluginId() != plugin.pluginId()) { QStringList dependencies = KPluginMetaData::readStringList(plugin.rawData(), KEY_Required()); dependencies += KPluginMetaData::readStringList(plugin.rawData(), KEY_Optional()); foreach (const QString& dep, dependencies) { Dependency dependency(dep); if (!dependency.pluginName.isEmpty() && dependency.pluginName != plugin.pluginId()) { continue; } if (interfaces.contains(dependency.interface) && !canUnload(info)) { return false; } } } } return true; } KPluginMetaData infoForId( const QString& id ) const { foreach (const KPluginMetaData& info, plugins) { if (info.pluginId() == id) { return info; } } return KPluginMetaData(); } /** * Iterate over all cached plugin infos, and call the functor for every enabled plugin. * * If an extension and/or pluginName is given, the functor will only be called for * those plugins matching this information. * * The functor should return false when the iteration can be stopped, and true if it * should be continued. */ template void foreachEnabledPlugin(F func, const QString &extension = {}, const QVariantMap& constraints = QVariantMap(), const QString &pluginName = {}) { foreach (const auto& info, plugins) { if ((pluginName.isEmpty() || info.pluginId() == pluginName) && (extension.isEmpty() || KPluginMetaData::readStringList(info.rawData(), KEY_Interfaces()).contains(extension)) && constraintsMatch(info, constraints) && isEnabled(info)) { if (!func(info)) { break; } } } } /** * Decide whether a plugin is enabled */ bool isEnabled(const KPluginMetaData& info) const { static const QStringList disabledPlugins = QString::fromLatin1(qgetenv("KDEV_DISABLE_PLUGINS")).split(';'); if (disabledPlugins.contains(info.pluginId())) { return false; } if (!isUserSelectable( info )) return true; // in case there's a user preference, prefer that const KConfigGroup grp = Core::self()->activeSession()->config()->group( KEY_Plugins() ); const QString pluginEnabledKey = info.pluginId() + KEY_Suffix_Enabled(); if (grp.hasKey(pluginEnabledKey)) { return grp.readEntry(pluginEnabledKey, true); } // in all other cases: figure out if we want to load that plugin by default const auto defaultPlugins = ShellExtension::getInstance()->defaultPlugins(); const bool isDefaultPlugin = defaultPlugins.isEmpty() || defaultPlugins.contains(info.pluginId()); if (isDefaultPlugin) { return true; } if (!isGlobalPlugin( info )) { QJsonValue enabledByDefault = info.rawData()[QStringLiteral("KPlugin")].toObject()[QStringLiteral("EnabledByDefault")]; return enabledByDefault.isNull() || enabledByDefault.toBool(); //We consider plugins enabled until specified otherwise } return false; } Core *core; }; PluginController::PluginController(Core *core) : IPluginController(), d(new PluginControllerPrivate) { setObjectName(QStringLiteral("PluginController")); d->core = core; QSet foundPlugins; auto newPlugins = KPluginLoader::findPlugins(QStringLiteral("kdevplatform/" QT_STRINGIFY(KDEVELOP_PLUGIN_VERSION)), [&](const KPluginMetaData& meta) { if (meta.serviceTypes().contains(QStringLiteral("KDevelop/Plugin"))) { foundPlugins.insert(meta.pluginId()); return true; } else { qWarning() << "Plugin" << meta.fileName() << "is installed into the kdevplatform plugin directory, but does not have" " \"KDevelop/Plugin\" set as the service type. This plugin will not be loaded."; return false; } }); qCDebug(SHELL) << "Found" << newPlugins.size() << " plugins:" << foundPlugins; d->plugins = newPlugins; KTextEditorIntegration::initialize(); const QVector ktePlugins = KPluginLoader::findPlugins(QStringLiteral("ktexteditor"), [](const KPluginMetaData & md) { return md.serviceTypes().contains(QStringLiteral("KTextEditor/Plugin")) && md.serviceTypes().contains(QStringLiteral("KDevelop/Plugin")); }); foundPlugins.clear(); std::for_each(ktePlugins.cbegin(), ktePlugins.cend(), [&foundPlugins](const KPluginMetaData& data) { foundPlugins << data.pluginId(); }); qCDebug(SHELL) << "Found" << ktePlugins.size() << " KTextEditor plugins:" << foundPlugins; foreach (const auto& info, ktePlugins) { auto data = info.rawData(); // add some KDevelop specific JSON data data[KEY_Category()] = KEY_Global(); data[KEY_Mode()] = KEY_Gui(); data[KEY_Version()] = KDEVELOP_PLUGIN_VERSION; d->plugins.append({data, info.fileName(), info.metaDataFileName()}); } d->cleanupMode = PluginControllerPrivate::Running; // Register the KDevelop::IPlugin* metatype so we can properly unload it qRegisterMetaType( "KDevelop::IPlugin*" ); } PluginController::~PluginController() { if ( d->cleanupMode != PluginControllerPrivate::CleanupDone ) { qCWarning(SHELL) << "Destructing plugin controller without going through the shutdown process!"; } delete d; } KPluginMetaData PluginController::pluginInfo( const IPlugin* plugin ) const { return d->loadedPlugins.key(const_cast(plugin)); } void PluginController::cleanup() { if(d->cleanupMode != PluginControllerPrivate::Running) { //qCDebug(SHELL) << "called when not running. state =" << d->cleanupMode; return; } d->cleanupMode = PluginControllerPrivate::CleaningUp; // Ask all plugins to unload while ( !d->loadedPlugins.isEmpty() ) { //Let the plugin do some stuff before unloading unloadPlugin(d->loadedPlugins.begin().value(), Now); } d->cleanupMode = PluginControllerPrivate::CleanupDone; } IPlugin* PluginController::loadPlugin( const QString& pluginName ) { return loadPluginInternal( pluginName ); } bool PluginController::isEnabled( const KPluginMetaData& info ) const { return d->isEnabled(info); } void PluginController::initialize() { QElapsedTimer timer; timer.start(); QMap pluginMap; if( ShellExtension::getInstance()->defaultPlugins().isEmpty() ) { foreach( const KPluginMetaData& pi, d->plugins ) { pluginMap.insert( pi.pluginId(), true ); } } else { // Get the default from the ShellExtension foreach( const QString& s, ShellExtension::getInstance()->defaultPlugins() ) { pluginMap.insert( s, true ); } } KConfigGroup grp = Core::self()->activeSession()->config()->group( KEY_Plugins() ); QMap entries = grp.entryMap(); QMap::Iterator it; for ( it = entries.begin(); it != entries.end(); ++it ) { const QString key = it.key(); if (key.endsWith(KEY_Suffix_Enabled())) { bool enabled = false; const QString pluginid = key.left(key.length() - 7); const bool defValue = pluginMap.value( pluginid, false ); enabled = grp.readEntry(key, defValue); pluginMap.insert( pluginid, enabled ); } } foreach( const KPluginMetaData& pi, d->plugins ) { if( isGlobalPlugin( pi ) ) { QMap::const_iterator it = pluginMap.constFind( pi.pluginId() ); if( it != pluginMap.constEnd() && ( it.value() || !isUserSelectable( pi ) ) ) { // Plugin is mentioned in pluginmap and the value is true, so try to load it loadPluginInternal( pi.pluginId() ); if(!grp.hasKey(pi.pluginId() + KEY_Suffix_Enabled())) { if( isUserSelectable( pi ) ) { // If plugin isn't listed yet, add it with true now grp.writeEntry(pi.pluginId() + KEY_Suffix_Enabled(), true); } } else if( grp.hasKey( pi.pluginId() + "Disabled" ) && !isUserSelectable( pi ) ) { // Remove now-obsolete entries grp.deleteEntry( pi.pluginId() + "Disabled" ); } } } } // Synchronize so we're writing out to the file. grp.sync(); qCDebug(SHELL) << "Done loading plugins - took:" << timer.elapsed() << "ms"; } QList PluginController::loadedPlugins() const { return d->loadedPlugins.values(); } bool PluginController::unloadPlugin( const QString & pluginId ) { IPlugin *thePlugin = plugin( pluginId ); bool canUnload = d->canUnload( d->infoForId( pluginId ) ); qCDebug(SHELL) << "Unloading plugin:" << pluginId << "?" << thePlugin << canUnload; if( thePlugin && canUnload ) { return unloadPlugin(thePlugin, Later); } return (canUnload && thePlugin); } bool PluginController::unloadPlugin(IPlugin* plugin, PluginDeletion deletion) { qCDebug(SHELL) << "unloading plugin:" << plugin << pluginInfo( plugin ).name(); emit unloadingPlugin(plugin); plugin->unload(); emit pluginUnloaded(plugin); //Remove the plugin from our list of plugins so we create a new //instance when we're asked for it again. //This is important to do right here, not later when the plugin really //vanishes. For example project re-opening might try to reload the plugin //and then would get the "old" pointer which will be deleted in the next //event loop run and thus causing crashes. for ( PluginControllerPrivate::InfoToPluginMap::Iterator it = d->loadedPlugins.begin(); it != d->loadedPlugins.end(); ++it ) { if ( it.value() == plugin ) { d->loadedPlugins.erase( it ); break; } } if (deletion == Later) plugin->deleteLater(); else delete plugin; return true; } KPluginMetaData PluginController::infoForPluginId( const QString &pluginId ) const { foreach (const KPluginMetaData& info, d->plugins) { if (info.pluginId() == pluginId) { return info; } } return KPluginMetaData(); } IPlugin *PluginController::loadPluginInternal( const QString &pluginId ) { QElapsedTimer timer; timer.start(); KPluginMetaData info = infoForPluginId( pluginId ); if ( !info.isValid() ) { qCWarning(SHELL) << "Unable to find a plugin named '" << pluginId << "'!" ; return nullptr; } if ( IPlugin* plugin = d->loadedPlugins.value( info ) ) { return plugin; } if ( !isEnabled( info ) ) { // Do not load disabled plugins qWarning() << "Not loading plugin named" << pluginId << "because it has been disabled!"; return nullptr; } if ( !hasMandatoryProperties( info ) ) { qWarning() << "Unable to load plugin named" << pluginId << "because not all mandatory properties are set."; return nullptr; } if ( info.value(KEY_Mode()) == KEY_Gui() && Core::self()->setupFlags() == Core::NoUi ) { qCDebug(SHELL) << "Not loading plugin named" << pluginId << "- Running in No-Ui mode, but the plugin says it needs a GUI"; return nullptr; } qCDebug(SHELL) << "Attempting to load" << pluginId << "- name:" << info.name(); emit loadingPlugin( info.pluginId() ); // first, ensure all dependencies are available and not disabled // this is unrelated to whether they are loaded already or not. // when we depend on e.g. A and B, but B cannot be found, then we // do not want to load A first and then fail on B and leave A loaded. // this would happen if we'd skip this step here and directly loadDependencies. QStringList missingInterfaces; if ( !hasUnresolvedDependencies( info, missingInterfaces ) ) { qWarning() << "Can't load plugin" << pluginId << "some of its required dependencies could not be fulfilled:" << missingInterfaces.join(QStringLiteral(",")); return nullptr; } // now ensure all dependencies are loaded QString failedDependency; if( !loadDependencies( info, failedDependency ) ) { qWarning() << "Can't load plugin" << pluginId << "because a required dependency could not be loaded:" << failedDependency; return nullptr; } // same for optional dependencies, but don't error out if anything fails loadOptionalDependencies( info ); // now we can finally load the plugin itself KPluginLoader loader(info.fileName()); auto factory = loader.factory(); if (!factory) { qWarning() << "Can't load plugin" << pluginId << "because a factory to load the plugin could not be obtained:" << loader.errorString(); return nullptr; } // now create it auto plugin = factory->create(d->core); if (!plugin) { if (auto katePlugin = factory->create(d->core, QVariantList() << info.pluginId())) { plugin = new KTextEditorIntegration::Plugin(katePlugin, d->core); } else { - qWarning() << "Creating plugin" << pluginId << "failed."; + qCWarning(SHELL) << "Creating plugin" << pluginId << "failed."; return nullptr; } } KConfigGroup group = Core::self()->activeSession()->config()->group(KEY_Plugins()); // runtime errors such as missing executables on the system or such get checked now if (plugin->hasError()) { - qWarning() << "Could not load plugin" << pluginId << ", it reported the error:" << plugin->errorDescription() + qCWarning(SHELL) << "Could not load plugin" << pluginId << ", it reported the error:" << plugin->errorDescription() << "Disabling the plugin now."; group.writeEntry(info.pluginId() + KEY_Suffix_Enabled(), false); // do the same as KPluginInfo did group.sync(); unloadPlugin(pluginId); return nullptr; } // yay, it all worked - the plugin is loaded d->loadedPlugins.insert(info, plugin); group.writeEntry(info.pluginId() + KEY_Suffix_Enabled(), true); // do the same as KPluginInfo did group.sync(); qCDebug(SHELL) << "Successfully loaded plugin" << pluginId << "from" << loader.fileName() << "- took:" << timer.elapsed() << "ms"; emit pluginLoaded( plugin ); return plugin; } IPlugin* PluginController::plugin( const QString& pluginId ) { KPluginMetaData info = infoForPluginId( pluginId ); if ( !info.isValid() ) return nullptr; return d->loadedPlugins.value( info ); } bool PluginController::hasUnresolvedDependencies( const KPluginMetaData& info, QStringList& missing ) const { QSet required = KPluginMetaData::readStringList(info.rawData(), KEY_Required()).toSet(); if (!required.isEmpty()) { d->foreachEnabledPlugin([&required] (const KPluginMetaData& plugin) -> bool { foreach (const QString& iface, KPluginMetaData::readStringList(plugin.rawData(), KEY_Interfaces())) { required.remove(iface); required.remove(iface + '@' + plugin.pluginId()); } return !required.isEmpty(); }); } // if we found all dependencies required should be empty now if (!required.isEmpty()) { missing = required.toList(); return false; } return true; } void PluginController::loadOptionalDependencies( const KPluginMetaData& info ) { const QStringList dependencies = KPluginMetaData::readStringList(info.rawData(), KEY_Optional()); foreach (const QString& dep, dependencies) { Dependency dependency(dep); if (!pluginForExtension(dependency.interface, dependency.pluginName)) { qCDebug(SHELL) << "Couldn't load optional dependency:" << dep << info.pluginId(); } } } bool PluginController::loadDependencies( const KPluginMetaData& info, QString& failedDependency ) { const QStringList dependencies = KPluginMetaData::readStringList(info.rawData(), KEY_Required()); foreach (const QString& value, dependencies) { Dependency dependency(value); if (!pluginForExtension(dependency.interface, dependency.pluginName)) { failedDependency = value; return false; } } return true; } IPlugin *PluginController::pluginForExtension(const QString &extension, const QString &pluginName, const QVariantMap& constraints) { IPlugin* plugin = nullptr; d->foreachEnabledPlugin([this, &plugin] (const KPluginMetaData& info) -> bool { plugin = d->loadedPlugins.value( info ); if( !plugin ) { plugin = loadPluginInternal( info.pluginId() ); } return !plugin; }, extension, constraints, pluginName); return plugin; } QList PluginController::allPluginsForExtension(const QString &extension, const QVariantMap& constraints) { //qCDebug(SHELL) << "Finding all Plugins for Extension:" << extension << "|" << constraints; QList plugins; d->foreachEnabledPlugin([this, &plugins] (const KPluginMetaData& info) -> bool { IPlugin* plugin = d->loadedPlugins.value( info ); if( !plugin) { plugin = loadPluginInternal( info.pluginId() ); } if (plugin && !plugins.contains(plugin)) { plugins << plugin; } return true; }, extension, constraints); return plugins; } QVector PluginController::queryExtensionPlugins(const QString& extension, const QVariantMap& constraints) const { QVector plugins; d->foreachEnabledPlugin([&plugins] (const KPluginMetaData& info) -> bool { plugins << info; return true; }, extension, constraints); return plugins; } QStringList PluginController::allPluginNames() { QStringList names; Q_FOREACH( const KPluginMetaData& info , d->plugins ) { names << info.pluginId(); } return names; } QList PluginController::queryPluginsForContextMenuExtensions(KDevelop::Context* context) const { // This fixes random order of extension menu items between different runs of KDevelop. // Without sorting we have random reordering of "Analyze With" submenu for example: // 1) "Cppcheck" actions, "Vera++" actions - first run // 2) "Vera++" actions, "Cppcheck" actions - some other run. QMultiMap sortedPlugins; for (auto it = d->loadedPlugins.constBegin(); it != d->loadedPlugins.constEnd(); ++it) { sortedPlugins.insert(it.key().name(), it.value()); } QList exts; foreach (IPlugin* plugin, sortedPlugins) { exts << plugin->contextMenuExtension(context); } exts << Core::self()->debugControllerInternal()->contextMenuExtension(context); exts << Core::self()->documentationControllerInternal()->contextMenuExtension(context); exts << Core::self()->sourceFormatterControllerInternal()->contextMenuExtension(context); exts << Core::self()->runControllerInternal()->contextMenuExtension(context); exts << Core::self()->projectControllerInternal()->contextMenuExtension(context); return exts; } QStringList PluginController::projectPlugins() { QStringList names; foreach (const KPluginMetaData& info, d->plugins) { if (info.value(KEY_Category()) == KEY_Project()) { names << info.pluginId(); } } return names; } void PluginController::loadProjectPlugins() { Q_FOREACH( const QString& name, projectPlugins() ) { loadPluginInternal( name ); } } void PluginController::unloadProjectPlugins() { Q_FOREACH( const QString& name, projectPlugins() ) { unloadPlugin( name ); } } QVector PluginController::allPluginInfos() const { return d->plugins; } void PluginController::updateLoadedPlugins() { QStringList defaultPlugins = ShellExtension::getInstance()->defaultPlugins(); KConfigGroup grp = Core::self()->activeSession()->config()->group( KEY_Plugins() ); foreach( const KPluginMetaData& info, d->plugins ) { if( isGlobalPlugin( info ) ) { bool enabled = grp.readEntry(info.pluginId() + KEY_Suffix_Enabled(), ( defaultPlugins.isEmpty() || defaultPlugins.contains( info.pluginId() ) ) ) || !isUserSelectable( info ); bool loaded = d->loadedPlugins.contains( info ); if( loaded && !enabled ) { qCDebug(SHELL) << "unloading" << info.pluginId(); if( !unloadPlugin( info.pluginId() ) ) { grp.writeEntry( info.pluginId() + KEY_Suffix_Enabled(), false ); } } else if( !loaded && enabled ) { loadPluginInternal( info.pluginId() ); } } } } void PluginController::resetToDefaults() { KSharedConfigPtr cfg = Core::self()->activeSession()->config(); cfg->deleteGroup( KEY_Plugins() ); cfg->sync(); KConfigGroup grp = cfg->group( KEY_Plugins() ); QStringList plugins = ShellExtension::getInstance()->defaultPlugins(); if( plugins.isEmpty() ) { foreach( const KPluginMetaData& info, d->plugins ) { plugins << info.pluginId(); } } foreach( const QString& s, plugins ) { grp.writeEntry(s + KEY_Suffix_Enabled(), true); } grp.sync(); } } diff --git a/shell/progresswidget/statusbarprogresswidget.cpp b/shell/progresswidget/statusbarprogresswidget.cpp index a15146cb9..da18aad1e 100644 --- a/shell/progresswidget/statusbarprogresswidget.cpp +++ b/shell/progresswidget/statusbarprogresswidget.cpp @@ -1,287 +1,288 @@ /* statusbarprogresswidget.cpp (C) 2004 Till Adam Don Sanders David Faure Copyright 2004 David Faure Includes StatusbarProgressWidget which is based on KIOLittleProgressDlg by Matt Koss KMail is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License, version 2 or above, as published by the Free Software Foundation. KMail 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 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 In addition, as a special exception, the copyright holders give permission to link the code of this program with any edition of the Qt library by Trolltech AS, Norway (or with modified versions of Qt that use the same license as Qt), and distribute linked combinations including the two. You must obey the GNU General Public License in all respects for all of the code used other than Qt. If you modify this file, you may extend this exception to your version of the file, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your version. */ #include "statusbarprogresswidget.h" #include "progressdialog.h" #include "progressmanager.h" #ifdef Q_OS_OSX #include "../macdockprogressview.h" #endif #include #include #include #include #include #include #include #include #include #include #include using namespace KDevelop; //----------------------------------------------------------------------------- StatusbarProgressWidget::StatusbarProgressWidget( ProgressDialog* progressDialog, QWidget* parent, bool button ) : QFrame( parent ), mCurrentItem( nullptr ), mProgressDialog( progressDialog ), mDelayTimer( nullptr ), mCleanTimer( nullptr ) { m_bShowButton = button; int w = fontMetrics().width( QStringLiteral(" 999.9 kB/s 00:00:01 ") ) + 8; box = new QHBoxLayout( this ); box->setMargin(0); box->setSpacing(0); m_pButton = new QToolButton( this ); m_pButton->setAttribute(Qt::WA_MacMiniSize); m_pButton->setSizePolicy( QSizePolicy( QSizePolicy::Minimum, QSizePolicy::Minimum ) ); QIcon smallIcon = QIcon::fromTheme( QStringLiteral("go-up") ); m_pButton->setIcon( smallIcon ); box->addWidget( m_pButton ); stack = new QStackedWidget( this ); int maximumHeight = fontMetrics().height(); stack->setMaximumHeight( maximumHeight ); box->addWidget( stack ); m_pButton->setToolTip( i18n("Open detailed progress dialog") ); m_pButton->setAutoRaise(true); m_pProgressBar = new QProgressBar( this ); m_pProgressBar->installEventFilter( this ); m_pProgressBar->setMinimumWidth( w ); stack->insertWidget( 1,m_pProgressBar ); m_pLabel = new QLabel( QString(), this ); m_pLabel->setAlignment( Qt::AlignHCenter | Qt::AlignVCenter ); m_pLabel->installEventFilter( this ); m_pLabel->setMinimumWidth( w ); stack->insertWidget( 2, m_pLabel ); #ifndef Q_OS_MAC // Currently on OSX this causes the button to be cut-off // It isn't necessary because on OSX the button's minimumSizeHint is small enough m_pButton->setMaximumHeight( maximumHeight ); #endif setMinimumWidth( minimumSizeHint().width() ); mode = None; setMode(); connect( m_pButton, &QPushButton::clicked, progressDialog, &ProgressDialog::slotToggleVisibility ); connect ( ProgressManager::instance(), &ProgressManager::progressItemAdded, this, &StatusbarProgressWidget::slotProgressItemAdded ); connect ( ProgressManager::instance(), &ProgressManager::progressItemCompleted, this, &StatusbarProgressWidget::slotProgressItemCompleted ); connect ( ProgressManager::instance(), &ProgressManager::progressItemUsesBusyIndicator, this, &StatusbarProgressWidget::updateBusyMode ); connect ( progressDialog, &ProgressDialog::visibilityChanged, this, &StatusbarProgressWidget::slotProgressDialogVisible ); mDelayTimer = new QTimer( this ); mDelayTimer->setSingleShot( true ); connect ( mDelayTimer, &QTimer::timeout, this, &StatusbarProgressWidget::slotShowItemDelayed ); mCleanTimer = new QTimer( this ); mCleanTimer->setSingleShot( true ); connect ( mCleanTimer, &QTimer::timeout, this, &StatusbarProgressWidget::slotClean ); } // There are three cases: no progressitem, one progressitem (connect to it directly), // or many progressitems (display busy indicator). Let's call them 0,1,N. // In slot..Added we can only end up in 1 or N. // In slot..Removed we can end up in 0, 1, or we can stay in N if we were already. void StatusbarProgressWidget::updateBusyMode() { connectSingleItem(); // if going to 1 item - mDelayTimer->start( 1000 ); + if (!mDelayTimer->isActive()) + mDelayTimer->start( 1000 ); } void StatusbarProgressWidget::slotProgressItemAdded( ProgressItem *item ) { if ( item->parent() ) return; // we are only interested in top level items updateBusyMode(); } void StatusbarProgressWidget::slotProgressItemCompleted( ProgressItem *item ) { if ( item->parent() ) { item->deleteLater(); item = nullptr; return; // we are only interested in top level items } item->deleteLater(); item = nullptr; connectSingleItem(); // if going back to 1 item if ( ProgressManager::instance()->isEmpty() ) { // No item // Done. In 5s the progress-widget will close, then we can clean up the statusbar mCleanTimer->start( 5000 ); } else if ( mCurrentItem ) { // Exactly one item activateSingleItemMode(); } } void StatusbarProgressWidget::connectSingleItem() { if ( mCurrentItem ) { disconnect ( mCurrentItem, &ProgressItem::progressItemProgress, this, &StatusbarProgressWidget::slotProgressItemProgress ); mCurrentItem = nullptr; } mCurrentItem = ProgressManager::instance()->singleItem(); if ( mCurrentItem ) { connect ( mCurrentItem, &ProgressItem::progressItemProgress, this, &StatusbarProgressWidget::slotProgressItemProgress ); } } void StatusbarProgressWidget::activateSingleItemMode() { m_pProgressBar->setMaximum( 100 ); m_pProgressBar->setValue( mCurrentItem->progress() ); m_pProgressBar->setTextVisible( true ); #ifdef Q_OS_OSX MacDockProgressView::setRange( 0, 100 ); MacDockProgressView::setProgress( mCurrentItem->progress() ); #endif } void StatusbarProgressWidget::slotShowItemDelayed() { bool noItems = ProgressManager::instance()->isEmpty(); if ( mCurrentItem ) { activateSingleItemMode(); } else if ( !noItems ) { // N items m_pProgressBar->setMaximum( 0 ); m_pProgressBar->setTextVisible( false ); #ifdef Q_OS_OSX MacDockProgressView::setRange( 0, 0 ); MacDockProgressView::setProgress( 0 ); #endif } if ( !noItems && mode == None ) { mode = Progress; setMode(); } } void StatusbarProgressWidget::slotProgressItemProgress( ProgressItem *item, unsigned int value ) { Q_ASSERT( item == mCurrentItem); // the only one we should be connected to Q_UNUSED( item ); m_pProgressBar->setValue( value ); #ifdef Q_OS_OSX MacDockProgressView::setProgress( value ); #endif } void StatusbarProgressWidget::setMode() { switch ( mode ) { case None: if ( m_bShowButton ) { m_pButton->hide(); } // show the empty label in order to make the status bar look better stack->show(); stack->setCurrentWidget( m_pLabel ); #ifdef Q_OS_OSX MacDockProgressView::setProgressVisible( false ); #endif break; case Progress: stack->show(); stack->setCurrentWidget( m_pProgressBar ); if ( m_bShowButton ) { m_pButton->show(); } #ifdef Q_OS_OSX MacDockProgressView::setProgressVisible( true ); #endif break; } } void StatusbarProgressWidget::slotClean() { // check if a new item showed up since we started the timer. If not, clear if ( ProgressManager::instance()->isEmpty() ) { m_pProgressBar->setValue( 0 ); //m_pLabel->clear(); mode = None; setMode(); } } bool StatusbarProgressWidget::eventFilter( QObject *, QEvent *ev ) { if ( ev->type() == QEvent::MouseButtonPress ) { QMouseEvent *e = (QMouseEvent*)ev; if ( e->button() == Qt::LeftButton && mode != None ) { // toggle view on left mouse button // Consensus seems to be that we should show/hide the fancy dialog when the user // clicks anywhere in the small one. mProgressDialog->slotToggleVisibility(); return true; } } return false; } void StatusbarProgressWidget::slotProgressDialogVisible( bool b ) { // Update the hide/show button when the detailed one is shown/hidden if ( b ) { m_pButton->setIcon( QIcon::fromTheme( QStringLiteral("go-down") ) ); m_pButton->setToolTip( i18n("Hide detailed progress window") ); setMode(); } else { m_pButton->setIcon( QIcon::fromTheme( QStringLiteral("go-up") ) ); m_pButton->setToolTip( i18n("Show detailed progress window") ); } } diff --git a/sublime/container.cpp b/sublime/container.cpp index a189b1e59..9b7db4a8d 100644 --- a/sublime/container.cpp +++ b/sublime/container.cpp @@ -1,668 +1,687 @@ /*************************************************************************** * 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 #include "view.h" #include "document.h" #include namespace Sublime { // struct ContainerPrivate class ContainerTabBar : public QTabBar { Q_OBJECT public: ContainerTabBar(Container* container) : QTabBar(container), m_container(container) { + if (QApplication::style()->objectName() == QLatin1String("macintosh")) { + static QPointer qTabBarStyle = QStyleFactory::create("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(); int tab = tabAt(mapFromGlobal(QCursor::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(mapFromGlobal(QCursor::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; } 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. } } }; class UnderlinedLabel: public KSqueezedTextLabel { Q_OBJECT public: 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: 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); 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 d->tabBar->blockSignals(true); d->tabBar->setCurrentIndex(d->stack->indexOf(w)); d->tabBar->blockSignals(false); 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); QMenu menu; 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)); 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().toString() ); } } // 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() )); } } #include "container.moc"