123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492 |
- /*=========================================================================
- Library: CTK
- Copyright (c) Kitware Inc.
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0.txt
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- =========================================================================*/
- // Qt includes
- #include <QActionEvent>
- #include <QCompleter>
- #include <QDebug>
- #include <QEvent>
- #include <QHBoxLayout>
- #include <QLineEdit>
- #include <QStringList>
- #include <QStringListModel>
- // CTK includes
- #include "ctkCompleter.h"
- #include "ctkSearchBox.h"
- #include "ctkMenuComboBox_p.h"
- // -------------------------------------------------------------------------
- ctkMenuComboBoxInternal::ctkMenuComboBoxInternal()
- {
- }
- // -------------------------------------------------------------------------
- ctkMenuComboBoxInternal::~ctkMenuComboBoxInternal()
- {
- }
- // -------------------------------------------------------------------------
- void ctkMenuComboBoxInternal::showPopup()
- {
- QMenu* menu = this->Menu.data();
- if (!menu)
- {
- return;
- }
- menu->popup(this->mapToGlobal(this->rect().bottomLeft()));
- static int minWidth = menu->sizeHint().width();
- menu->setFixedWidth(qMax(this->width(), minWidth));
- emit popupShown();
- }
- // -------------------------------------------------------------------------
- QSize ctkMenuComboBoxInternal::minimumSizeHint()const
- {
- // Cached QComboBox::minimumSizeHint is not recomputed when the current
- // index change, however QComboBox::sizeHint is. Use it instead.
- return this->sizeHint();
- }
- // -------------------------------------------------------------------------
- ctkMenuComboBoxPrivate::ctkMenuComboBoxPrivate(ctkMenuComboBox& object)
- :q_ptr(&object)
- {
- this->MenuComboBox = 0;
- this->SearchCompleter = 0;
- this->EditBehavior = ctkMenuComboBox::EditableOnPopup;
- this->IsDefaultTextCurrent = true;
- this->IsDefaultIconCurrent = true;
- }
- // -------------------------------------------------------------------------
- void ctkMenuComboBoxPrivate::init()
- {
- Q_Q(ctkMenuComboBox);
- this->setParent(q);
- QHBoxLayout* layout = new QHBoxLayout(q);
- layout->setContentsMargins(0,0,0,0);
- layout->setSizeConstraint(QLayout::SetMinimumSize);
- this->MenuComboBox = new ctkMenuComboBoxInternal();
- this->MenuComboBox->setMinimumContentsLength(12);
- layout->addWidget(this->MenuComboBox);
- this->MenuComboBox->installEventFilter(q);
- this->MenuComboBox->setSizeAdjustPolicy(QComboBox::AdjustToContents);
- this->MenuComboBox->addItem(this->DefaultIcon, this->DefaultText);
- this->SearchCompleter = new ctkCompleter(QStringList(), q);
- this->SearchCompleter->setCaseSensitivity(Qt::CaseInsensitive);
- this->SearchCompleter->setModelFiltering(ctkCompleter::FilterWordStartsWith);
- q->connect(this->SearchCompleter, SIGNAL(activated(QString)),
- q, SLOT(onEditingFinished()));
- // Automatically set the minimumSizeHint of the layout to the widget
- layout->setSizeConstraint(QLayout::SetMinimumSize);
- // Behave like a QComboBox
- q->setSizePolicy(QSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed,
- QSizePolicy::ComboBox));
- q->setDefaultText(ctkMenuComboBox::tr("Search..."));
- }
- // ------------------------------------------------------------------------
- QAction* ctkMenuComboBoxPrivate::actionByTitle(const QString& text, const QMenu* parentMenu)
- {
- if (parentMenu->title() == text)
- {
- return 0;
- }
- foreach(QAction* action, parentMenu->actions())
- {
- if (!action->menu() && action->text().toLower() == text.toLower())
- {
- return action;
- }
- if (action->menu())
- {
- QAction* subAction = this->actionByTitle(text, action->menu());
- if(subAction)
- {
- return subAction;
- }
- }
- }
- return 0;
- }
- // ------------------------------------------------------------------------
- void ctkMenuComboBoxPrivate::setCurrentText(const QString& newCurrentText)
- {
- if (this->MenuComboBox->lineEdit())
- {
- static_cast<ctkSearchBox*>(this->MenuComboBox->lineEdit())
- ->setPlaceholderText(newCurrentText);
- }
- this->MenuComboBox->setItemText(this->MenuComboBox->currentIndex(),
- newCurrentText);
- }
- // ------------------------------------------------------------------------
- QString ctkMenuComboBoxPrivate::currentText()const
- {
- return this->MenuComboBox->itemText(this->MenuComboBox->currentIndex());
- }
- // ------------------------------------------------------------------------
- QIcon ctkMenuComboBoxPrivate::currentIcon()const
- {
- return this->MenuComboBox->itemIcon(this->MenuComboBox->currentIndex());
- }
- // ------------------------------------------------------------------------
- void ctkMenuComboBoxPrivate::setCurrentIcon(const QIcon& newCurrentIcon)
- {
- this->MenuComboBox->setItemIcon(this->MenuComboBox->currentIndex(),
- newCurrentIcon);
- }
- // -------------------------------------------------------------------------
- void ctkMenuComboBoxPrivate::setComboBoxEditable(bool edit)
- {
- Q_Q(ctkMenuComboBox);
- if(edit)
- {
- if (!this->MenuComboBox->lineEdit())
- {
- ctkSearchBox* line = new ctkSearchBox();
- this->MenuComboBox->setLineEdit(line);
- q->connect(line, SIGNAL(editingFinished()),
- q,SLOT(onEditingFinished()));
- }
- this->MenuComboBox->setCompleter(this->SearchCompleter);
- }
- this->MenuComboBox->setEditable(edit);
- }
- // -------------------------------------------------------------------------
- void ctkMenuComboBoxPrivate::addAction(QAction *action)
- {
- if (action->menu())
- {
- this->addMenuToCompleter(action->menu());
- }
- else
- {
- this->addActionToCompleter(action);
- }
- }
- // -------------------------------------------------------------------------
- void ctkMenuComboBoxPrivate::addMenuToCompleter(QMenu* menu)
- {
- Q_Q(ctkMenuComboBox);
- menu->installEventFilter(q);
- // Bug QT : see this link for more details
- // https://bugreports.qt.nokia.com/browse/QTBUG-20929?focusedCommentId=161370#comment-161370
- // if the submenu doesn't have a parent, the submenu triggered(QAction*)
- // signal is not propagated. So we listened this submenu to fix the bug.
- QObject* emptyObject = 0;
- if(menu->parent() == emptyObject)
- {
- q->connect(menu, SIGNAL(triggered(QAction*)),
- q, SLOT(onActionSelected(QAction*)), Qt::UniqueConnection);
- }
- foreach (QAction* action, menu->actions())
- {
- this->addAction(action);
- }
- }
- // -------------------------------------------------------------------------
- void ctkMenuComboBoxPrivate::addActionToCompleter(QAction *action)
- {
- QStringListModel* model = qobject_cast<QStringListModel* >(
- this->SearchCompleter->sourceModel());
- Q_ASSERT(model);
- QModelIndex start = model->index(0,0);
- QModelIndexList indexList = model->match(start, 0, action->text());
- if (indexList.count())
- {
- return;
- }
- int actionCount = model->rowCount();
- model->insertRow(actionCount);
- QModelIndex index = model->index(actionCount, 0);
- model->setData(index, action->text());
- }
- // ------------------------------------------------------------------------
- void ctkMenuComboBoxPrivate::removeActionToCompleter(QAction *action)
- {
- QStringListModel* model = qobject_cast<QStringListModel* >(
- this->SearchCompleter->sourceModel());
- Q_ASSERT(model);
- if (!model->stringList().contains(action->text()) )
- {
- return;
- }
- // Maybe the action is present multiple times in different submenus
- // Don't remove its entry from the completer model if there are still some action instances
- // in the menus.
- if (this->actionByTitle(action->text(), this->Menu.data()))
- {
- return;
- }
- QModelIndex start = model->index(0,0);
- QModelIndexList indexList = model->match(start, 0, action->text());
- Q_ASSERT(indexList.count() == 1);
- foreach (QModelIndex index, indexList)
- {
- // Search completer model is a flat list
- model->removeRow(index.row());
- }
- }
- // ------------------------------------------------------------------------
- ctkMenuComboBox::ctkMenuComboBox(QWidget* _parent)
- :QWidget(_parent)
- , d_ptr(new ctkMenuComboBoxPrivate(*this))
- {
- Q_D(ctkMenuComboBox);
- d->init();
- }
- // ------------------------------------------------------------------------
- ctkMenuComboBox::~ctkMenuComboBox()
- {
- }
- // ------------------------------------------------------------------------
- void ctkMenuComboBox::setMenu(QMenu* menu)
- {
- Q_D(ctkMenuComboBox);
- if (d->Menu.data() == menu)
- {
- return;
- }
- if (d->Menu)
- {
- this->removeAction(d->Menu.data()->menuAction());
- QObject::disconnect(d->Menu.data(),SIGNAL(triggered(QAction*)),
- this,SLOT(onActionSelected(QAction*)));
- }
- d->Menu = menu;
- d->MenuComboBox->Menu = menu;
- d->addMenuToCompleter(menu);
- if (d->Menu)
- {
- this->addAction(d->Menu.data()->menuAction());
- QObject::connect(d->Menu.data(),SIGNAL(triggered(QAction*)),
- this,SLOT(onActionSelected(QAction*)), Qt::UniqueConnection);
- }
- }
- // -------------------------------------------------------------------------
- QMenu* ctkMenuComboBox::menu()const
- {
- Q_D(const ctkMenuComboBox);
- return d->Menu.data();
- }
- // -------------------------------------------------------------------------
- void ctkMenuComboBox::setDefaultText(const QString& newDefaultText)
- {
- Q_D(ctkMenuComboBox);
- d->DefaultText = newDefaultText;
- if (d->IsDefaultTextCurrent)
- {
- d->setCurrentText(d->DefaultText);
- }
- }
- // -------------------------------------------------------------------------
- QString ctkMenuComboBox::defaultText()const
- {
- Q_D(const ctkMenuComboBox);
- return d->DefaultText;
- }
- // -------------------------------------------------------------------------
- void ctkMenuComboBox::setDefaultIcon(const QIcon& newIcon)
- {
- Q_D(ctkMenuComboBox);
- d->DefaultIcon = newIcon;
- if (d->IsDefaultIconCurrent)
- {
- d->setCurrentIcon(d->DefaultIcon);
- }
- }
- // -------------------------------------------------------------------------
- QIcon ctkMenuComboBox::defaultIcon()const
- {
- Q_D(const ctkMenuComboBox);
- return d->DefaultIcon;
- }
- // -------------------------------------------------------------------------
- void ctkMenuComboBox::setEditableBehavior(ctkMenuComboBox::EditableBehavior edit)
- {
- Q_D(ctkMenuComboBox);
- d->EditBehavior = edit;
- this->disconnect(d->MenuComboBox, SIGNAL(popupShown()),
- d, SLOT(setComboBoxEditable()));
- switch (edit)
- {
- case ctkMenuComboBox::Editable:
- d->MenuComboBox->setContextMenuPolicy(Qt::DefaultContextMenu);
- d->setComboBoxEditable(true);
- break;
- case ctkMenuComboBox::NotEditable:
- d->MenuComboBox->setContextMenuPolicy(Qt::DefaultContextMenu);
- d->setComboBoxEditable(false);
- break;
- case ctkMenuComboBox::EditableOnFocus:
- d->setComboBoxEditable(this->hasFocus());
- // Here we set the context menu policy to fix a crash on the right click.
- // Opening the context menu removes the focus on the line edit,
- // the comboBox becomes not editable, and the line edit is deleted.
- // The opening of the context menu is done in the line edit and lead to
- // a crash because it infers that the line edit is valid. Another fix
- // could be to delete the line edit later (deleteLater()).
- d->MenuComboBox->setContextMenuPolicy(Qt::NoContextMenu);
- break;
- case ctkMenuComboBox::EditableOnPopup:
- d->setComboBoxEditable(false);
- this->connect(d->MenuComboBox, SIGNAL(popupShown()),
- d, SLOT(setComboBoxEditable()));
- // Same reason as in ctkMenuComboBox::EditableOnFocus.
- d->MenuComboBox->setContextMenuPolicy(Qt::NoContextMenu);
- break;
- }
- }
- // -------------------------------------------------------------------------
- ctkMenuComboBox::EditableBehavior ctkMenuComboBox::editableBehavior()const
- {
- Q_D(const ctkMenuComboBox);
- return d->EditBehavior;
- }
- // -------------------------------------------------------------------------
- void ctkMenuComboBox::setMinimumContentsLength(int characters)
- {
- Q_D(ctkMenuComboBox);
- d->MenuComboBox->setMinimumContentsLength(characters);
- }
- // -------------------------------------------------------------------------
- void ctkMenuComboBox::onActionSelected(QAction* action)
- {
- Q_D(ctkMenuComboBox);
- /// Set the action selected in the combobox.
- d->IsDefaultTextCurrent = true;
- QString newText = d->DefaultText;
- if (action && !action->text().isEmpty())
- {
- newText = action->text();
- d->IsDefaultTextCurrent = false;
- }
- d->setCurrentText(newText);
- d->IsDefaultIconCurrent = true;
- QIcon newIcon = d->DefaultIcon;
- if (action && !action->icon().isNull())
- {
- d->IsDefaultIconCurrent = false;
- newIcon = action->icon();
- }
- d->setCurrentIcon(newIcon);
- d->MenuComboBox->clearFocus();
- emit ctkMenuComboBox::actionChanged(action);
- }
- // -------------------------------------------------------------------------
- void ctkMenuComboBox::clearActiveAction()
- {
- this->onActionSelected(0);
- }
- // -------------------------------------------------------------------------
- void ctkMenuComboBox::onEditingFinished()
- {
- Q_D(ctkMenuComboBox);
- if (!d->MenuComboBox->lineEdit())
- {
- return;
- }
- QAction* action = d->actionByTitle(d->MenuComboBox->lineEdit()->text(), d->Menu.data());
- if (!action)
- {
- return;
- }
- action->trigger();
- }
- // -------------------------------------------------------------------------
- bool ctkMenuComboBox::eventFilter(QObject* target, QEvent* event)
- {
- Q_D(ctkMenuComboBox);
- if (target == d->MenuComboBox)
- {
- if (event->type() == QEvent::Resize)
- {
- this->layout()->invalidate();
- }
- if (event->type() == QEvent::FocusIn &&
- d->EditBehavior == ctkMenuComboBox::EditableOnFocus)
- {
- d->setComboBoxEditable(true);
- }
- if (event->type() == QEvent::FocusOut &&
- (d->EditBehavior == ctkMenuComboBox::EditableOnFocus ||
- d->EditBehavior == ctkMenuComboBox::EditableOnPopup))
- {
- d->setComboBoxEditable(false);
- }
- }
- else if (event->type() == QEvent::ActionAdded)
- {
- QActionEvent* actionEvent = static_cast<QActionEvent *>(event);
- d->addAction(actionEvent->action());
- }
- else if (event->type() == QEvent::ActionRemoved)
- {
- QActionEvent* actionEvent = static_cast<QActionEvent *>(event);
- d->removeActionToCompleter(actionEvent->action());
- }
- return this->Superclass::eventFilter(target, event);
- }
|