add more spacing
[personal-kdebase.git] / workspace / krunner / interfaces / quicksand / qs_matchview.cpp
blob3b1383c7bf4ba51a817dcb9fc5d5a2b72b6eff8f
1 /*
2 * Copyright (C) 2007-2009 Ryan P. Bitanga <ryan.bitanga@gmail.com>
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, 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 General Public License
15 * 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 <cmath>
22 #include <QBoxLayout>
23 #include <QFocusEvent>
24 #include <QGraphicsItemAnimation>
25 #include <QGraphicsPixmapItem>
26 #include <QGraphicsView>
27 #include <QGraphicsWidget>
28 #include <QKeyEvent>
29 #include <QResizeEvent>
30 #include <QStackedWidget>
31 #include <QTimeLine>
32 #include <QTimer>
33 #include <QToolButton>
35 #include <KDebug>
36 #include <KIcon>
37 #include <KLineEdit>
38 #include <KLocale>
40 #include <Plasma/Theme>
42 #include "qs_completionbox.h"
43 #include "qs_statusbar.h"
44 #include "qs_matchitem.h"
45 #include "qs_matchview.h"
47 //Widget dimensions
48 const int WIDTH = 390;
49 const int HEIGHT = 80; //10px overlap with text
50 const int ICON_AREA_HEIGHT = 70; //3 px margins
51 const int LARGE_ICON_PADDING = 3;
52 const int SMALL_ICON_PADDING = 19; //(70 - ITEM_SIZE)/2
53 //FIXME: Magic numbers galore...
55 namespace QuickSand{
57 class QsMatchView::Private
59 public:
60 QLabel *m_titleLabel;
61 QLabel *m_itemCountLabel;
62 QToolButton *m_arrowButton;
63 QStackedWidget *m_stack;
64 QGraphicsScene *m_scene;
65 QGraphicsView *m_view;
66 KLineEdit *m_lineEdit;
67 QsCompletionBox *m_compBox;
68 QList<MatchItem*> m_items;
69 QString m_searchTerm;
70 QString m_itemCountSuffix;
71 QGraphicsRectItem *m_descRect;
72 QGraphicsTextItem *m_descText;
73 int m_currentItem;
74 bool m_hasFocus;
75 bool m_itemsRemoved;
76 bool m_listVisible;
77 bool m_selectionMade;
80 QsMatchView::QsMatchView(QWidget *parent)
81 : QWidget(parent),
82 d(new Private())
84 setFocusPolicy(Qt::StrongFocus);
85 //Track focus because focus changes between internal widgets trigger focus events
86 d->m_hasFocus = false;
87 d->m_itemsRemoved = false;
88 d->m_listVisible = true;
89 d->m_selectionMade = false; //Prevent completion box from popping up once a user chooses a match
90 //FIXME: don't hardcode black
91 setStyleSheet("QListWidget {color: black} QLineEdit {color: black}");
93 d->m_descRect = 0;
94 d->m_descText = 0;
96 d->m_view = new QGraphicsView(this);
97 d->m_view->setRenderHint(QPainter::Antialiasing);
98 d->m_view->viewport()->setAutoFillBackground(false);
99 d->m_view->setInteractive(true);
100 d->m_view->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
101 d->m_view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
102 d->m_view->setOptimizationFlag(QGraphicsView::DontSavePainterState);
103 d->m_view->setAlignment(Qt::AlignLeft | Qt::AlignTop);
104 d->m_view->setFocusPolicy(Qt::NoFocus);
106 d->m_scene = new QGraphicsScene(-WIDTH/2, 0, WIDTH, HEIGHT, this);
107 d->m_view->setScene(d->m_scene);
109 d->m_currentItem = 0;
111 d->m_lineEdit = new KLineEdit(this);
112 d->m_compBox = new QuickSand::QsCompletionBox(this);
113 d->m_compBox->setTabHandling(false);
115 d->m_stack = new QStackedWidget(this);
116 d->m_stack->addWidget(d->m_view);
117 d->m_stack->addWidget(d->m_lineEdit);
118 d->m_stack->setCurrentIndex(0);
120 d->m_titleLabel = new QLabel(this);
121 d->m_itemCountLabel = new QLabel(this);
122 d->m_itemCountSuffix = i18n("items");
124 d->m_arrowButton = new QToolButton(this);
125 d->m_arrowButton->setFocusPolicy(Qt::NoFocus);
126 d->m_arrowButton->setArrowType(Qt::RightArrow);
127 Plasma::Theme *theme = Plasma::Theme::defaultTheme();
128 QString buttonStyleSheet = QString("QToolButton { border-radius: 4px; border: 0px; background-color: transparent }");
129 buttonStyleSheet += QString("QToolButton:hover { border: 1px solid %1; }")
130 .arg(theme->color(Plasma::Theme::HighlightColor).name());
131 d->m_arrowButton->setStyleSheet(buttonStyleSheet);
133 QHBoxLayout *topLayout = new QHBoxLayout();
134 topLayout->addWidget(d->m_titleLabel);
135 topLayout->addStretch();
136 topLayout->addWidget(d->m_itemCountLabel);
137 topLayout->addWidget(d->m_arrowButton);
139 QVBoxLayout *layout = new QVBoxLayout(this);
140 layout->addLayout(topLayout);
141 layout->addWidget(d->m_stack);
143 connect(d->m_compBox, SIGNAL(currentRowChanged(int)), this, SLOT(scrollToItem(int)));
144 connect(d->m_compBox, SIGNAL(activated(const QString&)), this, SLOT(showSelected()));
145 connect(d->m_lineEdit, SIGNAL(textChanged(const QString&)), this, SIGNAL(textChanged(const QString&)));
146 connect(d->m_arrowButton, SIGNAL(pressed()), this, SLOT(toggleView()));
148 reset();
151 QsMatchView::~QsMatchView()
153 qDeleteAll(d->m_items);
154 d->m_items.clear();
155 delete d;
158 void QsMatchView::reset()
160 clear(true);
162 d->m_stack->setCurrentIndex(0);
163 d->m_arrowButton->hide();
164 d->m_listVisible = true;
165 d->m_selectionMade = false;
166 d->m_hasFocus = false;
167 d->m_searchTerm = QString();
168 d->m_compBox->clear();
169 d->m_compBox->hide();
170 d->m_itemCountLabel->setText(QString());
172 QGraphicsPixmapItem *p = new QGraphicsPixmapItem(KIcon("edit-find").pixmap(MatchItem::ITEM_SIZE));
173 p->setPos(-MatchItem::ITEM_SIZE/2, LARGE_ICON_PADDING);
174 d->m_scene->addItem(p);
175 //Replace with a suitable message
176 setDescriptionText(i18n("Type to search."));
179 void QsMatchView::setItems(const QList<MatchItem*> &items, bool popup, bool append)
181 int spacing = MatchItem::ITEM_SIZE/2;
183 int pos = spacing;
185 if (!append) {
186 clear(true);
187 d->m_compBox->clear();
189 d->m_currentItem = -1;
190 d->m_items = items;
191 } else {
192 // FIXME: This completely disregards item ranking
193 // Maybe should we just sort then scroll to previously selected item
194 if (!d->m_items.isEmpty()) {
195 pos += d->m_items.last()->pos().x();
197 d->m_items << items;
200 foreach(MatchItem *item, items) {
201 if (item) {
202 item->setPos(pos, SMALL_ICON_PADDING);
203 item->scale(0.5, 0.5);
204 pos += spacing;
205 d->m_scene->addItem(item);
206 QString description;
207 if (item->description().isEmpty()) {
208 description = item->name();
209 } else {
210 description = QString("%1 (%2)").arg(item->name()).arg(item->description());
212 QListWidgetItem *wi = new QListWidgetItem(item->icon(), description, d->m_compBox);
213 d->m_compBox->addItem(wi);
216 d->m_itemsRemoved = false;
217 setItemCount(d->m_items.size());
219 if (d->m_selectionMade) {
220 //kDebug() << "A user selection was already made" << endl;
221 return;
224 scrollToItem(0);
226 //Ensure popup is shown if desired
227 if (popup) {
228 if (items.size()) {
229 d->m_compBox->popup();
230 d->m_compBox->setCurrentRow(0);
231 } else {
232 d->m_compBox->hide();
234 d->m_arrowButton->setArrowType(Qt::DownArrow);
235 } else {
236 d->m_currentItem = 0;
237 showSelected();
241 void QsMatchView::setTitle(const QString &title)
243 d->m_titleLabel->setText(title);
246 void QsMatchView::setItemCount(int count)
248 //TODO: place a context to aid translation
249 d->m_itemCountLabel->setText(i18n("%1 %2", count, d->m_itemCountSuffix));
250 if (count) {
251 d->m_arrowButton->show();
255 void QsMatchView::setItemCountSuffix(const QString &suffix)
257 d->m_itemCountSuffix = suffix;
260 void QsMatchView::setDescriptionText(const QString &text)
262 QColor color(Qt::white);
263 setDescriptionText(text, color);
266 void QsMatchView::setDescriptionText(const QString &text, const QColor &color)
268 if (d->m_descRect) {
269 d->m_scene->removeItem(d->m_descRect);
270 delete d->m_descRect;
271 d->m_descRect = 0;
274 QColor bg(color);
275 bg.setAlphaF(0.6);
276 QBrush b(bg);
278 QPen p(QColor(0, 0, 0, 0));
279 d->m_descRect = new QGraphicsRectItem(-WIDTH/2, 60, WIDTH, 20);
280 d->m_descRect->setBrush(b);
281 d->m_descRect->setPen(p);
283 QFontMetrics fm(font());
285 // Show ellipsis in the middle to distinguish between strings with identical
286 // beginnings e.g. paths
287 d->m_descText = new QGraphicsTextItem(fm.elidedText(text, Qt::ElideMiddle, WIDTH), d->m_descRect);
288 //Center text
289 d->m_descText->setPos(-(d->m_descText->boundingRect().width()/2), 60);
291 d->m_scene->addItem(d->m_descRect);
294 void QsMatchView::clearItems()
296 if (!d->m_itemsRemoved) {
297 foreach (MatchItem *item, d->m_items) {
298 d->m_scene->removeItem(item);
300 d->m_itemsRemoved = true;
304 void QsMatchView::clear(bool deleteItems)
306 if (!deleteItems) {
307 clearItems();
308 } else {
309 d->m_items.clear();
310 d->m_itemsRemoved = false;
312 d->m_scene->clear();
313 d->m_descRect = 0;
316 void QsMatchView::toggleView()
318 //It might be better not to rely on m_arrowButton...
319 //should make things more readable
320 if (d->m_arrowButton->arrowType() == Qt::RightArrow) {
321 showList();
322 } else {
323 showSelected();
327 //TODO: Fix animation
328 void QsMatchView::showLoading()
330 clear(true);
332 d->m_descText = new QGraphicsTextItem(i18n("Loading..."), d->m_descRect);
333 d->m_descText->setDefaultTextColor(QColor(Qt::white));
334 QFontMetrics fm(d->m_descText->font());
336 //Center text
337 d->m_descText->setPos(-(d->m_descText->boundingRect().width()/2), (HEIGHT - fm.height())/2);
338 d->m_scene->addItem(d->m_descText);
341 void QsMatchView::showList()
343 if (d->m_items.size()) {
344 clear();
346 foreach (MatchItem *item, d->m_items) {
347 d->m_scene->addItem(item);
350 d->m_itemsRemoved = false;
351 d->m_arrowButton->setArrowType(Qt::DownArrow);
353 //Restore highlighted icon
354 focusItem(d->m_currentItem);
355 //Popup the completion box - should make this configurable
356 showPopup();
358 d->m_listVisible = true;
361 void QsMatchView::showSelected()
363 if (!d->m_items.size()) {
364 reset();
365 return;
368 MatchItem *it = d->m_items[d->m_currentItem];
369 if (!it) {
370 return;
373 d->m_listVisible = false;
374 d->m_arrowButton->setArrowType(Qt::RightArrow);
376 clear();
378 d->m_stack->setCurrentIndex(0);
380 QGraphicsPixmapItem *pixmap = new QGraphicsPixmapItem(it->icon().pixmap(64));
381 pixmap->setPos(-WIDTH/2 + 5, LARGE_ICON_PADDING);
383 Plasma::Theme *theme = Plasma::Theme::defaultTheme();
384 QColor c = theme->color(Plasma::Theme::TextColor);
386 QGraphicsTextItem *name = new QGraphicsTextItem();
387 //TODO: Modify QFont instead of using setHtml?
388 name->setHtml(QString("<b>%1</b>").arg(it->name()));
389 name->setDefaultTextColor(c);
390 QFontMetrics fm(name->font());
392 int tm = ICON_AREA_HEIGHT/2 - fm.height();
393 name->setPos(-115, tm);
395 QGraphicsTextItem *desc = new QGraphicsTextItem(it->description());
396 desc->setDefaultTextColor(c);
397 desc->setPos(-115, ICON_AREA_HEIGHT/2);
399 d->m_scene->addItem(name);
400 d->m_scene->addItem(desc);
401 d->m_scene->addItem(pixmap);
403 emit selectionChanged(it);
405 d->m_compBox->hide();
408 void QsMatchView::focusItem(int index)
410 if (!d->m_items.size()) {
411 if (d->m_searchTerm.isEmpty()) {
412 reset();
413 } else {
414 setDescriptionText(i18n("No results found."));
416 emit selectionChanged(0);
417 return;
419 if (index > -1 && index < d->m_items.size()) {
420 MatchItem *it = d->m_items[index];
421 d->m_scene->setFocusItem(it);
422 QString description;
423 if (it->description().isEmpty()) {
424 description = it->name();
425 } else {
426 description = QString("%1 (%2)").arg(it->name()).arg(it->description());
428 setDescriptionText(description, it->backgroundColor());
429 emit selectionChanged(it);
433 void QsMatchView::selectItem(int index)
435 Q_UNUSED(index)
436 showSelected();
439 void QsMatchView::scrollLeft()
441 if (d->m_currentItem > 0){
442 --d->m_currentItem;
443 } else {
444 d->m_currentItem = d->m_items.size() - 1;
447 QTimeLine *t = new QTimeLine(150);
448 foreach (MatchItem *item, d->m_items) {
449 QGraphicsItemAnimation *anim = item->anim(true);
450 int spacing = MatchItem::ITEM_SIZE/2;
451 int y = SMALL_ICON_PADDING;
452 int x = -spacing;
453 int index = d->m_items.indexOf(item);
454 if (index == d->m_currentItem) {
455 anim->setScaleAt(1, 1, 1);
456 y = LARGE_ICON_PADDING;
457 } else {
458 if ((!index && d->m_currentItem == d->m_items.size() - 1)
459 || index == d->m_currentItem + 1) {
460 x = item->pos().x() + spacing*2;
461 } else {
462 x = item->pos().x() + spacing;
464 anim->setScaleAt(0, 0.5, 0.5);
465 anim->setScaleAt(1, 0.5, 0.5);
467 anim->setPosAt(1.0, QPointF(x, y));
468 anim->setTimeLine(t);
470 t->start();
471 focusItem(d->m_currentItem);
474 void QsMatchView::scrollRight()
476 if (d->m_currentItem < d->m_items.size() - 1) {
477 ++d->m_currentItem;
478 } else {
479 d->m_currentItem = 0;
482 QTimeLine *t = new QTimeLine(150);
483 foreach (MatchItem *item, d->m_items) {
484 QGraphicsItemAnimation *anim = item->anim(true);
485 int spacing = MatchItem::ITEM_SIZE/2;
486 int y = SMALL_ICON_PADDING;
487 int x = -spacing;
488 if (d->m_items.indexOf(item) == d->m_currentItem) {
489 anim->setScaleAt(1, 1, 1);
490 y = LARGE_ICON_PADDING;
491 } else {
492 anim->setScaleAt(0, 0.5, 0.5);
493 anim->setScaleAt(1, 0.5, 0.5);
494 x = item->pos().x() - spacing;
496 anim->setPosAt(1.0, QPointF(x, y));
497 anim->setTimeLine(t);
499 t->start();
500 focusItem(d->m_currentItem);
503 void QsMatchView::scrollToItem(int index)
505 if (index < 0 || d->m_items.size() == 0) {
506 return;
509 qreal shift = d->m_items[index]->pos().x();
511 QTimeLine *t = new QTimeLine(150);
512 foreach (MatchItem *item, d->m_items) {
513 QGraphicsItemAnimation *anim = item->anim(true);
514 qreal y = SMALL_ICON_PADDING;
515 qreal x = -MatchItem::ITEM_SIZE/2;
516 int ix = d->m_items.indexOf(item);
517 if (ix == index) {
518 anim->setScaleAt(1, 1, 1);
519 y = LARGE_ICON_PADDING;
520 } else {
521 x = item->pos().x() - shift;
522 if ((shift > 0 && ix < index && ix > d->m_currentItem)
523 || (shift < 0 && !(ix <= d->m_currentItem && ix > index))) {
524 x -= MatchItem::ITEM_SIZE/2;
526 anim->setScaleAt(0, 0.5, 0.5);
527 anim->setScaleAt(1, 0.5, 0.5);
529 anim->setPosAt(1.0, QPointF(x, y));
530 anim->setTimeLine(t);
532 t->start();
533 d->m_currentItem = index;
534 focusItem(index);
537 void QsMatchView::showPopup()
539 if (d->m_hasFocus && d->m_items.size()) {
540 //Prevent triggering of scroll to item
541 disconnect(d->m_compBox, SIGNAL(currentRowChanged(int)), this, SLOT(scrollToItem(int)));
542 d->m_compBox->popup();
543 QListWidgetItem *item = d->m_compBox->item(d->m_currentItem);
544 if (item) {
545 d->m_compBox->scrollToItem(item, QAbstractItemView::PositionAtTop);
546 d->m_compBox->setCurrentItem(item, QItemSelectionModel::SelectCurrent);
548 connect(d->m_compBox, SIGNAL(currentRowChanged(int)), this, SLOT(scrollToItem(int)));
552 void QsMatchView::resizeEvent(QResizeEvent *e)
554 QWidget::resizeEvent(e);
555 QTimer::singleShot(150, this, SLOT(showPopup()));
558 void QsMatchView::focusInEvent(QFocusEvent *event)
560 Q_UNUSED(event)
561 if (!d->m_hasFocus) {
562 d->m_hasFocus = true;
563 showList();
567 void QsMatchView::focusOutEvent(QFocusEvent *event)
569 Q_UNUSED(event)
570 if (hasFocus()) {
571 return;
573 d->m_hasFocus = false;
574 showSelected();
577 //TODO: Make it possible to disable text mode
578 void QsMatchView::keyPressEvent(QKeyEvent *e)
580 //Do not handle non-alphanumeric events
581 if (e->modifiers() & ~Qt::ShiftModifier) {
582 QWidget::keyPressEvent(e);
583 return;
586 switch (e->key()) {
587 case Qt::Key_Period:
588 //Switch to line edit
589 d->m_stack->setCurrentIndex(1);
590 d->m_lineEdit->setFocus();
591 break;
592 case Qt::Key_Backspace:
593 //d->m_stack->setCurrentIndex(0);
594 d->m_searchTerm.chop(1);
595 setTitle(d->m_searchTerm);
596 d->m_lineEdit->setText(d->m_searchTerm);
597 return;
598 case Qt::Key_Left:
599 if (!d->m_listVisible) {
600 showList();
602 scrollLeft();
603 return;
604 case Qt::Key_Right:
605 if (!d->m_listVisible) {
606 showList();
608 scrollRight();
609 return;
610 case Qt::Key_Enter:
611 case Qt::Key_Return:
612 //Do not activate item if popup is open
613 if (d->m_compBox->isVisible()) {
614 d->m_compBox->hide();
615 } else if (d->m_items.size() && d->m_currentItem > -1
616 && d->m_currentItem < d->m_items.size()) {
617 emit itemActivated(d->m_items[d->m_currentItem]);
619 d->m_selectionMade = true;
620 showSelected();
621 return;
622 default:
623 break;
626 //Don't add control characters to the search term
627 foreach (QChar c, e->text()) {
628 if (c.isPrint()) {
629 if (d->m_stack->currentIndex() == 1) {
630 d->m_searchTerm = d->m_lineEdit->text() + c;
631 } else {
632 d->m_searchTerm += c;
634 d->m_selectionMade = false;
637 d->m_lineEdit->setText(d->m_searchTerm);
638 QWidget::keyPressEvent(e);
641 } // namespace QuickSand
643 #include "qs_matchview.moc"