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 .
23 #include <QFocusEvent>
24 #include <QGraphicsItemAnimation>
25 #include <QGraphicsPixmapItem>
26 #include <QGraphicsView>
27 #include <QGraphicsWidget>
29 #include <QResizeEvent>
30 #include <QStackedWidget>
33 #include <QToolButton>
40 #include <Plasma/Theme>
42 #include "qs_completionbox.h"
43 #include "qs_statusbar.h"
44 #include "qs_matchitem.h"
45 #include "qs_matchview.h"
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...
57 class QsMatchView::Private
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
;
70 QString m_itemCountSuffix
;
71 QGraphicsRectItem
*m_descRect
;
72 QGraphicsTextItem
*m_descText
;
80 QsMatchView::QsMatchView(QWidget
*parent
)
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}");
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()));
151 QsMatchView::~QsMatchView()
153 qDeleteAll(d
->m_items
);
158 void QsMatchView::reset()
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;
187 d
->m_compBox
->clear();
189 d
->m_currentItem
= -1;
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();
200 foreach(MatchItem
*item
, items
) {
202 item
->setPos(pos
, SMALL_ICON_PADDING
);
203 item
->scale(0.5, 0.5);
205 d
->m_scene
->addItem(item
);
207 if (item
->description().isEmpty()) {
208 description
= item
->name();
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;
226 //Ensure popup is shown if desired
229 d
->m_compBox
->popup();
230 d
->m_compBox
->setCurrentRow(0);
232 d
->m_compBox
->hide();
234 d
->m_arrowButton
->setArrowType(Qt::DownArrow
);
236 d
->m_currentItem
= 0;
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
));
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
)
269 d
->m_scene
->removeItem(d
->m_descRect
);
270 delete d
->m_descRect
;
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
);
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
)
310 d
->m_itemsRemoved
= false;
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
) {
327 //TODO: Fix animation
328 void QsMatchView::showLoading()
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());
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()) {
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
358 d
->m_listVisible
= true;
361 void QsMatchView::showSelected()
363 if (!d
->m_items
.size()) {
368 MatchItem
*it
= d
->m_items
[d
->m_currentItem
];
373 d
->m_listVisible
= false;
374 d
->m_arrowButton
->setArrowType(Qt::RightArrow
);
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()) {
414 setDescriptionText(i18n("No results found."));
416 emit
selectionChanged(0);
419 if (index
> -1 && index
< d
->m_items
.size()) {
420 MatchItem
*it
= d
->m_items
[index
];
421 d
->m_scene
->setFocusItem(it
);
423 if (it
->description().isEmpty()) {
424 description
= it
->name();
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
)
439 void QsMatchView::scrollLeft()
441 if (d
->m_currentItem
> 0){
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
;
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
;
458 if ((!index
&& d
->m_currentItem
== d
->m_items
.size() - 1)
459 || index
== d
->m_currentItem
+ 1) {
460 x
= item
->pos().x() + spacing
*2;
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
);
471 focusItem(d
->m_currentItem
);
474 void QsMatchView::scrollRight()
476 if (d
->m_currentItem
< d
->m_items
.size() - 1) {
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
;
488 if (d
->m_items
.indexOf(item
) == d
->m_currentItem
) {
489 anim
->setScaleAt(1, 1, 1);
490 y
= LARGE_ICON_PADDING
;
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
);
500 focusItem(d
->m_currentItem
);
503 void QsMatchView::scrollToItem(int index
)
505 if (index
< 0 || d
->m_items
.size() == 0) {
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
);
518 anim
->setScaleAt(1, 1, 1);
519 y
= LARGE_ICON_PADDING
;
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
);
533 d
->m_currentItem
= 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
);
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
)
561 if (!d
->m_hasFocus
) {
562 d
->m_hasFocus
= true;
567 void QsMatchView::focusOutEvent(QFocusEvent
*event
)
573 d
->m_hasFocus
= false;
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
);
588 //Switch to line edit
589 d
->m_stack
->setCurrentIndex(1);
590 d
->m_lineEdit
->setFocus();
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
);
599 if (!d
->m_listVisible
) {
605 if (!d
->m_listVisible
) {
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;
626 //Don't add control characters to the search term
627 foreach (QChar c
, e
->text()) {
629 if (d
->m_stack
->currentIndex() == 1) {
630 d
->m_searchTerm
= d
->m_lineEdit
->text() + c
;
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"