2
* Copyright 2008 Aaron Seigo <aseigo@kde.org>
4
* This program is free software; you can redistribute it and/or modify
5
* it under the terms of the GNU Library General Public License as
6
* published by the Free Software Foundation; either version 2, or
7
* (at your option) any later version.
9
* This program is distributed in the hope that it will be useful,
10
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
* GNU General Public License for more details
14
* You should have received a copy of the GNU Library General Public
15
* License along with this program; if not, write to the
16
* Free Software Foundation, Inc.,
17
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20
#include "desktopcorona.h"
23
#include <QApplication>
25
#include <QGraphicsLayout>
28
#include <QSignalMapper>
34
#include <KGlobalSettings>
35
#include <KServiceTypeTrader>
36
#include <KStandardDirs>
38
#include <KWindowSystem>
40
#include <Plasma/AbstractToolBox>
41
#include <Plasma/Containment>
42
#include <plasma/containmentactionspluginsconfig.h>
43
#include <Plasma/Context>
44
#include <Plasma/DataEngineManager>
45
#include <Plasma/Package>
47
#include <kephal/screens.h>
49
#include <scripting/layouttemplatepackagestructure.h>
52
#include "kactivitycontroller.h"
53
#include "kactivityinfo.h"
54
#include "panelview.h"
55
#include "plasmaapp.h"
56
#include "plasma-shell-desktop.h"
57
#include "scripting/desktopscriptengine.h"
59
static const QString s_panelTemplatesPath("plasma-layout-templates/panels/*");
61
DesktopCorona::DesktopCorona(QObject *parent)
62
: Plasma::Corona(parent),
65
m_activityController(new KActivityController(this))
70
DesktopCorona::~DesktopCorona()
72
delete m_addPanelsMenu;
75
void DesktopCorona::init()
77
setPreferredToolBoxPlugin(Plasma::Containment::DesktopContainment, "org.kde.desktoptoolbox");
78
setPreferredToolBoxPlugin(Plasma::Containment::CustomContainment, "org.kde.desktoptoolbox");
79
setPreferredToolBoxPlugin(Plasma::Containment::PanelContainment, "org.kde.paneltoolbox");
80
setPreferredToolBoxPlugin(Plasma::Containment::CustomPanelContainment, "org.kde.paneltoolbox");
82
kDebug() << "!!{} STARTUP TIME" << QTime().msecsTo(QTime::currentTime()) << "DesktopCorona init start" << "(line:" << __LINE__ << ")";
83
Kephal::Screens *screens = Kephal::Screens::self();
84
connect(screens, SIGNAL(screenAdded(Kephal::Screen *)), SLOT(screenAdded(Kephal::Screen *)));
85
connect(KWindowSystem::self(), SIGNAL(workAreaChanged()), this, SIGNAL(availableScreenRegionChanged()));
87
Plasma::ContainmentActionsPluginsConfig desktopPlugins;
88
desktopPlugins.addPlugin(Qt::NoModifier, Qt::Vertical, "switchdesktop");
89
desktopPlugins.addPlugin(Qt::NoModifier, Qt::MidButton, "paste");
90
desktopPlugins.addPlugin(Qt::NoModifier, Qt::RightButton, "contextmenu");
91
Plasma::ContainmentActionsPluginsConfig panelPlugins;
92
panelPlugins.addPlugin(Qt::NoModifier, Qt::RightButton, "contextmenu");
94
setContainmentActionsDefaults(Plasma::Containment::DesktopContainment, desktopPlugins);
95
setContainmentActionsDefaults(Plasma::Containment::CustomContainment, desktopPlugins);
96
setContainmentActionsDefaults(Plasma::Containment::PanelContainment, panelPlugins);
97
setContainmentActionsDefaults(Plasma::Containment::CustomPanelContainment, panelPlugins);
99
checkAddPanelAction();
101
//why do these actions belong to plasmaapp?
102
//because it makes the keyboard shortcuts work.
103
KAction *action = new KAction(PlasmaApp::self());
104
action->setText(i18n("Next Activity"));
105
action->setObjectName( QLatin1String("Next Activity" )); // NO I18N
106
action->setGlobalShortcut(KShortcut(Qt::META + Qt::Key_Tab));
107
connect(action, SIGNAL(triggered()), this, SLOT(activateNextActivity()));
109
action = new KAction(PlasmaApp::self());
110
action->setText(i18n("Previous Activity"));
111
action->setObjectName( QLatin1String("Previous Activity" )); // NO I18N
112
action->setGlobalShortcut(KShortcut(Qt::META + Qt::SHIFT + Qt::Key_Tab));
113
connect(action, SIGNAL(triggered()), this, SLOT(activatePreviousActivity()));
115
connect(this, SIGNAL(immutabilityChanged(Plasma::ImmutabilityType)),
116
this, SLOT(updateImmutability(Plasma::ImmutabilityType)));
117
connect(KSycoca::self(), SIGNAL(databaseChanged(QStringList)), this, SLOT(checkAddPanelAction(QStringList)));
119
connect(m_activityController, SIGNAL(currentActivityChanged(QString)), this, SLOT(currentActivityChanged(QString)));
120
connect(m_activityController, SIGNAL(activityAdded(const QString &)), this, SLOT(activityAdded(const QString &)));
121
connect(m_activityController, SIGNAL(activityRemoved(const QString &)), this, SLOT(activityRemoved(const QString &)));
123
mapAnimation(Plasma::Animator::AppearAnimation, Plasma::Animator::ZoomAnimation);
124
mapAnimation(Plasma::Animator::DisappearAnimation, Plasma::Animator::ZoomAnimation);
125
kDebug() << "!!{} STARTUP TIME" << QTime().msecsTo(QTime::currentTime()) << "DesktopCorona init end" << "(line:" << __LINE__ << ")";
128
void DesktopCorona::checkAddPanelAction(const QStringList &sycocaChanges)
130
if (!sycocaChanges.isEmpty() && !sycocaChanges.contains("services")) {
134
delete m_addPanelAction;
135
m_addPanelAction = 0;
137
delete m_addPanelsMenu;
140
KPluginInfo::List panelContainmentPlugins = Plasma::Containment::listContainmentsOfType("panel");
141
const QString constraint = QString("[X-Plasma-Shell] == '%1' and 'panel' ~in [X-Plasma-ContainmentCategories]")
142
.arg(KGlobal::mainComponent().componentName());
143
KService::List templates = KServiceTypeTrader::self()->query("Plasma/LayoutTemplate", constraint);
145
if (panelContainmentPlugins.count() + templates.count() == 1) {
146
m_addPanelAction = new QAction(i18n("Add Panel"), this);
147
m_addPanelAction->setData(Plasma::AbstractToolBox::AddTool);
148
connect(m_addPanelAction, SIGNAL(triggered(bool)), this, SLOT(addPanel()));
149
} else if (!panelContainmentPlugins.isEmpty()) {
150
m_addPanelsMenu = new QMenu;
151
m_addPanelAction = m_addPanelsMenu->menuAction();
152
m_addPanelAction->setText(i18n("Add Panel"));
153
m_addPanelAction->setData(Plasma::AbstractToolBox::AddTool);
154
kDebug() << "populateAddPanelsMenu" << panelContainmentPlugins.count();
155
connect(m_addPanelsMenu, SIGNAL(aboutToShow()), this, SLOT(populateAddPanelsMenu()));
156
connect(m_addPanelsMenu, SIGNAL(triggered(QAction*)), this, SLOT(addPanel(QAction*)));
159
if (m_addPanelAction) {
160
m_addPanelAction->setIcon(KIcon("list-add"));
161
addAction("add panel", m_addPanelAction);
165
void DesktopCorona::updateImmutability(Plasma::ImmutabilityType immutability)
167
if (m_addPanelAction) {
168
m_addPanelAction->setEnabled(immutability == Plasma::Mutable);
172
void DesktopCorona::checkScreens(bool signalWhenExists)
174
// quick sanity check to ensure we have containments for each screen
175
int num = numScreens();
176
for (int i = 0; i < num; ++i) {
177
checkScreen(i, signalWhenExists);
181
void DesktopCorona::checkScreen(int screen, bool signalWhenExists)
183
// signalWhenExists is there to allow PlasmaApp to know when to create views
184
// it does this only on containment addition, but in the case of a screen being
185
// added and the containment already existing for that screen, no signal is emitted
186
// and so PlasmaApp does not know that it needs to create a view for it. to avoid
187
// taking care of that case in PlasmaApp (which would duplicate some of the code below,
188
// DesktopCorona will, when signalWhenExists is true, emit a containmentAdded signal
189
// even if the containment actually existed prior to this method being called.
191
//note: hte signal actually triggers view creation only for panels, atm.
192
//desktop views are created in response to containment's screenChanged signal instead, which is
193
//buggy (sometimes the containment thinks it's already on the screen, so no view is created)
195
Activity *currentActivity = activity(m_activityController->currentActivity());
196
//ensure the desktop(s) have a containment and view
197
if (AppSettings::perVirtualDesktopViews()) {
198
int numDesktops = KWindowSystem::numberOfDesktops();
200
for (int j = 0; j < numDesktops; ++j) {
201
checkDesktop(currentActivity, signalWhenExists, screen, j);
204
checkDesktop(currentActivity, signalWhenExists, screen);
207
//ensure the panels get views too
208
if (signalWhenExists) {
209
foreach (Plasma::Containment * c, containments()) {
210
if (c->screen() != screen) {
214
Plasma::Containment::Type t = c->containmentType();
215
if (t == Plasma::Containment::PanelContainment ||
216
t == Plasma::Containment::CustomPanelContainment) {
217
emit containmentAdded(c);
223
void DesktopCorona::checkDesktop(Activity *activity, bool signalWhenExists, int screen, int desktop)
225
Plasma::Containment *c = activity->containmentForScreen(screen, desktop);
231
c->setScreen(screen, desktop);
232
c->flushPendingConstraintsEvents();
235
if (signalWhenExists) {
236
emit containmentAdded(c);
240
int DesktopCorona::numScreens() const
243
if (KGlobalSettings::isMultiHead()) {
244
// with multihead, we "lie" and say that there is only one screen
249
return Kephal::ScreenUtils::numScreens();
252
QRect DesktopCorona::screenGeometry(int id) const
255
if (KGlobalSettings::isMultiHead()) {
256
// with multihead, we "lie" and say that screen 0 is the default screen, in fact, we pretend
257
// we have only one screen at all
258
Display *dpy = XOpenDisplay(NULL);
260
id = DefaultScreen(dpy);
266
return Kephal::ScreenUtils::screenGeometry(id);
269
QRegion DesktopCorona::availableScreenRegion(int id) const
272
if (KGlobalSettings::isMultiHead()) {
273
// with multihead, we "lie" and say that screen 0 is the default screen, in fact, we pretend
274
// we have only one screen at all
275
Display *dpy = XOpenDisplay(NULL);
277
id = DefaultScreen(dpy);
284
id = Kephal::ScreenUtils::primaryScreenId();
287
QRegion r(screenGeometry(id));
288
foreach (PanelView *view, PlasmaApp::self()->panelViews()) {
289
if (view->screen() == id && view->visibilityMode() == PanelView::NormalPanel) {
290
r = r.subtracted(view->geometry());
297
QRect DesktopCorona::availableScreenRect(int id) const
300
id = Kephal::ScreenUtils::primaryScreenId();
303
QRect r(screenGeometry(id));
305
foreach (PanelView *view, PlasmaApp::self()->panelViews()) {
306
if (view->screen() == id && view->visibilityMode() == PanelView::NormalPanel) {
307
QRect v = view->geometry();
308
switch (view->location()) {
309
case Plasma::TopEdge:
310
if (v.bottom() > r.top()) {
311
r.setTop(v.bottom());
315
case Plasma::BottomEdge:
316
if (v.top() < r.bottom()) {
317
r.setBottom(v.top());
321
case Plasma::LeftEdge:
322
if (v.right() > r.left()) {
323
r.setLeft(v.right());
327
case Plasma::RightEdge:
328
if (v.left() < r.right()) {
329
r.setRight(v.left());
342
int DesktopCorona::screenId(const QPoint &pos) const
345
if (KGlobalSettings::isMultiHead()) {
346
// with multihead, we "lie" and say that there is only one screen
351
return Kephal::ScreenUtils::screenId(pos);
354
void DesktopCorona::processUpdateScripts()
356
evaluateScripts(WorkspaceScripting::ScriptEngine::pendingUpdateScripts());
359
void DesktopCorona::evaluateScripts(const QStringList &scripts)
361
foreach (const QString &script, scripts) {
362
WorkspaceScripting::DesktopScriptEngine scriptEngine(this);
363
connect(&scriptEngine, SIGNAL(printError(QString)), this, SLOT(printScriptError(QString)));
364
connect(&scriptEngine, SIGNAL(print(QString)), this, SLOT(printScriptMessage(QString)));
365
connect(&scriptEngine, SIGNAL(createPendingPanelViews()), PlasmaApp::self(), SLOT(createWaitingPanels()));
368
if (file.open(QIODevice::ReadOnly | QIODevice::Text) ) {
369
QString code = file.readAll();
370
kDebug() << "evaluating startup script:" << script;
371
scriptEngine.evaluateScript(code);
376
void DesktopCorona::printScriptError(const QString &error)
378
kWarning() << "Startup script errror:" << error;
381
void DesktopCorona::printScriptMessage(const QString &error)
383
kDebug() << "Startup script: " << error;
386
void DesktopCorona::loadDefaultLayout()
388
evaluateScripts(WorkspaceScripting::ScriptEngine::defaultLayoutScripts());
389
if (containments().isEmpty()) {
390
QString defaultConfig = KStandardDirs::locate("appdata", "plasma-default-layoutrc");
391
if (!defaultConfig.isEmpty()) {
392
kDebug() << "attempting to load the default layout from:" << defaultConfig;
393
loadLayout(defaultConfig);
394
QTimer::singleShot(1000, this, SLOT(saveDefaultSetup()));
398
QTimer::singleShot(1000, this, SLOT(saveDefaultSetup()));
401
void DesktopCorona::saveDefaultSetup()
403
// a "null" KConfigGroup is used to force a save into the config file
404
KConfigGroup invalidConfig;
406
foreach (Plasma::Containment *containment, containments()) {
407
containment->save(invalidConfig);
408
foreach (Plasma::Applet* applet, containment->applets()) {
409
applet->save(invalidConfig);
416
Plasma::Applet *DesktopCorona::loadDefaultApplet(const QString &pluginName, Plasma::Containment *c)
419
Plasma::Applet *applet = Plasma::Applet::load(pluginName, 0, args);
422
c->addApplet(applet);
428
void DesktopCorona::screenAdded(Kephal::Screen *s)
431
checkScreen(s->id(), true);
434
void DesktopCorona::populateAddPanelsMenu()
436
m_addPanelsMenu->clear();
437
const KPluginInfo emptyInfo;
439
KPluginInfo::List panelContainmentPlugins = Plasma::Containment::listContainmentsOfType("panel");
440
QMap<QString, QPair<KPluginInfo, KService::Ptr> > sorted;
441
foreach (const KPluginInfo &plugin, panelContainmentPlugins) {
442
//FIXME: a better way to filter out what is not wanted?
443
if (!plugin.property("X-Plasma-ContainmentCategories").value<QStringList>().contains("netbook")) {
444
sorted.insert(plugin.name(), qMakePair(plugin, KService::Ptr(0)));
448
const QString constraint = QString("[X-Plasma-Shell] == '%1' and 'panel' in [X-Plasma-ContainmentCategories]")
449
.arg(KGlobal::mainComponent().componentName());
450
KService::List templates = KServiceTypeTrader::self()->query("Plasma/LayoutTemplate", constraint);
451
foreach (const KService::Ptr &service, templates) {
452
sorted.insert(service->name(), qMakePair(emptyInfo, service));
455
QMapIterator<QString, QPair<KPluginInfo, KService::Ptr> > it(sorted);
456
Plasma::PackageStructure::Ptr templateStructure(new WorkspaceScripting::LayoutTemplatePackageStructure);
457
while (it.hasNext()) {
459
QPair<KPluginInfo, KService::Ptr> pair = it.value();
460
if (pair.first.isValid()) {
461
KPluginInfo plugin = pair.first;
462
QAction *action = m_addPanelsMenu->addAction(plugin.name());
463
if (!plugin.icon().isEmpty()) {
464
action->setIcon(KIcon(plugin.icon()));
467
action->setData(plugin.pluginName());
469
//FIXME: proper names
470
KPluginInfo info(pair.second);
471
const QString path = KStandardDirs::locate("data", templateStructure->defaultPackageRoot() + '/' + info.pluginName() + '/');
472
if (!path.isEmpty()) {
473
Plasma::Package package(path, templateStructure);
474
const QString scriptFile = package.filePath("mainscript");
475
if (!scriptFile.isEmpty()) {
476
QAction *action = m_addPanelsMenu->addAction(info.name());
477
action->setData(QString::fromLatin1("plasma-desktop-template:%1").arg(scriptFile));
484
void DesktopCorona::addPanel()
486
KPluginInfo::List panelPlugins = Plasma::Containment::listContainmentsOfType("panel");
488
if (!panelPlugins.isEmpty()) {
489
addPanel(panelPlugins.first().pluginName());
493
void DesktopCorona::addPanel(QAction *action)
495
const QString plugin = action->data().toString();
496
if (plugin.startsWith("plasma-desktop-template:")) {
497
evaluateScripts(QStringList() << plugin.right(plugin.length() - qstrlen("plasma-desktop-template:")));
498
} else if (!plugin.isEmpty()) {
503
void DesktopCorona::addPanel(const QString &plugin)
505
Plasma::Containment *panel = addContainment(plugin);
510
panel->showConfigurationInterface();
512
//Fall back to the cursor position since we don't know what is the originating containment
513
const int screen = Kephal::ScreenUtils::screenId(QCursor::pos());
515
panel->setScreen(screen);
517
QList<Plasma::Location> freeEdges = DesktopCorona::freeEdges(screen);
518
//kDebug() << freeEdges;
519
Plasma::Location destination;
520
if (freeEdges.contains(Plasma::TopEdge)) {
521
destination = Plasma::TopEdge;
522
} else if (freeEdges.contains(Plasma::BottomEdge)) {
523
destination = Plasma::BottomEdge;
524
} else if (freeEdges.contains(Plasma::LeftEdge)) {
525
destination = Plasma::LeftEdge;
526
} else if (freeEdges.contains(Plasma::RightEdge)) {
527
destination = Plasma::RightEdge;
528
} else destination = Plasma::TopEdge;
530
panel->setLocation(destination);
532
const QRect screenGeom = screenGeometry(screen);
533
const QRegion availGeom = availableScreenRegion(screen);
539
//FIXME: this should really step through the rects on the relevant screen edge to find
541
if (destination == Plasma::LeftEdge) {
542
QRect r = availGeom.intersected(QRect(screenGeom.x(), screenGeom.y(), w, screenGeom.height())).boundingRect();
546
} else if (destination == Plasma::RightEdge) {
547
QRect r = availGeom.intersected(QRect(screenGeom.right() - w, screenGeom.y(), w, screenGeom.height())).boundingRect();
551
} else if (destination == Plasma::TopEdge) {
552
QRect r = availGeom.intersected(QRect(screenGeom.x(), screenGeom.y(), screenGeom.width(), h)).boundingRect();
556
} else if (destination == Plasma::BottomEdge) {
557
QRect r = availGeom.intersected(QRect(screenGeom.x(), screenGeom.bottom() - h, screenGeom.width(), h)).boundingRect();
563
panel->setMinimumSize(minW, minH);
564
panel->setMaximumSize(w, h);
568
void DesktopCorona::checkActivities()
570
kDebug() << "containments to start with" << containments().count();
572
KActivityConsumer::ServiceStatus status = m_activityController->serviceStatus();
573
//kDebug() << "$%$%$#%$%$%Status:" << status;
574
if (status == KActivityConsumer::NotRunning) {
575
//panic and give up - better than causing a mess
576
kDebug() << "No ActivityManager? Help, I've fallen and I can't get up!";
580
QStringList existingActivities = m_activityController->listActivities();
581
foreach (const QString &id, existingActivities) {
585
QStringList newActivities;
586
QString newCurrentActivity;
588
//-containments with an invalid id are deleted.
589
//-containments that claim they were on a screen are kept together, and are preferred if we
590
//need to initialize the current activity.
591
//-containments that don't know where they were or who they were with just get made into their
593
foreach (Plasma::Containment *cont, containments()) {
594
if ((cont->containmentType() == Plasma::Containment::DesktopContainment ||
595
cont->containmentType() == Plasma::Containment::CustomContainment) &&
596
!offscreenWidgets().contains(cont)) {
597
Plasma::Context *context = cont->context();
598
QString oldId = context->currentActivityId();
599
if (!oldId.isEmpty()) {
600
if (existingActivities.contains(oldId)) {
601
continue; //it's already claimed
603
kDebug() << "invalid id" << oldId;
605
cont->destroy(false);
608
if (cont->screen() > -1) {
609
//it belongs on the current activity
610
if (!newCurrentActivity.isEmpty()) {
611
context->setCurrentActivityId(newCurrentActivity);
615
//discourage blank names
616
if (context->currentActivity().isEmpty()) {
617
context->setCurrentActivity(i18nc("Default name for a new activity", "New Activity"));
619
//create a new activity for the containment
620
QString id = m_activityController->addActivity(context->currentActivity());
621
context->setCurrentActivityId(id);
623
if (cont->screen() > -1) {
624
newCurrentActivity = id;
626
kDebug() << "migrated" << context->currentActivityId() << context->currentActivity();
630
kDebug() << "migrated?" << !newActivities.isEmpty() << containments().count();
631
if (!newActivities.isEmpty()) {
636
foreach (const QString &id, newActivities) {
640
//ensure the current activity is initialized
641
if (m_activityController->currentActivity().isEmpty()) {
642
kDebug() << "guessing at current activity";
643
if (existingActivities.isEmpty()) {
644
if (newCurrentActivity.isEmpty()) {
645
if (newActivities.isEmpty()) {
646
kDebug() << "no activities!?! Bad activitymanager, no cookie!";
647
QString id = m_activityController->addActivity(i18nc("Default name for a new activity", "New Activity"));
649
m_activityController->setCurrentActivity(id);
650
kDebug() << "created emergency activity" << id;
652
m_activityController->setCurrentActivity(newActivities.first());
655
m_activityController->setCurrentActivity(newCurrentActivity);
658
m_activityController->setCurrentActivity(existingActivities.first());
663
void DesktopCorona::currentActivityChanged(const QString &newActivity)
665
kDebug() << newActivity;
666
Activity *act =activity(newActivity);
672
Activity* DesktopCorona::activity(const QString &id)
674
if (!m_activities.contains(id)) {
675
//the add signal comes late sometimes
678
return m_activities.value(id);
681
void DesktopCorona::activityAdded(const QString &id)
683
//TODO more sanity checks
684
if (m_activities.contains(id)) {
685
kDebug() << "you're late." << id;
689
Activity *a = new Activity(id, this);
690
if (a->isCurrent()) {
693
m_activities.insert(id, a);
696
void DesktopCorona::activityRemoved(const QString &id)
698
Activity *a = m_activities.take(id);
702
void DesktopCorona::activateNextActivity()
704
QStringList list = m_activityController->listActivities(KActivityInfo::Running);
705
if (list.isEmpty()) {
709
//FIXME: if the current activity is in transition the "next" will be the first
710
int start = list.indexOf(m_activityController->currentActivity());
711
int i = (start + 1) % list.size();
713
m_activityController->setCurrentActivity(list.at(i));
716
void DesktopCorona::activatePreviousActivity()
718
QStringList list = m_activityController->listActivities(KActivityInfo::Running);
719
if (list.isEmpty()) {
723
//FIXME: if the current activity is in transition the "previous" will be the last
724
int start = list.indexOf(m_activityController->currentActivity());
725
//fun fact: in c++, (-1 % foo) == -1
731
m_activityController->setCurrentActivity(list.at(i));
735
#include "desktopcorona.moc"