/* Copyright 2007 Robert Knight Copyright 2008-2009 Sebastian Sauer This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // Own #include "menuview.h" // Qt #include #include #include #include #include #include #include #include #include // KDE #include #include #include // Local #include "core/models.h" #include "core/itemhandlers.h" #define MAX_NAME_SIZE 50 Q_DECLARE_METATYPE(QPersistentModelIndex) Q_DECLARE_METATYPE(QAction*) using namespace Kickoff; /// @internal d-pointer class class MenuView::Private { public: enum { ActionRole = Qt::UserRole + 52 }; Private(MenuView *q) : q(q), column(0), launcher(new UrlItemLauncher(q)), formattype(MenuView::DescriptionName) {} ~Private() { qDeleteAll(items); } QAction *createActionForIndex(QAbstractItemModel *model, const QModelIndex& index, QMenu *parent) { Q_ASSERT(index.isValid()); QAction *action = 0; if (model->hasChildren(index)) { KMenu *childMenu = new KMenu(parent); childMenu->installEventFilter(q); childMenu->setContextMenuPolicy(Qt::CustomContextMenu); connect(childMenu, SIGNAL(customContextMenuRequested(QPoint)), q, SLOT(contextMenuRequested(QPoint))); action = childMenu->menuAction(); buildBranch(childMenu, model, index); } else { action = q->createLeafAction(index, parent); } q->updateAction(model, action, index); return action; } QString trunctuateName(QString name, int maxSize) { if (name.length() <= maxSize) { return name; } maxSize -= 2; // remove the 3 placeholder points const int start = maxSize / 3; //use one third of the chars for the start of the name const int end = maxSize - start; return name.left(start) + ".." + name.right(end); } void buildBranch(KMenu *menu, QAbstractItemModel *model, const QModelIndex& parent) { if (model->canFetchMore(parent)) { model->fetchMore(parent); } const int rowCount = model->rowCount(parent); for (int i = 0; i < rowCount; i++) { QAction *action = createActionForIndex(model, model->index(i, column, parent), menu); action->setText(trunctuateName(action->text(), MAX_NAME_SIZE)); menu->addAction(action); } } QModelIndex findByRelPath(QAbstractItemModel * model, const QModelIndex & parent, const QString & relPath) { QModelIndex newRoot; if (model->canFetchMore(parent)) { model->fetchMore(parent); } const int rowCount = model->rowCount(parent); for (int row = 0; row < rowCount; row++) { QModelIndex index = model->index(row, 0, parent); Q_ASSERT(index.isValid()); if (index.data(Kickoff::RelPathRole).isValid()) { QString indexRelPath = index.data(Kickoff::RelPathRole).toString(); if (relPath == indexRelPath) { newRoot = index; break; } if (!indexRelPath.isEmpty() && relPath.startsWith(indexRelPath)) { newRoot = findByRelPath(model, index, relPath); break; } } } return newRoot; } MenuView * const q; int column; UrlItemLauncher *launcher; MenuView::FormatType formattype; QPoint mousePressPos; QList items; QHash modelsHeader; QList > models; }; MenuView::MenuView(QWidget *parent, const QString &title, const QIcon &icon) : KMenu(parent) , d(new Private(this)) { if (! title.isNull()) setTitle(title); if (! icon.isNull()) setIcon(icon); installEventFilter(this); connect(this, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(contextMenuRequested(QPoint))); } MenuView::~MenuView() { QListIterator > it(d->models); while (it.hasNext()) { QAbstractItemModel *model = it.next().data(); if (model) { model->disconnect(this); } } delete d; } QAction *MenuView::createLeafAction(const QModelIndex &index, QObject *parent) { Q_UNUSED(index); return new QAction(parent); } void MenuView::updateAction(QAbstractItemModel *model, QAction *action, const QModelIndex& index) { bool isSeparator = index.data(Kickoff::SeparatorRole).value(); // if Description or DescriptionName -> displayOrder = Kickoff::NameAfterDescription // Qt::DisplayRole returns genericName, the generic name e.g. "Advanced Text Editor" or "Spreadsheet" or just "" (right, it's a mess too) // Kickoff::SubTitleRole returns appName, the name e.g. "Kate" or "OpenOffice.org Calc" (right, sometimes the text is also used for the generic app-name) // // if Name or NameDescription or NameDashDescription -> displayOrder = Kickoff::NameBeforeDescription // Qt::DisplayRole returns appName, // Kickoff::SubTitleRole returns genericName. QString mainText = index.data(Qt::DisplayRole).value().replace('&', "&&"); QString altText = index.data(Kickoff::SubTitleRole).value().replace('&', "&&"); if (action->menu() != 0) { // if it is an item with sub-menuitems, we probably like to thread them another way... action->setText(mainText); } else { switch (d->formattype) { case Name: case Description: { action->setText(mainText); action->setToolTip(altText); } break; case NameDescription: // fall through case NameDashDescription: // fall through case DescriptionName: { if (!mainText.isEmpty()) { // seems we have a program, but some of them don't define a name at all if (mainText.contains(altText, Qt::CaseInsensitive)) { // sometimes the description contains also the name action->setText(mainText); } else if (altText.contains(mainText, Qt::CaseInsensitive)) { // and sometimes the name also contains the description action->setText(altText); } else { // seems we have a perfect desktop-file (likely a KDE one, heh) and name+description are clear separated if (d->formattype == NameDashDescription) { action->setText(QString("%1 - %2").arg(mainText).arg(altText)); } else { action->setText(QString("%1 (%2)").arg(mainText).arg(altText)); } } } else { // if there is no name, let's just use the describing text action->setText(altText); } } break; } } action->setSeparator(isSeparator); if (!isSeparator) { action->setIcon(index.data(Qt::DecorationRole).value()); } // we map modelindex and action together action->setData(qVariantFromValue(QPersistentModelIndex(index))); // don't emit the dataChanged-signal cause else we may end in a infinite loop model->blockSignals(true); model->setData(index, qVariantFromValue(action), Private::ActionRole); model->blockSignals(false); } bool MenuView::eventFilter(QObject *watched, QEvent *event) { switch(event->type()) { case QEvent::MouseMove: { QMouseEvent *mouseEvent = static_cast(event); QMenu *watchedMenu = qobject_cast(watched); const int mousePressDistance = !d->mousePressPos.isNull() ? (mouseEvent->pos() - d->mousePressPos).manhattanLength() : 0; if (watchedMenu && mouseEvent->buttons() & Qt::LeftButton && mousePressDistance >= QApplication::startDragDistance()) { QAction *action = watchedMenu->actionAt(mouseEvent->pos()); if (!action) { return KMenu::eventFilter(watched, event); } QPersistentModelIndex index = action->data().value(); if (!index.isValid()) { return KMenu::eventFilter(watched, event); } QUrl url = index.data(UrlRole).toUrl(); if (url.isEmpty()) { return KMenu::eventFilter(watched, event); } QMimeData *mimeData = new QMimeData(); mimeData->setUrls(QList() << url); mimeData->setText(url.toString()); QDrag *drag = new QDrag(this); drag->setMimeData(mimeData); QIcon icon = action->icon(); drag->setPixmap(icon.pixmap(IconSize(KIconLoader::Desktop))); d->mousePressPos = QPoint(); Qt::DropAction dropAction = drag->exec(); Q_UNUSED(dropAction); return true; } break; } case QEvent::MouseButtonPress: { QMouseEvent *mouseEvent = static_cast(event); QMenu *watchedMenu = qobject_cast(watched); if (watchedMenu) { d->mousePressPos = mouseEvent->pos(); } break; } case QEvent::MouseButtonRelease: { QMenu *watchedMenu = qobject_cast(watched); if (watchedMenu) { d->mousePressPos = QPoint(); } break; } case QEvent::Hide: { emit afterBeingHidden(); break; } case QEvent::ToolTip: { bool hide = true; if ((d->formattype == Name) || (d->formattype == Description)) { QHelpEvent *helpEvent = static_cast(event); QMenu *watchedMenu = qobject_cast(watched); if (watchedMenu && watchedMenu->activeAction()) { QString toolTip = watchedMenu->activeAction()->toolTip(); if ((toolTip != watchedMenu->activeAction()->text()) && (watchedMenu->activeAction()->menu() == 0)) { QToolTip::showText(helpEvent->globalPos(), toolTip); hide = false ; } } } if (hide) { QToolTip::hideText(); event->ignore(); } break; } default: break; } return KMenu::eventFilter(watched, event); } void MenuView::addModel(QAbstractItemModel *model, MenuView::ModelOptions options, const QString & relativePath) { QString title = model->headerData(0, Qt::Horizontal, Qt::DisplayRole).toString(); QAction *header = addTitle(title); header->setVisible(false); d->modelsHeader.insert(model, header); d->models.append(model); if (options & MergeFirstLevel) { const int count = model->rowCount(); for (int row = 0; row < count; ++row) { QModelIndex index = model->index(row, 0, QModelIndex()); Q_ASSERT(index.isValid()); const QString title = index.data(Qt::DisplayRole).value(); if (count > 1 && ! title.isEmpty() && model->rowCount(index) > 0) { addTitle(title); } model->blockSignals(true); model->setData(index, qVariantFromValue(this->menuAction()), Private::ActionRole); model->blockSignals(false); d->buildBranch(this, model, index); } } else { QModelIndex root; if (!relativePath.isEmpty()) { root = d->findByRelPath(model, root, relativePath); } d->buildBranch(this, model, root); } connect(model, SIGNAL(rowsInserted(QModelIndex,int,int)), this, SLOT(rowsInserted(QModelIndex,int,int))); connect(model, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)), this, SLOT(rowsAboutToBeRemoved(QModelIndex,int,int))); connect(model, SIGNAL(dataChanged(QModelIndex,QModelIndex)), this, SLOT(dataChanged(QModelIndex,QModelIndex))); connect(model, SIGNAL(modelReset()), this, SLOT(modelReset())); } void MenuView::addItem(QStandardItem *item) { QAction *action = new QAction(item->icon(), item->text(), this); KUrl url(item->data(Kickoff::UrlRole).toString()); Q_ASSERT(url.isValid()); action->setData(url); addAction(action); d->items << item; } UrlItemLauncher *MenuView::launcher() const { return d->launcher; } QModelIndex MenuView::indexForAction(QAction *action) const { Q_ASSERT(action != 0); QPersistentModelIndex index = action->data().value(); return index; } QAction *MenuView::actionForIndex(const QModelIndex& index) const { if (!index.isValid()) { return this->menuAction(); } const QAbstractItemModel *model = index.model(); Q_ASSERT(model); QVariant v = model->data(index, Private::ActionRole); Q_ASSERT(v.isValid()); QAction* a = v.value(); Q_ASSERT(a); return a; } bool MenuView::isValidIndex(const QModelIndex& index) const { QVariant v = (index.isValid() && index.model()) ? index.model()->data(index, Private::ActionRole) : QVariant(); return v.isValid() && v.value(); } void MenuView::rowsInserted(const QModelIndex& parent, int start, int end) { kDebug() << start << end; Q_ASSERT(parent.isValid()); Q_ASSERT(parent.model()); //Q_ASSERT( ! isValidIndex(parent) ); QMenu *menu = isValidIndex(parent) ? actionForIndex(parent)->menu() : this; QAbstractItemModel *model = const_cast(parent.model()); if (!model) { return; } QList newActions; for (int row = start; row <= end; row++) { QModelIndex index = model->index(row, d->column, parent); QAction *newAction = d->createActionForIndex(model, index, menu); kDebug()<<"new action="<text(); newActions << newAction; } int lastidx = -1, offset = -1; for (int i = 0; i < menu->actions().count(); ++i) { QAction *action = menu->actions()[i]; Q_ASSERT(action); QModelIndex index = indexForAction(action); if (index.isValid() && index.model() == model) { lastidx = i; if(++offset == start) break; } } if (lastidx < 0 && d->modelsHeader.contains(model)) { QAction *header = d->modelsHeader[model]; lastidx = menu->actions().indexOf(header); } if (lastidx >= 0) { if (offset < start) { lastidx++; // insert action the item right after the last valid index } if (lastidx < menu->actions().count()) { menu->insertActions(menu->actions()[lastidx], newActions); } else { lastidx = -1; } } if (lastidx < 0) { // just append the action menu->addActions(newActions); } } void MenuView::rowsAboutToBeRemoved(const QModelIndex& parent, int start, int end) { kDebug() << start << end; Q_UNUSED(parent); Q_UNUSED(start); Q_UNUSED(end); modelReset(); } void MenuView::dataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { QAbstractItemModel *model = const_cast(topLeft.model()); Q_ASSERT(model); //Q_ASSERT( ! isValidIndex(topLeft) ); QMenu *menu = isValidIndex(topLeft) ? actionForIndex(topLeft)->menu() : this; QList actions = menu->actions(); Q_ASSERT(bottomRight.row() < actions.count()); for (int row = topLeft.row(); row <= bottomRight.row() && row < actions.count(); ++row) { QModelIndex index = model->index(row, d->column, topLeft.parent()); kDebug()<(); updateAction(model, actions[row], index); } } void MenuView::modelReset() { kDebug(); deleteLater(); // we need to force clearance of the menu and rebuild from scratch } void MenuView::setColumn(int column) { d->column = column; modelReset(); } int MenuView::column() const { return d->column; } MenuView::FormatType MenuView::formatType() const { return d->formattype; } void MenuView::setFormatType(MenuView::FormatType formattype) { d->formattype = formattype; } void MenuView::setModelTitleVisible(QAbstractItemModel *model, bool visible) { if (d->modelsHeader.contains(model)) { QAction *header = d->modelsHeader[model]; header->setVisible(visible); } } void MenuView::actionTriggered(QAction *action) { KUrl url = action->data().value(); if (url.isValid()) { d->launcher->openUrl(url.url()); } else { QModelIndex index = indexForAction(action); if (index.isValid()) { d->launcher->openItem(index); } else { kWarning()<<"Invalid action objectName="<objectName()<<"text="<text()<<"parent="<<(action->parent()?action->parent()->metaObject()->className():"NULL"); } } } void MenuView::contextMenuRequested(const QPoint &pos) { KMenu *menu = qobject_cast(sender()); emit customContextMenuRequested(menu, pos); } #include "moc_menuview.cpp"