Make a branch to make krunner Good Enough For Aaron™.
[kdebase/uwolfer.git] / workspace / krunner / interface.cpp
blob24afa67673fd93cb657367f63b544a21a2d6311c
1 /*
2 * Copyright (C) 2006 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 version 2 as
6 * published by the Free Software Foundation
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details
13 * You should have received a copy of the GNU Library General Public
14 * License along with this program; if not, write to the
15 * Free Software Foundation, Inc.,
16 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 #include "interface.h"
21 #include <QAction>
22 #include <QApplication>
23 #include <QDesktopWidget>
24 #include <QLabel>
25 #include <QListWidget>
26 #include <QVBoxLayout>
27 #include <QHBoxLayout>
28 #include <QShortcut>
29 #include <QTimer>
30 #include <QHideEvent>
32 #include <KActionCollection>
33 #include <KHistoryComboBox>
34 #include <KCompletionBox>
35 #include <KDebug>
36 #include <KDialog>
37 #include <KLineEdit>
38 #include <KLocale>
39 #include <KGlobalSettings>
40 #include <KPushButton>
41 #include <KStandardGuiItem>
42 #include <KTitleWidget>
43 #include <KWindowSystem>
45 #include <Solid/Device>
46 #include <Solid/DeviceInterface>
48 // #include <threadweaver/DebuggingAids.h>
49 // #include <ThreadWeaver/Thread>
50 #include <ThreadWeaver/Job>
51 #include <ThreadWeaver/QueuePolicy>
52 #include <ThreadWeaver/Weaver>
53 #include <QMutex>
55 #include <plasma/abstractrunner.h>
57 #include "collapsiblewidget.h"
58 #include "interfaceadaptor.h"
59 #include "krunnersettings.h"
61 using ThreadWeaver::Weaver;
62 using ThreadWeaver::Job;
64 // A little hack of a class to let us easily activate a match
66 class SearchMatch : public QListWidgetItem
68 public:
69 SearchMatch(const Plasma::SearchMatch* action, QListWidget* parent)
70 : QListWidgetItem( parent ),
71 m_default(false),
72 m_action(0)
74 setAction(action);
77 void activate(const Plasma::SearchContext *context)
79 m_action->exec(context);
82 bool actionEnabled()
84 return m_action->isEnabled();
87 bool hasMatchOptions()
89 return m_action->runner()->hasMatchOptions();
92 void setAction(const Plasma::SearchMatch* action)
94 m_action = action;
95 setIcon(m_action->icon());
96 setText(i18n("%1 (%2)",
97 m_action->text(),
98 m_action->runner()->objectName()));
100 // in case our new action is now enabled and the old one wasn't, or
101 // vice versa
102 setDefault(m_default);
105 QString toString()
107 return m_action->data().toString();
110 Plasma::SearchMatch::Type actionType()
112 return m_action->type();
115 /*Plasma::SearchMatch* action()
117 return m_action;
120 void createMatchOptions(QWidget* parent)
122 m_action->runner()->createMatchOptions(parent);
125 void setDefault( bool def ) {
126 if ( m_default == def ) {
127 return;
130 m_default = def;
132 if ( m_default ) {
133 if ( m_action->isEnabled() ) {
134 setText( text().prepend( i18n("Default: ") ) );
136 } else {
137 setText( text().mid( 9 ) );
141 bool operator<(const QListWidgetItem & other) const
143 // Rules:
144 // 0. Default wins. Always.
145 // 1. Exact trumps informational
146 // 2. Informational trumps possible
147 // 3. Higher relevance wins
149 const SearchMatch *otherMatch = dynamic_cast<const SearchMatch*>(&other);
151 if (!otherMatch) {
152 return QListWidgetItem::operator<(other);
155 if (otherMatch->m_default) {
156 return true;
159 Plasma::SearchMatch::Type myType = m_action->type();
160 Plasma::SearchMatch::Type otherType = otherMatch->m_action->type();
162 if (myType != otherType) {
163 return myType < otherType;
166 return m_action->relevance() < otherMatch->m_action->relevance();
169 private:
170 bool m_default;
171 const Plasma::SearchMatch* m_action;
174 // Restricts simultaneous jobs of the same type
175 // Similar to ResourceRestrictionPolicy but check the object type first
176 class RunnerRestrictionPolicy : public ThreadWeaver::QueuePolicy
178 public:
179 ~RunnerRestrictionPolicy();
181 static RunnerRestrictionPolicy& instance();
183 void setCap(int cap)
185 m_cap = cap;
187 int cap() const
189 return m_cap;
192 bool canRun(Job* job);
193 void free(Job* job);
194 void release(Job* job);
195 void destructed(Job* job);
196 private:
197 RunnerRestrictionPolicy();
199 // QHash<QString, int> m_runCounts;
200 int m_count;
201 int m_cap;
202 QMutex m_mutex;
205 RunnerRestrictionPolicy::RunnerRestrictionPolicy()
206 : QueuePolicy(),
207 m_cap(2)
211 RunnerRestrictionPolicy::~RunnerRestrictionPolicy()
215 RunnerRestrictionPolicy& RunnerRestrictionPolicy::instance()
217 static RunnerRestrictionPolicy policy;
218 return policy;
221 bool RunnerRestrictionPolicy::canRun(Job* job)
223 Q_UNUSED(job)
224 QMutexLocker l(&m_mutex);
225 // QString type = job->objectName();
226 if (m_count/*m_runCounts.value(type)*/ > m_cap) {
227 // kDebug() << "Denying job " << type << " because of " << m_count/*m_runCounts[type]*/ << " current jobs" << endl;
228 return false;
229 } else {
230 // m_runCounts[type]++;
231 ++m_count;
232 return true;
236 void RunnerRestrictionPolicy::free(Job* job)
238 Q_UNUSED(job)
239 QMutexLocker l(&m_mutex);
240 // QString type = job->objectName();
241 --m_count;
242 // if (m_runCounts.value(type)) {
243 // m_runCounts[type]--;
244 // }
247 void RunnerRestrictionPolicy::release(Job* job)
249 free(job);
252 void RunnerRestrictionPolicy::destructed(Job* job)
254 Q_UNUSED(job)
257 // Class to run queries in different threads
258 class FindMatchesJob : public Job
260 public:
261 FindMatchesJob(const QString& term, Plasma::AbstractRunner* runner, Plasma::SearchContext* context, QObject* parent = 0);
263 int priority() const;
265 protected:
266 void run();
267 private:
268 QString m_term;
269 Plasma::SearchContext* m_context;
270 Plasma::AbstractRunner* m_runner;
273 FindMatchesJob::FindMatchesJob( const QString& term, Plasma::AbstractRunner* runner, Plasma::SearchContext* context, QObject* parent )
274 : ThreadWeaver::Job(parent),
275 m_term(term),
276 m_context(context),
277 m_runner(runner)
279 // setObjectName( runner->objectName() );
280 if (runner->speed() == Plasma::AbstractRunner::SlowSpeed) {
281 assignQueuePolicy(&RunnerRestrictionPolicy::instance());
285 void FindMatchesJob::run()
287 // kDebug() << "Running match for " << m_runner->objectName() << " in Thread " << thread()->id() << endl;
288 m_runner->performMatch(*m_context);
291 int FindMatchesJob::priority() const
293 return m_runner->priority();
296 Interface::Interface(QWidget* parent)
297 : KRunnerDialog( parent ),
298 m_expander(0),
299 m_optionsWidget(0),
300 m_defaultMatch(0),
301 m_execQueued(false)
303 setWindowTitle( i18n("Run Command") );
304 setWindowIcon(KIcon("system-run"));
306 m_matchTimer.setSingleShot(true);
307 connect(&m_matchTimer, SIGNAL(timeout()), this, SLOT(match()));
309 m_updateTimer.setSingleShot(true);
310 connect(&m_updateTimer, SIGNAL(timeout()), this, SLOT(updateMatches()));
312 const int numProcs = qMax(Solid::Device::listFromType(Solid::DeviceInterface::Processor).count(), 1);
313 const int numThreads = qMin(KRunnerSettings::maxThreads(), 2 + ((numProcs - 1) * 2));
314 //kDebug() << "setting up" << numThreads << "threads for" << numProcs << "processors";
315 Weaver::instance()->setMaximumNumberOfThreads(numThreads);
317 QWidget* w = mainWidget();
318 m_layout = new QVBoxLayout(w);
319 m_layout->setMargin(0);
321 m_header = new KTitleWidget(w);
322 m_header->setBackgroundRole( QPalette::Base );
323 m_layout->addWidget( m_header );
325 m_searchTerm = new KHistoryComboBox(false,w);
326 m_searchTerm->setDuplicatesEnabled(false);
327 KLineEdit *lineEdit = new KLineEdit(m_searchTerm);
328 lineEdit->setCompletionObject(m_context.completionObject());
329 lineEdit->setClearButtonShown(true);
330 m_searchTerm->setLineEdit(lineEdit);
331 m_header->setBuddy(m_searchTerm);
332 m_layout->addWidget(m_searchTerm);
333 connect(m_searchTerm, SIGNAL(editTextChanged(QString)),
334 this, SLOT(queueMatch()));
335 connect(m_searchTerm, SIGNAL(returnPressed()),
336 this, SLOT(exec()));
338 QStringList executions = KRunnerSettings::pastQueries();
339 //Handle updates to the completion object as well
340 m_searchTerm->setHistoryItems(executions, true);
342 //TODO: temporary feedback, change later with the "icon parade" :)
343 m_matchList = new QListWidget(w);
344 //m_matchList->setSortingEnabled(true);
346 connect( m_matchList, SIGNAL(itemActivated(QListWidgetItem*)),
347 SLOT(matchActivated(QListWidgetItem*)) );
348 connect( m_matchList, SIGNAL(itemClicked(QListWidgetItem*)),
349 SLOT(setDefaultItem(QListWidgetItem*)) );
350 m_layout->addWidget(m_matchList);
352 // buttons at the bottom
353 QHBoxLayout* bottomLayout = new QHBoxLayout(w);
354 m_optionsButton = new KPushButton(KStandardGuiItem::configure(), this);
355 m_optionsButton->setText( i18n( "Show Options" ) );
356 m_optionsButton->setEnabled( false );
357 m_optionsButton->setCheckable( true );
358 connect( m_optionsButton, SIGNAL(toggled(bool)), SLOT(showOptions(bool)) );
359 bottomLayout->addWidget( m_optionsButton );
361 KPushButton *activityButton = new KPushButton(w);
362 activityButton->setText(i18n("Show System Activity"));
363 activityButton->setIcon(KIcon("utilities-system-monitor"));
364 connect(activityButton, SIGNAL(clicked()), qApp, SLOT(showTaskManager()));
365 connect(activityButton, SIGNAL(clicked()), this, SLOT(close()));
366 bottomLayout->addWidget(activityButton);
368 bottomLayout->addStretch();
370 QString runButtonWhatsThis = i18n( "Click to execute the selected item above" );
371 m_runButton = new KPushButton(KGuiItem(i18n( "Launch" ), "system-run",
372 QString(), runButtonWhatsThis),
374 m_runButton->setEnabled( false );
375 m_runButton->setDefault(true);
376 connect( m_runButton, SIGNAL( clicked(bool) ), SLOT(exec()) );
377 bottomLayout->addWidget( m_runButton );
379 m_cancelButton = new KPushButton(KStandardGuiItem::cancel(), w);
380 connect( m_cancelButton, SIGNAL(clicked(bool)), SLOT(close()) );
381 bottomLayout->addWidget( m_cancelButton );
383 m_layout->addLayout (bottomLayout );
385 new InterfaceAdaptor( this );
386 QDBusConnection::sessionBus().registerObject( "/Interface", this );
388 new QShortcut( QKeySequence( Qt::Key_Escape ), this, SLOT(close()) );
390 //FIXME: what size should we be?
391 resize(400, 250);
393 //setWidgetPalettes();
394 //connect(KGlobalSettings::self(), SIGNAL(kdisplayPaletteChanged()),
395 // SLOT(setWidgetPalettes()));
397 //TODO: how should we order runners, particularly ones loaded from plugins?
398 QStringList whitelist = KRunnerSettings::runners();
399 m_runners += Plasma::AbstractRunner::load(this, whitelist);
401 connect(&m_context, SIGNAL(matchesChanged()), this, SLOT(queueUpdates()));
403 resetInterface();
405 // ThreadWeaver::setDebugLevel(true, 4);
408 Interface::~Interface()
410 KRunnerSettings::setPastQueries(m_searchTerm->historyItems());
411 m_context.clearMatches();
414 void Interface::clearHistory()
416 m_searchTerm->clearHistory();
417 KRunnerSettings::setPastQueries(m_searchTerm->historyItems());
420 void Interface::display(const QString& term)
422 m_searchTerm->setFocus();
424 if (!term.isEmpty()) {
425 m_searchTerm->setItemText(0, term);
428 if (!isVisible()) {
429 queueMatch();
432 KWindowSystem::setOnDesktop(winId(), KWindowSystem::currentDesktop());
434 int screen = 0;
435 if (QApplication::desktop()->numScreens() > 1) {
436 screen = QApplication::desktop()->screenNumber(QCursor::pos());
439 KDialog::centerOnScreen(this, screen);
440 show();
441 KWindowSystem::forceActiveWindow(winId());
444 void Interface::switchUser()
446 Plasma::AbstractRunner *sessionrunner = 0;
447 foreach (Plasma::AbstractRunner* runner, m_runners) {
448 if (qstrcmp(runner->metaObject()->className(), "SessionRunner") == 0) {
449 sessionrunner = runner;
450 break;
454 if (!sessionrunner) {
455 kDebug() << "Could not find the Sessionrunner; not showing any sessions!";
456 return;
458 //TODO: ugh, magic strings. See sessions/sessionrunner.cpp
459 display("SESSIONS");
460 m_header->setText(i18n("Switch users"));
461 m_header->setPixmap("system-switch-user");
462 m_defaultMatch = 0;
463 m_context.resetSearchTerm("SESSIONS");
464 sessionrunner->match(&m_context);
466 foreach (const Plasma::SearchMatch *action, m_context.matches()) {
467 bool makeDefault = !m_defaultMatch && action->type() != Plasma::SearchMatch::InformationalMatch;
469 SearchMatch *match = new SearchMatch(action, m_matchList);
471 if (makeDefault) {
472 m_defaultMatch = match;
473 m_defaultMatch->setDefault(true);
474 m_runButton->setEnabled(true);
475 m_optionsButton->setEnabled(sessionrunner->hasMatchOptions());
479 if (!m_defaultMatch) {
480 m_matchList->addItem(i18n("No desktop sessions available"));
481 } else {
482 m_matchList->sortItems(Qt::DescendingOrder);
486 void Interface::setWidgetPalettes()
488 // a nice palette to use with the widgets
489 QPalette widgetPalette = palette();
490 QColor headerBgColor = widgetPalette.color( QPalette::Active,
491 QPalette::Base );
492 headerBgColor.setAlpha( 200 );
493 widgetPalette.setColor( QPalette::Base, headerBgColor );
495 m_header->setPalette( widgetPalette );
496 m_searchTerm->setPalette( widgetPalette );
497 m_matchList->setPalette( widgetPalette );
500 void Interface::resetInterface()
502 m_header->setText(i18n("Enter the name of an application, location or search term below."));
503 m_header->setPixmap("system-search");
504 m_defaultMatch = 0;
505 m_context.resetSearchTerm(QString());
506 m_searchTerm->setCurrentItem(QString(), true, 0);
507 m_matchList->clear();
508 m_runButton->setEnabled( false );
509 m_optionsButton->setEnabled( false );
510 showOptions( false );
511 m_matchTimer.stop();
514 void Interface::closeEvent(QCloseEvent* e)
516 resetInterface();
517 e->accept();
520 void Interface::matchActivated(QListWidgetItem* item)
522 SearchMatch* match = dynamic_cast<SearchMatch*>(item);
524 if (!match) {
525 return;
528 if (!match->actionEnabled()) {
529 return;
532 QString searchTerm = m_searchTerm->currentText();
533 m_searchTerm->addToHistory(searchTerm);
535 if (match->actionType() == Plasma::SearchMatch::InformationalMatch) {
536 //kDebug() << "informational match activated" << match->toString();
537 m_searchTerm->setItemText(m_searchTerm->currentIndex(), match->toString());
538 } else {
539 //kDebug() << "match activated! " << match->text();
540 QString term = m_searchTerm->currentText().trimmed();
541 m_context.setSearchTerm(term);
543 match->activate(&m_context);
544 close();
548 void Interface::queueMatch()
550 if (m_matchTimer.isActive()) {
551 return;
553 Weaver::instance()->dequeue();
554 m_matchTimer.start(200);
557 void Interface::queueUpdates()
559 if (m_updateTimer.isActive()) {
560 return;
562 //Wait 100ms between updating matches
563 m_updateTimer.start(100);
566 void Interface::match()
568 // If ThreadWeaver is idle, it is safe to clear previous jobs
569 if ( Weaver::instance()->isIdle() ) {
570 qDeleteAll( m_searchJobs );
571 m_searchJobs.clear();
573 m_defaultMatch = 0;
575 QString term = m_searchTerm->currentText().trimmed();
577 if (term.isEmpty()) {
578 resetInterface();
579 m_execQueued = false;
580 return;
582 m_context.resetSearchTerm(term);
583 m_context.addStringCompletions(m_searchTerm->historyItems());
585 foreach (Plasma::AbstractRunner* runner, m_runners) {
586 Job *job = new FindMatchesJob(term, runner, &m_context, this);
587 Weaver::instance()->enqueue( job );
588 m_searchJobs.append( job );
592 void Interface::updateMatches()
594 m_matchList->clear();
595 m_defaultMatch = 0;
597 foreach (const Plasma::SearchMatch *action, m_context.matches()) {
598 SearchMatch *match = new SearchMatch(action, m_matchList);
600 if (action->isEnabled() && action->relevance() > 0 &&
601 (!m_defaultMatch || *m_defaultMatch < *match) &&
602 (action->type() != Plasma::SearchMatch::InformationalMatch ||
603 !action->data().toString().isEmpty())) {
604 if (m_defaultMatch) {
605 m_defaultMatch->setDefault(false);
608 match->setDefault(true);
609 m_defaultMatch = match;
610 m_optionsButton->setEnabled(action->runner()->hasMatchOptions());
611 m_runButton->setEnabled(true);
615 m_matchList->sortItems(Qt::DescendingOrder);
616 m_execQueued = false;
618 if (!m_defaultMatch) {
619 showOptions(false);
620 m_runButton->setEnabled(false);
621 } else if (m_execQueued) {
622 exec();
626 void Interface::exec()
628 if (!m_execQueued && m_searchTerm->completionBox() && m_searchTerm->completionBox()->isVisible()) {
629 queueMatch();
630 return;
633 kDebug() << "match list has" << m_matchList->count() << "items";
635 QListWidgetItem* currentMatch = m_matchList->currentItem();
636 if (!currentMatch) {
637 if (m_defaultMatch) {
638 currentMatch = m_defaultMatch;
639 } else if (m_matchList->count() < 2) {
640 //TODO: the < 2 is a bit of a hack; we *always* get a search option returned,
641 // so if we only have 1 item and it's not selected, guess what it is? ;)
642 // we might be able to do better here.
643 m_execQueued = true;
644 match();
645 return;
648 matchActivated(currentMatch);
651 void Interface::showOptions(bool show)
653 //TODO: in the case where we are no longer showing options
654 // should we have the runner delete it's options?
655 if (show) {
656 if (!m_defaultMatch || !m_defaultMatch->hasMatchOptions()) {
657 // in this case, there is nothing to show
658 return;
661 if (!m_expander) {
662 //kDebug() << "creating m_expander";
663 m_expander = new CollapsibleWidget( this );
664 m_expander->show();
665 connect( m_expander, SIGNAL( collapseCompleted() ),
666 m_expander, SLOT( close() ) );
667 m_layout->insertWidget( 3, m_expander );
670 //kDebug() << "set inner widget to " << m_defaultMatch->runner()->options();
671 delete m_optionsWidget;
672 m_optionsWidget = new QWidget(this);
673 m_defaultMatch->createMatchOptions(m_optionsWidget);
674 m_optionsButton->setText( i18n( "Hide Options" ) );
675 } else {
676 delete m_optionsWidget;
677 m_optionsWidget = 0;
678 m_optionsButton->setText( i18n( "Show Options" ) );
679 resize( 400, 250 );
682 if ( m_expander ) {
683 //TODO: we need to insert an element into the krunner dialog
684 // that is big enough for the options. this will prevent
685 // other items in the dialog from moving around and look
686 // more "natural"; it should appear as if a "drawer" is
687 // being pulled open, e.g. an expander.
688 m_expander->setInnerWidget(m_optionsWidget);
689 m_expander->setVisible(show);
690 m_expander->setExpanded(show);
693 m_optionsButton->setChecked(show);
696 void Interface::setDefaultItem( QListWidgetItem* item )
698 bool hasOptions = false;
700 if (item) {
701 if (m_defaultMatch) {
702 m_defaultMatch->setDefault(false);
705 m_defaultMatch = dynamic_cast<SearchMatch*>(item);
706 if (!m_defaultMatch) {
707 return;
709 hasOptions = m_defaultMatch && m_defaultMatch->hasMatchOptions();
712 m_optionsButton->setEnabled(hasOptions);
714 if (hasOptions) {
715 if (m_expander && m_expander->isExpanded()) {
716 m_optionsButton->setText(i18n("Hide Options"));
717 } else {
718 m_optionsButton->setText(i18n("Show Options"));
720 } else {
721 m_optionsButton->setText(i18n("Show Options"));
722 m_optionsButton->setChecked(false);
723 if (m_expander) {
724 m_expander->hide();
729 #include "interface.moc"