/* * Copyright 2007 by Aaron Seigo * Copyright 2008 by Ménard Alexis * Copyright 2009 Chani Armitage * Copyright 2012 Marco Martin * * 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, 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 "containment.h" #include "private/containment_p.h" #include "config-plasma.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if !PLASMA_NO_KIO #include "kio/jobclasses.h" // for KIO::JobFlags #include "kio/job.h" #include "kio/scheduler.h" #endif #include "containmentactions.h" #include "containmentactionspluginsconfig.h" #include "corona.h" #include "pluginloader.h" #include "svg.h" #include "remote/accessappletjob.h" #include "remote/accessmanager.h" #include "private/applet_p.h" #include "private/containmentactionspluginsconfig_p.h" #include "plasma/plasma.h" namespace Plasma { Containment::Containment(QObject *parent, const QString &serviceId, uint containmentId) : Applet(parent, serviceId, containmentId), d(new ContainmentPrivate(this)) { // WARNING: do not access config() OR globalConfig() in this method! // that requires a scene, which is not available at this point setBackgroundHints(NoBackground); setContainmentType(CustomContainment); setHasConfigurationInterface(false); } Containment::Containment(QObject *parent, const QVariantList &args) : Applet(parent, args), d(new ContainmentPrivate(this)) { // WARNING: do not access config() OR globalConfig() in this method! // that requires a scene, which is not available at this point setBackgroundHints(NoBackground); setHasConfigurationInterface(false); } Containment::Containment(const QString &packagePath, uint appletId, const QVariantList &args) : Applet(packagePath, appletId, args), d(new ContainmentPrivate(this)) { // WARNING: do not access config() OR globalConfig() in this method! // that requires a scene, which is not available at this point setBackgroundHints(NoBackground); setHasConfigurationInterface(false); } Containment::~Containment() { // Applet touches our dptr if we are a containment and is the superclass (think of dtors) // so we reset this as we exit the building Applet::d->isContainment = false; delete d; } void Containment::init() { Applet::init(); if (!isContainment()) { return; } if (d->type == NoContainmentType) { setContainmentType(DesktopContainment); } //connect actions ContainmentPrivate::addDefaultActions(d->actions(), this); bool unlocked = immutability() == Mutable; //fix the text of the actions that need name() //btw, do we really want to use name() when it's a desktopcontainment? QAction *closeApplet = action("remove"); if (closeApplet) { closeApplet->setText(i18nc("%1 is the name of the applet", "Remove this %1", name())); } QAction *configAction = action("configure"); if (configAction) { configAction->setText(i18nc("%1 is the name of the applet", "%1 Settings", name())); } QAction *appletBrowserAction = action("add widgets"); if (appletBrowserAction) { appletBrowserAction->setVisible(unlocked); appletBrowserAction->setEnabled(unlocked); connect(appletBrowserAction, SIGNAL(triggered()), this, SLOT(triggerShowAddWidgets())); } QAction *act = action("next applet"); if (act) { connect(act, SIGNAL(triggered()), this, SLOT(focusNextApplet())); } act = action("previous applet"); if (act) { connect(act, SIGNAL(triggered()), this, SLOT(focusPreviousApplet())); } if (immutability() != SystemImmutable && corona()) { QAction *lockDesktopAction = corona()->action("lock widgets"); //keep a pointer so nobody notices it moved to corona if (lockDesktopAction) { d->actions()->addAction("lock widgets", lockDesktopAction); } } if (d->type != PanelContainment && d->type != CustomPanelContainment) { if (corona()) { //FIXME this is just here because of the darn keyboard shortcut :/ act = corona()->action("manage activities"); if (act) { d->actions()->addAction("manage activities", act); } //a stupid hack to make this one's keyboard shortcut work act = corona()->action("configure shortcuts"); if (act) { d->actions()->addAction("configure shortcuts", act); } } if (d->type == DesktopContainment) { addToolBoxAction(action("add widgets")); //TODO: do we need some way to allow this be overridden? // it's always available because shells rely on this // to offer their own custom configuration as well QAction *configureContainment = action("configure"); if (configureContainment) { addToolBoxAction(configureContainment); } } } } // helper function for sorting the list of applets bool appletConfigLessThan(const KConfigGroup &c1, const KConfigGroup &c2) { QPointF p1 = c1.readEntry("geometry", QRectF()).topLeft(); QPointF p2 = c2.readEntry("geometry", QRectF()).topLeft(); if (!qFuzzyCompare(p1.x(), p2.x())) { if (QApplication::layoutDirection() == Qt::RightToLeft) { return p1.x() > p2.x(); } return p1.x() < p2.x(); } return qFuzzyCompare(p1.y(), p2.y()) || p1.y() < p2.y(); } void Containment::restore(KConfigGroup &group) { /* #ifndef NDEBUG kDebug() << "!!!!!!!!!!!!initConstraints" << group.name() << d->type; kDebug() << " location:" << group.readEntry("location", (int)d->location); kDebug() << " geom:" << group.readEntry("geometry", geometry()); kDebug() << " formfactor:" << group.readEntry("formfactor", (int)d->formFactor); kDebug() << " screen:" << group.readEntry("screen", d->screen); #endif */ if (!isContainment()) { Applet::restore(group); return; } setLocation((Plasma::Location)group.readEntry("location", (int)d->location)); setFormFactor((Plasma::FormFactor)group.readEntry("formfactor", (int)d->formFactor)); //kDebug() << "setScreen from restore"; d->lastScreen = group.readEntry("lastScreen", d->lastScreen); d->lastDesktop = group.readEntry("lastDesktop", d->lastDesktop); d->setScreen(group.readEntry("screen", d->screen), group.readEntry("desktop", d->desktop), false); d->activityId = group.readEntry("activityId", QString()); flushPendingConstraintsEvents(); restoreContents(group); setImmutability((ImmutabilityType)group.readEntry("immutability", (int)Mutable)); setWallpaper(group.readEntry("wallpaperplugin", ContainmentPrivate::defaultWallpaper), group.readEntry("wallpaperpluginmode", ContainmentPrivate::defaultWallpaperMode)); KConfigGroup cfg; if (containmentType() == PanelContainment || containmentType() == CustomPanelContainment) { //don't let global desktop actions conflict with panels //this also prevents panels from sharing config with each other //but the panels aren't configurable anyways, and I doubt that'll change. d->containmentActionsSource = ContainmentPrivate::Local; cfg = KConfigGroup(&group, "ActionPlugins"); } else { const QString source = group.readEntry("ActionPluginsSource", QString()); if (source == "Global") { cfg = KConfigGroup(corona()->config(), "ActionPlugins"); d->containmentActionsSource = ContainmentPrivate::Global; } else if (source == "Activity") { cfg = KConfigGroup(corona()->config(), "Activities"); cfg = KConfigGroup(&cfg, d->activityId); cfg = KConfigGroup(&cfg, "ActionPlugins"); d->containmentActionsSource = ContainmentPrivate::Activity; } else if (source == "Local") { cfg = group; d->containmentActionsSource = ContainmentPrivate::Local; } else { //default to global //but, if there is no global config, try copying it from local. cfg = KConfigGroup(corona()->config(), "ActionPlugins"); if (!cfg.exists()) { cfg = KConfigGroup(&group, "ActionPlugins"); } d->containmentActionsSource = ContainmentPrivate::Global; group.writeEntry("ActionPluginsSource", "Global"); } } //kDebug() << cfg.keyList(); if (cfg.exists()) { foreach (const QString &key, cfg.keyList()) { //kDebug() << "loading" << key; setContainmentActions(key, cfg.readEntry(key, QString())); } } else { //shell defaults ContainmentActionsPluginsConfig conf = corona()->containmentActionsDefaults(d->type); //steal the data directly, for efficiency QHash defaults = conf.d->plugins; for (QHash::const_iterator it = defaults.constBegin(), end = defaults.constEnd(); it != end; ++it) { setContainmentActions(it.key(), it.value()); } } /* #ifndef NDEBUG kDebug() << "Containment" << id() << #endif "screen" << screen() << "geometry is" << geometry() << "wallpaper" << ((d->wallpaper) ? d->wallpaper->pluginName() : QString()) << "wallpaper mode" << wallpaperMode() << "config entries" << group.entryMap(); */ } void Containment::save(KConfigGroup &g) const { if (Applet::d->transient) { return; } KConfigGroup group = g; if (!group.isValid()) { group = config(); } // locking is saved in Applet::save Applet::save(group); if (!isContainment()) { return; } group.writeEntry("screen", d->screen); group.writeEntry("lastScreen", d->lastScreen); group.writeEntry("desktop", d->desktop); group.writeEntry("lastDesktop", d->lastDesktop); group.writeEntry("formfactor", (int)d->formFactor); group.writeEntry("location", (int)d->location); group.writeEntry("activityId", d->activityId); group.writeEntry("wallpaperplugin", d->wallpaper); group.writeEntry("wallpaperpluginmode", d->wallpaperMode); //TODO: the wallpaper implementation must know it has to save at this point saveContents(group); } void Containment::saveContents(KConfigGroup &group) const { KConfigGroup applets(&group, "Applets"); foreach (const Applet *applet, d->applets) { KConfigGroup appletConfig(&applets, QString::number(applet->id())); applet->save(appletConfig); } } void Containment::restoreContents(KConfigGroup &group) { KConfigGroup applets(&group, "Applets"); // Sort the applet configs in order of geometry to ensure that applets // are added from left to right or top to bottom for a panel containment QList appletConfigs; foreach (const QString &appletGroup, applets.groupList()) { //kDebug() << "reading from applet group" << appletGroup; KConfigGroup appletConfig(&applets, appletGroup); appletConfigs.append(appletConfig); } qStableSort(appletConfigs.begin(), appletConfigs.end(), appletConfigLessThan); QMutableListIterator it(appletConfigs); while (it.hasNext()) { KConfigGroup &appletConfig = it.next(); int appId = appletConfig.name().toUInt(); QString plugin = appletConfig.readEntry("plugin", QString()); if (plugin.isEmpty()) { continue; } d->addApplet(plugin, QVariantList(), appletConfig.readEntry("geometry", QRectF()), appId, true); } } Containment::Type Containment::containmentType() const { return d->type; } void Containment::setContainmentType(Containment::Type type) { if (d->type == type) { return; } d->type = type; d->checkContainmentFurniture(); } Corona *Containment::corona() const { return qobject_cast(parent()); } void Containment::showDropZone(const QPoint pos) { Q_UNUSED(pos) //Base implementation does nothing, don't put code here } void Containment::showContextMenu(const QPointF &containmentPos, const QPoint &screenPos) { //kDebug() << containmentPos << screenPos; QContextMenuEvent gvevent(QContextMenuEvent::Mouse, screenPos); //FIXME: do we need views here? //gvevent.setWidget(view()); contextMenuEvent(&gvevent); } void Containment::contextMenuEvent(QContextMenuEvent *event) { if (!isContainment() || !KAuthorized::authorizeKAction("plasma/containment_context_menu")) { return; } KMenu desktopMenu; //TODO: port to the new containmentactions architecture Applet *applet = 0;//d->appletAt(event->pos()); //kDebug() << "context menu event " << (QObject*)applet; if (applet) { d->addAppletActions(desktopMenu, applet, event); } else { d->addContainmentActions(desktopMenu, event); } //kDebug() << "executing at" << screenPos; QMenu *menu = &desktopMenu; //kDebug() << "showing menu, actions" << desktopMenu.actions().size() << desktopMenu.actions().first()->menu(); if (desktopMenu.actions().size() == 1 && desktopMenu.actions().first()->menu()) { // we have a menu with a single top level menu; just show that top level menu instad. menu = desktopMenu.actions().first()->menu(); } if (!menu->isEmpty()) { QPoint pos = event->globalPos(); if (applet && d->isPanelContainment()) { menu->adjustSize(); pos = applet->popupPosition(menu->size()); if (event->reason() == QContextMenuEvent::Mouse) { // if the menu pops up way away from the mouse press, then move it // to the mouse press if (d->formFactor == Vertical) { if (pos.y() + menu->height() < event->globalPos().y()) { pos.setY(event->globalPos().y()); } } else if (d->formFactor == Horizontal) { if (pos.x() + menu->width() < event->globalPos().x()) { pos.setX(event->globalPos().x()); } } } } menu->exec(pos); event->accept(); } } void Containment::setFormFactor(FormFactor formFactor) { if (d->formFactor == formFactor) { return; } //kDebug() << "switching FF to " << formFactor; d->formFactor = formFactor; updateConstraints(Plasma::FormFactorConstraint); KConfigGroup c = config(); c.writeEntry("formfactor", (int)formFactor); emit configNeedsSaving(); } void Containment::setLocation(Location location) { if (d->location == location) { return; } d->location = location; foreach (Applet *applet, d->applets) { applet->updateConstraints(Plasma::LocationConstraint); } updateConstraints(Plasma::LocationConstraint); KConfigGroup c = config(); c.writeEntry("location", (int)location); emit configNeedsSaving(); } void Containment::addSiblingContainment() { emit addSiblingContainment(this); } void Containment::clearApplets() { foreach (Applet *applet, d->applets) { applet->d->cleanUpAndDelete(); } d->applets.clear(); } Applet *Containment::addApplet(const QString &name, const QVariantList &args, const QRectF &appletGeometry) { return d->addApplet(name, args, appletGeometry); } void Containment::addApplet(Applet *applet, const QPointF &pos, bool delayInit) { if (!isContainment() || (!delayInit && immutability() != Mutable)) { return; } if (!applet) { #ifndef NDEBUG kDebug() << "adding null applet!?!"; #endif return; } if (d->applets.contains(applet)) { #ifndef NDEBUG kDebug() << "already have this applet!"; #endif } Containment *currentContainment = applet->containment(); if (d->type == PanelContainment) { //panels don't want backgrounds, which is important when setting geometry setBackgroundHints(NoBackground); } if (currentContainment && currentContainment != this) { emit currentContainment->appletRemoved(applet); disconnect(applet, 0, currentContainment, 0); KConfigGroup oldConfig = applet->config(); currentContainment->d->applets.removeAll(applet); applet->setParent(this); // now move the old config to the new location //FIXME: this doesn't seem to get the actual main config group containing plugin=, etc KConfigGroup c = config().group("Applets").group(QString::number(applet->id())); oldConfig.reparent(&c); applet->d->resetConfigurationObject(); disconnect(applet, SIGNAL(activate()), currentContainment, SIGNAL(activate())); } else { applet->setParent(this); } d->applets << applet; connect(applet, SIGNAL(configNeedsSaving()), this, SIGNAL(configNeedsSaving())); connect(applet, SIGNAL(releaseVisualFocus()), this, SIGNAL(releaseVisualFocus())); connect(applet, SIGNAL(appletDeleted(Plasma::Applet*)), this, SLOT(appletDestroyed(Plasma::Applet*))); connect(applet, SIGNAL(newStatus(Plasma::ItemStatus)), this, SLOT(checkStatus(Plasma::ItemStatus))); connect(applet, SIGNAL(activate()), this, SIGNAL(activate())); if (!delayInit && !currentContainment) { applet->restore(*applet->d->mainConfigGroup()); applet->init(); //FIXME: an on-appear animation would be nice to have again d->appletAppeared(applet); } applet->updateConstraints(Plasma::AllConstraints); if (!delayInit) { applet->flushPendingConstraintsEvents(); } emit appletAdded(applet, pos); if (!currentContainment) { applet->updateConstraints(Plasma::StartupCompletedConstraint); if (!delayInit) { applet->flushPendingConstraintsEvents(); } } if (!delayInit) { applet->d->scheduleModificationNotification(); } } Applet::List Containment::applets() const { return d->applets; } void Containment::setScreen(int newScreen, int newDesktop) { d->setScreen(newScreen, newDesktop); } int Containment::screen() const { return d->screen; } int Containment::lastScreen() const { return d->lastScreen; } int Containment::desktop() const { return d->desktop; } int Containment::lastDesktop() const { return d->lastDesktop; } KPluginInfo::List Containment::listContainments(const QString &category, const QString &parentApp) { return listContainmentsOfType(QString(), category, parentApp); } KPluginInfo::List Containment::listContainmentsOfType(const QString &type, const QString &category, const QString &parentApp) { QString constraint; if (parentApp.isEmpty()) { constraint.append("(not exist [X-KDE-ParentApp] or [X-KDE-ParentApp] == '')"); } else { constraint.append("[X-KDE-ParentApp] == '").append(parentApp).append("'"); } if (!type.isEmpty()) { if (!constraint.isEmpty()) { constraint.append(" and "); } constraint.append("'").append(type).append("' ~in [X-Plasma-ContainmentCategories]"); } if (!category.isEmpty()) { if (!constraint.isEmpty()) { constraint.append(" and "); } constraint.append("[X-KDE-PluginInfo-Category] == '").append(category).append("'"); if (category == "Miscellaneous") { constraint.append(" or (not exist [X-KDE-PluginInfo-Category] or [X-KDE-PluginInfo-Category] == '')"); } } KService::List offers = KServiceTypeTrader::self()->query("Plasma/Containment", constraint); //kDebug() << "constraint was" << constraint << "which got us" << offers.count() << "matches"; return KPluginInfo::fromServices(offers); } KPluginInfo::List Containment::listContainmentsForMimeType(const QString &mimeType) { const QString constraint = QString("'%1' in [X-Plasma-DropMimeTypes]").arg(mimeType); //kDebug() << mimeType << constraint; const KService::List offers = KServiceTypeTrader::self()->query("Plasma/Containment", constraint); return KPluginInfo::fromServices(offers); } QStringList Containment::listContainmentTypes() { KPluginInfo::List containmentInfos = listContainments(); QSet types; foreach (const KPluginInfo &containmentInfo, containmentInfos) { QStringList theseTypes = containmentInfo.service()->property("X-Plasma-ContainmentCategories").toStringList(); foreach (const QString &type, theseTypes) { types.insert(type); } } return types.toList(); } void Containment::keyPressEvent(QKeyEvent *event) { //kDebug() << "keyPressEvent with" << event->key() // << "and hoping and wishing for a" << Qt::Key_Tab; if (event->key() == Qt::Key_Tab) { // && event->modifiers() == 0) { if (!d->applets.isEmpty()) { #ifndef NDEBUG kDebug() << "let's give focus to...." << (QObject*)d->applets.first(); #endif d->applets.first()->setFocus(Qt::TabFocusReason); } } } void Containment::wheelEvent(QWheelEvent *event) { event->ignore(); QString trigger = ContainmentActions::eventToString(event); if (d->prepareContainmentActions(trigger, event->globalPos())) { if (event->delta() > 0) { d->actionPlugins()->value(trigger)->performNext(); } else { d->actionPlugins()->value(trigger)->performPrevious(); } event->accept(); } } void Containment::enableAction(const QString &name, bool enable) { QAction *action = this->action(name); if (action) { action->setEnabled(enable); action->setVisible(enable); } } void Containment::addToolBoxAction(QAction *action) { d->toolBoxActions << action; } void Containment::removeToolBoxAction(QAction *action) { d->toolBoxActions.removeAll(action); } void Containment::addAssociatedWidget(QWidget *widget) { //TODO: move this whole method in the c++ part of the QML implementation Applet::addAssociatedWidget(widget); /*if (d->focusedApplet) { d->focusedApplet->addAssociatedWidget(widget); }*/ foreach (const Applet *applet, d->applets) { if (applet->d->activationAction) { widget->addAction(applet->d->activationAction); } } } void Containment::removeAssociatedWidget(QWidget *widget) { //TODO: move this whole method in the c++ part of the QML implementation Applet::removeAssociatedWidget(widget); /*if (d->focusedApplet) { d->focusedApplet->removeAssociatedWidget(widget); }*/ foreach (const Applet *applet, d->applets) { if (applet->d->activationAction) { widget->removeAction(applet->d->activationAction); } } } void Containment::setDrawWallpaper(bool drawWallpaper) { d->drawWallpaper = drawWallpaper; } bool Containment::drawWallpaper() { return d->drawWallpaper; } void Containment::setWallpaper(const QString &pluginName, const QString &mode) { KConfigGroup cfg = config(); bool newPlugin = pluginName != d->wallpaper; bool newMode = mode != d->wallpaperMode; if (newPlugin || newMode) { d->wallpaper = pluginName; d->wallpaperMode = mode; if (newMode) { cfg.writeEntry("wallpaperpluginmode", mode); } if (newPlugin) { cfg.writeEntry("wallpaperplugin", pluginName); } emit configNeedsSaving(); } } QString Containment::wallpaper() const { return d->wallpaper; } QString Containment::wallpaperMode() const { return d->wallpaperMode; } void Containment::setContainmentActions(const QString &trigger, const QString &pluginName) { KConfigGroup cfg = containmentActionsConfig(); ContainmentActions *plugin = 0; if (d->actionPlugins()->contains(trigger)) { plugin = d->actionPlugins()->value(trigger); if (plugin->pluginName() != pluginName) { d->actionPlugins()->remove(trigger); delete plugin; plugin = 0; } } if (pluginName.isEmpty()) { cfg.deleteEntry(trigger); } else if (plugin) { // it already existed, reset the containment so it wil reload config on next show plugin->setContainment(0); } else { switch (d->containmentActionsSource) { case ContainmentPrivate::Activity: //FIXME case ContainmentPrivate::Local: plugin = PluginLoader::self()->loadContainmentActions(this, pluginName); break; default: plugin = PluginLoader::self()->loadContainmentActions(0, pluginName); } if (plugin) { cfg.writeEntry(trigger, pluginName); d->actionPlugins()->insert(trigger, plugin); } else { //bad plugin... gets removed. is this a feature or a bug? cfg.deleteEntry(trigger); } } emit configNeedsSaving(); } QStringList Containment::containmentActionsTriggers() { return d->actionPlugins()->keys(); } QString Containment::containmentActions(const QString &trigger) { ContainmentActions *c = d->actionPlugins()->value(trigger); return c ? c->pluginName() : QString(); } void Containment::setActivity(const QString &activityId) { if (activityId.isEmpty()) { return; } d->activityId = activityId; KConfigGroup c = config(); c.writeEntry("activityId", activityId); emit configNeedsSaving(); } QString Containment::activity() const { return d->activityId; } void Containment::destroy() { destroy(true); } void Containment::showConfigurationInterface() { Applet::showConfigurationInterface(); } void Containment::destroy(bool confirm) { if (immutability() != Mutable || Applet::d->transient) { return; } if (isContainment() && confirm) { //FIXME: should not be blocking const QString title = i18nc("@title:window %1 is the name of the containment", "Remove %1", name()); KGuiItem remove = KStandardGuiItem::remove(); remove.setText(title); //FIXME: make the view accessible? if (KMessageBox::warningContinueCancel(0/*view()*/, i18nc("%1 is the name of the containment", "Do you really want to remove this %1?", name()), title, remove) != KMessageBox::Continue) { return; } } Applet::destroy(); } KConfigGroup Containment::containmentActionsConfig() { KConfigGroup cfg; switch (d->containmentActionsSource) { case ContainmentPrivate::Local: cfg = config(); cfg = KConfigGroup(&cfg, "ActionPlugins"); break; case ContainmentPrivate::Activity: cfg = KConfigGroup(corona()->config(), "Activities"); cfg = KConfigGroup(&cfg, d->activityId); cfg = KConfigGroup(&cfg, "ActionPlugins"); break; default: cfg = KConfigGroup(corona()->config(), "ActionPlugins"); } return cfg; } } // Plasma namespace #include "moc_containment.cpp"