1 /*=========================================================================
2 
3   Library:   CTK
4 
5   Copyright (c) Kitware Inc.
6 
7   Licensed under the Apache License, Version 2.0 (the "License");
8   you may not use this file except in compliance with the License.
9   You may obtain a copy of the License at
10 
11       http://www.apache.org/licenses/LICENSE-2.0.txt
12 
13   Unless required by applicable law or agreed to in writing, software
14   distributed under the License is distributed on an "AS IS" BASIS,
15   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   See the License for the specific language governing permissions and
17   limitations under the License.
18 
19 =========================================================================*/
20 
21 // Qt includes
22 #include <QAbstractItemView>
23 #include <QActionEvent>
24 #include <QCompleter>
25 #include <QDebug>
26 #include <QEvent>
27 #include <QHBoxLayout>
28 #include <QLineEdit>
29 #include <QStringList>
30 #include <QStringListModel>
31 #include <QToolButton>
32 
33 // CTK includes
34 #include "ctkCompleter.h"
35 #include "ctkSearchBox.h"
36 #include "ctkMenuComboBox_p.h"
37 
38 // -------------------------------------------------------------------------
ctkMenuComboBoxInternal()39 ctkMenuComboBoxInternal::ctkMenuComboBoxInternal()
40 {
41 }
42 // -------------------------------------------------------------------------
~ctkMenuComboBoxInternal()43 ctkMenuComboBoxInternal::~ctkMenuComboBoxInternal()
44 {
45 }
46 
47 // -------------------------------------------------------------------------
showPopup()48 void ctkMenuComboBoxInternal::showPopup()
49 {
50   QMenu* menu = this->Menu.data();
51   if (!menu)
52     {
53     return;
54     }
55   menu->popup(this->mapToGlobal(this->rect().bottomLeft()));
56   static int minWidth = menu->sizeHint().width();
57   menu->setFixedWidth(qMax(this->width(), minWidth));
58   emit popupShown();
59 }
60 
61 // -------------------------------------------------------------------------
minimumSizeHint() const62 QSize ctkMenuComboBoxInternal::minimumSizeHint()const
63 {
64   // Cached QComboBox::minimumSizeHint is not recomputed when the current
65   // index change, however QComboBox::sizeHint is. Use it instead.
66   return this->sizeHint();
67 }
68 
69 // -------------------------------------------------------------------------
ctkMenuComboBoxPrivate(ctkMenuComboBox & object)70 ctkMenuComboBoxPrivate::ctkMenuComboBoxPrivate(ctkMenuComboBox& object)
71   :q_ptr(&object)
72 {
73   this->MenuComboBox = 0;
74   this->SearchCompleter = 0;
75   this->EditBehavior = ctkMenuComboBox::NotEditable;
76   this->IsDefaultTextCurrent = true;
77   this->IsDefaultIconCurrent = true;
78 }
79 
80 // -------------------------------------------------------------------------
init()81 void ctkMenuComboBoxPrivate::init()
82 {
83   Q_Q(ctkMenuComboBox);
84   this->setParent(q);
85 
86   QHBoxLayout* layout = new QHBoxLayout(q);
87   layout->setContentsMargins(0,0,0,0);
88   layout->setSizeConstraint(QLayout::SetMinimumSize);
89   layout->setSpacing(0);
90 
91   // SearchButton
92   this->SearchButton = new QToolButton();
93   this->SearchButton->setText(q->tr("Search"));
94   this->SearchButton->setIcon(QIcon(":/Icons/search.svg"));
95   this->SearchButton->setCheckable(true);
96   this->SearchButton->setAutoRaise(true);
97   layout->addWidget(this->SearchButton);
98   q->connect(this->SearchButton, SIGNAL(toggled(bool)),
99              this, SLOT(setComboBoxEditable(bool)));
100 
101   // MenuComboBox
102   this->MenuComboBox = new ctkMenuComboBoxInternal();
103   this->MenuComboBox->setMinimumContentsLength(12);
104   layout->addWidget(this->MenuComboBox);
105   this->MenuComboBox->installEventFilter(q);
106   this->MenuComboBox->setInsertPolicy(QComboBox::NoInsert);
107   this->MenuComboBox->setSizeAdjustPolicy(QComboBox::AdjustToContents);
108   this->MenuComboBox->addItem(this->DefaultIcon, this->DefaultText);
109   q->connect(this->MenuComboBox, SIGNAL(popupShown()),
110              q, SIGNAL(popupShown()));
111 
112   this->SearchCompleter = new ctkCompleter(QStringList(), this->MenuComboBox);
113   this->SearchCompleter->popup()->setParent(q);
114   this->SearchCompleter->setCaseSensitivity(Qt::CaseInsensitive);
115   this->SearchCompleter->setModelFiltering(ctkCompleter::FilterWordStartsWith);
116   q->connect(this->SearchCompleter, SIGNAL(activated(QString)),
117              this, SLOT(onCompletion(QString)));
118 
119   // Automatically set the minimumSizeHint of the layout to the widget
120   layout->setSizeConstraint(QLayout::SetMinimumSize);
121   // Behave like a QComboBox
122   q->setSizePolicy(QSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed,
123                                QSizePolicy::ComboBox));
124 
125   q->setDefaultText(ctkMenuComboBox::tr("Search..."));
126 }
127 
128 //  ------------------------------------------------------------------------
actionByTitle(const QString & text,const QMenu * parentMenu)129 QAction* ctkMenuComboBoxPrivate::actionByTitle(const QString& text, const QMenu* parentMenu)
130 {
131   if (!parentMenu || parentMenu->title() == text)
132     {
133     return 0;
134     }
135   foreach(QAction* action, parentMenu->actions())
136     {
137     if (!action->menu() && action->text().toLower() == text.toLower())
138       {
139       return action;
140       }
141     if (action->menu())
142       {
143       QAction* subAction = this->actionByTitle(text, action->menu());
144       if(subAction)
145         {
146         return subAction;
147         }
148       }
149     }
150   return 0;
151 }
152 
153 //  ------------------------------------------------------------------------
setCurrentText(const QString & newCurrentText)154 void ctkMenuComboBoxPrivate::setCurrentText(const QString& newCurrentText)
155 {
156   if (this->MenuComboBox->lineEdit())
157     {
158     static_cast<ctkSearchBox*>(this->MenuComboBox->lineEdit())
159       ->setPlaceholderText(newCurrentText);
160     }
161 
162   this->MenuComboBox->setItemText(this->MenuComboBox->currentIndex(),
163                                   newCurrentText);
164 }
165 
166 //  ------------------------------------------------------------------------
currentText() const167 QString ctkMenuComboBoxPrivate::currentText()const
168 {
169   return this->MenuComboBox->itemText(this->MenuComboBox->currentIndex());
170 }
171 
172 //  ------------------------------------------------------------------------
currentIcon() const173 QIcon ctkMenuComboBoxPrivate::currentIcon()const
174 {
175   return this->MenuComboBox->itemIcon(this->MenuComboBox->currentIndex());
176 }
177 
178 //  ------------------------------------------------------------------------
setCurrentIcon(const QIcon & newCurrentIcon)179 void ctkMenuComboBoxPrivate::setCurrentIcon(const QIcon& newCurrentIcon)
180 {
181   this->MenuComboBox->setItemIcon(this->MenuComboBox->currentIndex(),
182                                   newCurrentIcon);
183 }
184 
185 // -------------------------------------------------------------------------
setComboBoxEditable(bool edit)186 void ctkMenuComboBoxPrivate::setComboBoxEditable(bool edit)
187 {
188   Q_Q(ctkMenuComboBox);
189   if(edit)
190     {
191     if (!this->MenuComboBox->lineEdit())
192       {
193       ctkSearchBox* line = new ctkSearchBox();
194       this->MenuComboBox->setLineEdit(line);
195       if (q->isSearchIconVisible())
196         {
197         this->MenuComboBox->lineEdit()->selectAll();
198         this->MenuComboBox->setFocus();
199         }
200       q->connect(line, SIGNAL(editingFinished()),
201                  q,SLOT(onEditingFinished()));
202       }
203     this->MenuComboBox->setCompleter(this->SearchCompleter);
204     }
205 
206   this->MenuComboBox->setEditable(edit);
207 }
208 
209 // -------------------------------------------------------------------------
onCompletion(const QString & text)210 void ctkMenuComboBoxPrivate::onCompletion(const QString& text)
211 {
212   Q_Q(ctkMenuComboBox);
213 
214   // In Qt5, when QCompleter sends its activated() signal, QComboBox sets
215   // its current index to the activated item, if found. Work around that behavior
216   // by re-selecting the original item.
217   this->MenuComboBox->setCurrentIndex(0);
218 
219   // Set text to the completed string
220   if (this->MenuComboBox->lineEdit())
221     {
222     this->MenuComboBox->lineEdit()->setText(text);
223     }
224 
225   q->onEditingFinished();
226 }
227 
228 // -------------------------------------------------------------------------
addAction(QAction * action)229 void ctkMenuComboBoxPrivate::addAction(QAction *action)
230 {
231   if (action->menu())
232     {
233     this->addMenuToCompleter(action->menu());
234     }
235   else
236     {
237     this->addActionToCompleter(action);
238     }
239 }
240 
241 // -------------------------------------------------------------------------
addMenuToCompleter(QMenu * menu)242 void ctkMenuComboBoxPrivate::addMenuToCompleter(QMenu* menu)
243 {
244   Q_Q(ctkMenuComboBox);
245   menu->installEventFilter(q);
246 
247   // Bug QT : see this link for more details
248   // https://bugreports.qt.nokia.com/browse/QTBUG-20929?focusedCommentId=161370#comment-161370
249   // if the submenu doesn't have a parent, the submenu triggered(QAction*)
250   // signal is not propagated. So we listened this submenu to fix the bug.
251   QObject* emptyObject = 0;
252   if(menu->parent() == emptyObject)
253     {
254     q->connect(menu, SIGNAL(triggered(QAction*)),
255                q, SLOT(onActionSelected(QAction*)), Qt::UniqueConnection);
256     }
257 
258   foreach (QAction* action, menu->actions())
259     {
260     this->addAction(action);
261     }
262 }
263 
264 // -------------------------------------------------------------------------
addActionToCompleter(QAction * action)265 void ctkMenuComboBoxPrivate::addActionToCompleter(QAction *action)
266 {
267   QStringListModel* model = qobject_cast<QStringListModel* >(
268     this->SearchCompleter->sourceModel());
269   Q_ASSERT(model);
270   QModelIndex start = model->index(0,0);
271   QModelIndexList indexList = model->match(start, 0, action->text(), 1, Qt::MatchFixedString|Qt::MatchWrap);
272   if (indexList.count())
273     {
274     return;
275     }
276 
277   int actionCount = model->rowCount();
278   model->insertRow(actionCount);
279   QModelIndex index = model->index(actionCount, 0);
280   model->setData(index, action->text());
281 }
282 
283 //  ------------------------------------------------------------------------
removeActionToCompleter(QAction * action)284 void ctkMenuComboBoxPrivate::removeActionToCompleter(QAction *action)
285 {
286   QStringListModel* model = qobject_cast<QStringListModel* >(
287     this->SearchCompleter->sourceModel());
288   Q_ASSERT(model);
289   if (!model->stringList().contains(action->text()) )
290     {
291     return;
292     }
293 
294   // Maybe the action is present multiple times in different submenus
295   // Don't remove its entry from the completer model if there are still some action instances
296   // in the menus.
297   if (this->actionByTitle(action->text(), this->Menu.data()))
298     {
299     return;
300     }
301 
302   QModelIndex start = model->index(0,0);
303   QModelIndexList indexList = model->match(start, 0, action->text());
304   Q_ASSERT(indexList.count() == 1);
305   foreach (QModelIndex index, indexList)
306     {
307     // Search completer model is a flat list
308     model->removeRow(index.row());
309     }
310 }
311 
312 //  ------------------------------------------------------------------------
ctkMenuComboBox(QWidget * _parent)313 ctkMenuComboBox::ctkMenuComboBox(QWidget* _parent)
314   :QWidget(_parent)
315   , d_ptr(new ctkMenuComboBoxPrivate(*this))
316 {
317   Q_D(ctkMenuComboBox);
318   d->init();
319 }
320 
321 //  ------------------------------------------------------------------------
~ctkMenuComboBox()322 ctkMenuComboBox::~ctkMenuComboBox()
323 {
324 }
325 
326 //  ------------------------------------------------------------------------
setMenu(QMenu * menu)327 void ctkMenuComboBox::setMenu(QMenu* menu)
328 {
329   Q_D(ctkMenuComboBox);
330   if (d->Menu.data() == menu)
331     {
332     return;
333     }
334 
335   if (d->Menu)
336     {
337     this->removeAction(d->Menu.data()->menuAction());
338     QObject::disconnect(d->Menu.data(),SIGNAL(triggered(QAction*)),
339                         this,SLOT(onActionSelected(QAction*)));
340     }
341 
342   d->Menu = menu;
343   d->MenuComboBox->Menu = menu;
344   d->addMenuToCompleter(menu);
345 
346   if (d->Menu)
347     {
348     this->addAction(d->Menu.data()->menuAction());
349     QObject::connect(d->Menu.data(),SIGNAL(triggered(QAction*)),
350                      this,SLOT(onActionSelected(QAction*)), Qt::UniqueConnection);
351     }
352 }
353 
354 // -------------------------------------------------------------------------
menu() const355 QMenu* ctkMenuComboBox::menu()const
356 {
357   Q_D(const ctkMenuComboBox);
358   return d->Menu.data();
359 }
360 
361 // -------------------------------------------------------------------------
setDefaultText(const QString & newDefaultText)362 void ctkMenuComboBox::setDefaultText(const QString& newDefaultText)
363 {
364   Q_D(ctkMenuComboBox);
365   d->DefaultText = newDefaultText;
366   if (d->IsDefaultTextCurrent)
367     {
368     d->setCurrentText(d->DefaultText);
369     }
370 }
371 
372 // -------------------------------------------------------------------------
defaultText() const373 QString ctkMenuComboBox::defaultText()const
374 {
375   Q_D(const ctkMenuComboBox);
376   return d->DefaultText;
377 }
378 
379 // -------------------------------------------------------------------------
setDefaultIcon(const QIcon & newIcon)380 void ctkMenuComboBox::setDefaultIcon(const QIcon& newIcon)
381 {
382   Q_D(ctkMenuComboBox);
383   d->DefaultIcon = newIcon;
384   if (d->IsDefaultIconCurrent)
385     {
386     d->setCurrentIcon(d->DefaultIcon);
387     }
388 }
389 
390 // -------------------------------------------------------------------------
defaultIcon() const391 QIcon ctkMenuComboBox::defaultIcon()const
392 {
393   Q_D(const ctkMenuComboBox);
394   return d->DefaultIcon;
395 }
396 
397 // -------------------------------------------------------------------------
setEditableBehavior(ctkMenuComboBox::EditableBehavior edit)398 void ctkMenuComboBox::setEditableBehavior(ctkMenuComboBox::EditableBehavior edit)
399 {
400   Q_D(ctkMenuComboBox);
401   d->EditBehavior = edit;
402       this->disconnect(d->MenuComboBox, SIGNAL(popupShown()),
403                     d, SLOT(setComboBoxEditable()));
404   switch (edit)
405   {
406     case ctkMenuComboBox::Editable:
407       d->MenuComboBox->setContextMenuPolicy(Qt::DefaultContextMenu);
408       d->setComboBoxEditable(true);
409       break;
410     case ctkMenuComboBox::NotEditable:
411       d->MenuComboBox->setContextMenuPolicy(Qt::DefaultContextMenu);
412       d->setComboBoxEditable(false);
413       break;
414     case ctkMenuComboBox::EditableOnFocus:
415       d->setComboBoxEditable(this->hasFocus());
416       // Here we set the context menu policy to fix a crash on the right click.
417       // Opening the context menu removes the focus on the line edit,
418       // the comboBox becomes not editable, and the line edit is deleted.
419       // The opening of the context menu is done in the line edit and lead to
420       // a crash because it infers that the line edit is valid. Another fix
421       // could be to delete the line edit later (deleteLater()).
422       d->MenuComboBox->setContextMenuPolicy(Qt::NoContextMenu);
423       break;
424     case ctkMenuComboBox::EditableOnPopup:
425       d->setComboBoxEditable(false);
426       this->connect(d->MenuComboBox, SIGNAL(popupShown()),
427                     d, SLOT(setComboBoxEditable()));
428       // Same reason as in ctkMenuComboBox::EditableOnFocus.
429       d->MenuComboBox->setContextMenuPolicy(Qt::NoContextMenu);
430       break;
431   }
432 }
433 
434 // -------------------------------------------------------------------------
editableBehavior() const435 ctkMenuComboBox::EditableBehavior ctkMenuComboBox::editableBehavior()const
436 {
437   Q_D(const ctkMenuComboBox);
438   return d->EditBehavior;
439 }
440 
441 // -------------------------------------------------------------------------
setSearchIconVisible(bool state)442 void ctkMenuComboBox::setSearchIconVisible(bool state)
443 {
444   Q_D(ctkMenuComboBox);
445   d->SearchButton->setVisible(state);
446 }
447 
448 // -------------------------------------------------------------------------
isSearchIconVisible() const449 bool ctkMenuComboBox::isSearchIconVisible() const
450 {
451   Q_D(const ctkMenuComboBox);
452   return d->SearchButton->isVisibleTo(const_cast<ctkMenuComboBox*>(this));
453 }
454 
455 // -------------------------------------------------------------------------
setToolButtonStyle(Qt::ToolButtonStyle style)456 void ctkMenuComboBox::setToolButtonStyle(Qt::ToolButtonStyle style)
457 {
458   Q_D(ctkMenuComboBox);
459   d->SearchButton->setToolButtonStyle(style);
460 }
461 
462 // -------------------------------------------------------------------------
toolButtonStyle() const463 Qt::ToolButtonStyle ctkMenuComboBox::toolButtonStyle() const
464 {
465   Q_D(const ctkMenuComboBox);
466   return d->SearchButton->toolButtonStyle();
467 }
468 // -------------------------------------------------------------------------
setMinimumContentsLength(int characters)469 void ctkMenuComboBox::setMinimumContentsLength(int characters)
470 {
471   Q_D(ctkMenuComboBox);
472   d->MenuComboBox->setMinimumContentsLength(characters);
473 }
474 
475 // -------------------------------------------------------------------------
menuComboBoxInternal() const476 QComboBox* ctkMenuComboBox::menuComboBoxInternal() const
477 {
478   Q_D(const ctkMenuComboBox);
479   return d->MenuComboBox;
480 }
481 
482 // -------------------------------------------------------------------------
toolButtonInternal() const483 QToolButton* ctkMenuComboBox::toolButtonInternal() const
484 {
485   Q_D(const ctkMenuComboBox);
486   return d->SearchButton;
487 }
488 
489 // -------------------------------------------------------------------------
searchCompleter() const490 ctkCompleter* ctkMenuComboBox::searchCompleter() const
491 {
492   Q_D(const ctkMenuComboBox);
493   return d->SearchCompleter;
494 }
495 
496 // -------------------------------------------------------------------------
onActionSelected(QAction * action)497 void ctkMenuComboBox::onActionSelected(QAction* action)
498 {
499   Q_D(ctkMenuComboBox);
500   /// Set the action selected in the combobox.
501 
502   d->IsDefaultTextCurrent = true;
503   QString newText = d->DefaultText;
504   if (action && !action->text().isEmpty())
505     {
506     newText = action->text();
507     d->IsDefaultTextCurrent = false;
508     }
509   d->setCurrentText(newText);
510 
511   d->IsDefaultIconCurrent = true;
512   QIcon newIcon = d->DefaultIcon;
513   if (action && !action->icon().isNull())
514     {
515     d->IsDefaultIconCurrent = false;
516     newIcon = action->icon();
517     }
518   d->setCurrentIcon(newIcon);
519 
520   d->MenuComboBox->clearFocus();
521 
522   emit ctkMenuComboBox::actionChanged(action);
523 }
524 
525 // -------------------------------------------------------------------------
clearActiveAction()526 void ctkMenuComboBox::clearActiveAction()
527 {
528   this->onActionSelected(0);
529 }
530 
531 // -------------------------------------------------------------------------
onEditingFinished()532 void ctkMenuComboBox::onEditingFinished()
533 {
534   Q_D(ctkMenuComboBox);
535   if (!d->MenuComboBox->lineEdit())
536     {
537     return;
538     }
539   QAction* action = d->actionByTitle(d->MenuComboBox->lineEdit()->text(), d->Menu.data());
540   if (!action)
541     {
542     return;
543     }
544   if (this->isSearchIconVisible())
545     {
546     d->SearchButton->setChecked(false);
547     }
548 
549   action->trigger();
550 }
551 
552 // -------------------------------------------------------------------------
eventFilter(QObject * target,QEvent * event)553 bool ctkMenuComboBox::eventFilter(QObject* target, QEvent* event)
554 {
555   Q_D(ctkMenuComboBox);
556 
557   if (target == d->MenuComboBox)
558     {
559     if (event->type() == QEvent::Resize)
560       {
561       this->layout()->invalidate();
562       }
563     if (event->type() == QEvent::FocusIn &&
564         d->EditBehavior == ctkMenuComboBox::EditableOnFocus)
565       {
566       d->setComboBoxEditable(true);
567       }
568     if (event->type() == QEvent::FocusOut &&
569         (d->EditBehavior == ctkMenuComboBox::EditableOnFocus ||
570          d->EditBehavior == ctkMenuComboBox::EditableOnPopup))
571       {
572       d->setComboBoxEditable(false);
573       }
574     }
575   else if (event->type() == QEvent::ActionAdded)
576     {
577     QActionEvent* actionEvent = static_cast<QActionEvent *>(event);
578     d->addAction(actionEvent->action());
579     }
580   else if (event->type() == QEvent::ActionRemoved)
581     {
582     QActionEvent* actionEvent = static_cast<QActionEvent *>(event);
583     d->removeActionToCompleter(actionEvent->action());
584     }
585   return this->Superclass::eventFilter(target, event);
586 }
587