/* Copyright 2007 Pino Toscano Copyright 2007 Robert Knight 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 "applicationmodel.h" // Qt #include #include // KDE #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kickoffadaptor.h" // Local #include "models.h" #include template <> inline void KConfigGroup::writeEntry(const char *pKey, const KGlobalSettings::Completion& aValue, KConfigBase::WriteConfigFlags flags) { writeEntry(pKey, int(aValue), flags); } namespace Kickoff { class AppNode { public: AppNode() : parent(0), fetched(false), isDir(false), isSeparator(false), subTitleMandatory(false) { } ~AppNode() { qDeleteAll(children); } QList children; QIcon icon; QString iconName; QString genericName; QString appName; QString relPath; QString desktopEntry; AppNode *parent; DisplayOrder displayOrder; bool fetched : 1; bool isDir : 1; bool isSeparator : 1; bool subTitleMandatory : 1; }; class ApplicationModelPrivate { public: ApplicationModelPrivate(ApplicationModel *qq, bool _allowSeparators) : q(qq), root(new AppNode()), duplicatePolicy(ApplicationModel::ShowDuplicatesPolicy), systemApplicationPolicy(ApplicationModel::ShowApplicationAndSystemPolicy), // primaryNamePolicy(ApplicationModel::GenericNamePrimary), displayOrder(NameAfterDescription), allowSeparators(_allowSeparators), showRecentlyInstalled(true), sortAppsByName(false) { systemApplications = Kickoff::systemApplicationList(); } ~ApplicationModelPrivate() { delete root; } void fillNode(const QString &relPath, AppNode *node); static QHash iconNameMap(); ApplicationModel *q; QWeakPointer applet; AppNode *root; ApplicationModel::DuplicatePolicy duplicatePolicy; ApplicationModel::SystemApplicationPolicy systemApplicationPolicy; QStringList systemApplications; DisplayOrder displayOrder; bool allowSeparators; bool showRecentlyInstalled; bool sortAppsByName; QStringList newInstalledPrograms; QHash seenPrograms; }; void ApplicationModelPrivate::fillNode(const QString &_relPath, AppNode *node) { if (_relPath=="new/") { Q_FOREACH (const QString &it, newInstalledPrograms) { KService::Ptr p = KService::serviceByStorageId(it); if (p->noDisplay()) { continue; } AppNode *newnode = new AppNode(); newnode->icon = QIcon::fromTheme(p->icon(), QIcon::fromTheme("unknown")); newnode->appName = p->name(); newnode->genericName = p->genericName(); newnode->desktopEntry = p->entryPath(); newnode->parent = node; node->children.append(newnode); } return; } KServiceGroup::Ptr root = KServiceGroup::group(_relPath); if (!root || !root->isValid()) { return; } const KServiceGroup::List list = root->entries(true /* sorted */, true /* exclude no display entries */, allowSeparators /* allow separators */, !sortAppsByName /* sort by generic name */); // application name <-> service map for detecting duplicate entries QHash existingServices; // generic name <-> node mapping to determinate duplicate generic names QHash > genericNames; for (KServiceGroup::List::ConstIterator it = list.constBegin(); it != list.constEnd(); ++it) { QString icon; QString appName; QString genericName; QString relPath = _relPath; QString desktopEntry; bool isDir = false; bool isSeparator = false; const KSycocaEntry::Ptr p = (*it); if (p->isType(KST_KService)) { const KService::Ptr service = KService::Ptr(static_cast(p.data())); if (service->noDisplay()) { continue; } icon = service->icon(); appName = service->name(); genericName = service->genericName(); desktopEntry = service->entryPath(); // check for duplicates (eg. KDE 3 and KDE 4 versions of application // both present) if (duplicatePolicy == ApplicationModel::ShowLatestOnlyPolicy && existingServices.contains(appName)) { if (Kickoff::isLaterVersion(existingServices[appName], service)) { continue; } else { // find and remove the existing entry with the same name for (int i = node->children.count() - 1; i >= 0; --i) { AppNode *app = node->children.at(i); if (app->appName == appName && app->genericName == genericName && app->iconName == icon) { app = node->children.takeAt(i); const QString s = app->genericName.toLower(); if (genericNames.contains(s)) { QList list = genericNames[s]; for (int j = list.count() - 1; j >= 0; --j) { if(list.at(j) == app) { list.takeAt(j); } } genericNames[s] = list; } delete app; } } } } if (systemApplicationPolicy == ApplicationModel::ShowSystemOnlyPolicy && systemApplications.contains(service->desktopEntryName())) { // don't duplicate applications that are configured to appear in the System tab // in the Applications tab continue; } existingServices[appName] = service; } else if (p->isType(KST_KServiceGroup)) { const KServiceGroup::Ptr serviceGroup = KServiceGroup::Ptr(static_cast(p.data())); if (serviceGroup->noDisplay() || serviceGroup->childCount() == 0) { continue; } //qDebug() << "Service group" << serviceGroup->entryPath() << serviceGroup->icon() << serviceGroup->relPath() << serviceGroup->directoryEntryPath(); icon = serviceGroup->icon(); if (iconNameMap().contains(icon)) { icon = iconNameMap().value(icon); } desktopEntry = serviceGroup->entryPath(); genericName = serviceGroup->caption(); relPath = serviceGroup->relPath(); appName = serviceGroup->comment(); isDir = true; } else if (p->isType(KST_KServiceSeparator)) { isSeparator = true; } else { qWarning() << "KServiceGroup: Unexpected object in list!"; continue; } AppNode *newnode = new AppNode(); newnode->iconName = icon; newnode->icon = QIcon::fromTheme(icon, QIcon::fromTheme("unknown")); newnode->appName = appName; newnode->genericName = genericName; newnode->relPath = relPath; newnode->desktopEntry = desktopEntry; newnode->isDir = isDir; newnode->isSeparator = isSeparator; newnode->parent = node; node->children.append(newnode); if (p->isType(KST_KService)) { const QString s = genericName.toLower(); QList list = genericNames.value(s); list.append(newnode); genericNames[s] = list; } } if (showRecentlyInstalled && _relPath.isEmpty() && !newInstalledPrograms.isEmpty()) { AppNode *newnode = new AppNode(); newnode->icon = QIcon::fromTheme("chronometer"); newnode->appName = i18n("Recently Installed"); newnode->relPath = "new/"; newnode->isDir = true; newnode->parent = node; node->children.prepend(newnode); } // set the subTitleMandatory field for nodes that do not provide a unique generic // name what may help us on display to show in such cases also the subtitle to // provide a hint to the user what the duplicate entries are about. foreach (const QList &list, genericNames) { if (list.count() > 1) { foreach (AppNode* n, list) { n->subTitleMandatory = true; } } } } ApplicationModel::ApplicationModel(QObject *parent, bool allowSeparators) : KickoffAbstractModel(parent), d(new ApplicationModelPrivate(this, allowSeparators)) { QHash roles; roles[Qt::DisplayRole] = "display"; roles[Qt::DecorationRole] = "decoration"; roles[Kickoff::SubTitleRole] = "subtitle"; roles[Kickoff::UrlRole] = "url"; roles[Kickoff::GroupNameRole] = "group"; setRoleNames(roles); QDBusConnection dbus = QDBusConnection::sessionBus(); (void)new KickoffAdaptor(this); QDBusConnection::sessionBus().registerObject("/kickoff", this); dbus.connect(QString(), "/kickoff", "org.kde.plasma", "reloadMenu", this, SLOT(reloadMenu())); connect(KSycoca::self(), SIGNAL(databaseChanged(QStringList)), this, SLOT(checkSycocaChange(QStringList))); // TODO: remove me again reloadMenu(); } ApplicationModel::~ApplicationModel() { delete d; } bool ApplicationModel::canFetchMore(const QModelIndex &parent) const { if (!parent.isValid()) return false; AppNode *node = static_cast(parent.internalPointer()); return node->isDir && !node->fetched; } // void ApplicationModel::setNameDisplayOrder(DisplayOrder displayOrder) // { // d->displayOrder = displayOrder; // } // // DisplayOrder ApplicationModel::nameDisplayOrder() const // { // return d->displayOrder; // } void ApplicationModel::setShowRecentlyInstalled(bool showRecentlyInstalled) { if (d->showRecentlyInstalled != showRecentlyInstalled) { d->showRecentlyInstalled = showRecentlyInstalled; reloadMenu(); } } bool ApplicationModel::showRecentlyInstalled() const { return d->showRecentlyInstalled; } bool ApplicationModel::sortAppsByName() const { return d->sortAppsByName; } void ApplicationModel::setSortAppsByName(bool sortAppsByName) { if (d->sortAppsByName != sortAppsByName) { d->sortAppsByName = sortAppsByName; reloadMenu(); emit sortAppsByNameChanged(); } } int ApplicationModel::columnCount(const QModelIndex &parent) const { Q_UNUSED(parent) return 1; } QVariant ApplicationModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) { return QVariant(); } AppNode *node = static_cast(index.internalPointer()); switch (role) { case Qt::DisplayRole: if (node->genericName.isEmpty()) { return node->appName; } else { return node->genericName; } case Kickoff::SubTitleRole: if (node->appName.isEmpty() || node->appName == node->genericName) { return QString(); //return empty string to avoid errors in QML assigning null to Text } else { return node->appName; } case Kickoff::UrlRole: if (node->isDir) { return QString::fromLatin1("applications://%1").arg(node->desktopEntry); } else { return node->desktopEntry; } break; case Kickoff::SubTitleMandatoryRole: return node->subTitleMandatory; break; case Kickoff::SeparatorRole: return node->isSeparator; break; case Qt::DecorationRole: return node->icon; break; case Kickoff::RelPathRole: return node->relPath; break; case Kickoff::IconNameRole: return node->iconName; break; default: ; } return QVariant(); } void ApplicationModel::fetchMore(const QModelIndex &parent) { if (!parent.isValid()) { return; } AppNode *node = static_cast(parent.internalPointer()); if (!node->isDir) { return; } // FIXME? QML's QQmlAdaptorModel (the "backend" used for VisualDataModel) always hard-resets // the model when the source model (this one) changes layout, which breaks kickoff's // navigation as opening a category for the first time calls this and then calls // layoutChanged() --> QML's VisualDataModel is reset, that in turn invalidates // VDM's rootIndex and when that happens, Kickoff is switched to the root level. // Disabling the layout signals makes Kickoff's navigation work properly. // emit layoutAboutToBeChanged(QList() << parent); d->fillNode(node->relPath, node); node->fetched = true; // changePersistentIndex(parent, parent); // emit layoutChanged(QList() << parent); } bool ApplicationModel::hasChildren(const QModelIndex &parent) const { if (!parent.isValid()) { return true; } AppNode *node = static_cast(parent.internalPointer()); return node->isDir; } QVariant ApplicationModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation != Qt::Horizontal || section != 0) { return QVariant(); } switch (role) { case Qt::DisplayRole: return i18n("All Applications"); break; default: return QVariant(); } } QModelIndex ApplicationModel::index(int row, int column, const QModelIndex &parent) const { if (row < 0 || column != 0) return QModelIndex(); AppNode *node = d->root; if (parent.isValid()) node = static_cast(parent.internalPointer()); if (row >= node->children.count()) return QModelIndex(); else return createIndex(row, 0, node->children.at(row)); } QModelIndex ApplicationModel::parent(const QModelIndex &index) const { if (!index.isValid()) { return QModelIndex(); } AppNode *node = static_cast(index.internalPointer()); if (node->parent->parent) { int id = node->parent->parent->children.indexOf(node->parent); if (id >= 0 && id < node->parent->parent->children.count()) { return createIndex(id, 0, node->parent); } } return QModelIndex(); } int ApplicationModel::rowCount(const QModelIndex &parent) const { if (!parent.isValid()) { return d->root->children.count(); } AppNode *node = static_cast(parent.internalPointer()); return node->children.count(); } void ApplicationModel::setDuplicatePolicy(DuplicatePolicy policy) { if (d->duplicatePolicy != policy) { d->duplicatePolicy = policy; reloadMenu(); } } void ApplicationModel::setSystemApplicationPolicy(SystemApplicationPolicy policy) { if (d->systemApplicationPolicy != policy) { d->systemApplicationPolicy = policy; reloadMenu(); } } void ApplicationModel::reloadMenu() { beginResetModel(); delete d->root; d->root = new AppNode(); createNewProgramList(); d->fillNode(QString(), d->root); endResetModel(); } void ApplicationModel::checkSycocaChange(const QStringList &changes) { if (changes.contains("services") || changes.contains("apps")) { reloadMenu(); } } ApplicationModel::DuplicatePolicy ApplicationModel::duplicatePolicy() const { return d->duplicatePolicy; } ApplicationModel::SystemApplicationPolicy ApplicationModel::systemApplicationPolicy() const { return d->systemApplicationPolicy; } void ApplicationModel::setApplet(Plasma::Applet *applet) { if (d->applet.data() != applet) { d->applet = applet; createNewProgramList(); } } void ApplicationModel::createNewProgramList() { if (!d->applet) { return; } d->newInstalledPrograms.clear(); if (!d->showRecentlyInstalled) { return; } //FIXME KConfigGroup kickoffrc = d->applet.data()->config(); foreach (const QString &it, kickoffrc.keyList()) { d->seenPrograms.insert(it, QDate::fromString(kickoffrc.readEntry(it), Qt::ISODate)); } bool initialize = (d->seenPrograms.isEmpty()); bool seenProgramsChanged = createNewProgramListForPath(QString()); if (initialize) { // on first start, set all entries' dates to empty (means: they are not new) for (QHash::Iterator it = d->seenPrograms.begin(); it != d->seenPrograms.end(); ++it) *it = QDate(); d->newInstalledPrograms.clear(); } if (seenProgramsChanged) { for (QHash::Iterator it = d->seenPrograms.begin(); it != d->seenPrograms.end(); ++it) { kickoffrc.writeEntry(it.key(), it.value().toString(Qt::ISODate)); } kickoffrc.sync(); } } bool ApplicationModel::createNewProgramListForPath(const QString &relPath) { bool seenProgramsChanged = false; KServiceGroup::Ptr group = KServiceGroup::group(relPath); if (!group || !group->isValid()) { return false; } const KServiceGroup::List list = group->entries(); KServiceGroup::List::ConstIterator it = list.begin(); for (; it != list.end(); ++it) { KSycocaEntry::Ptr e = (*it); if (e->isType(KST_KServiceGroup)) { KServiceGroup::Ptr g(KServiceGroup::Ptr(static_cast(e.data()))); if (!g->noDisplay()) { if (createNewProgramListForPath(g->relPath())) seenProgramsChanged = true; } } else if (e->isType(KST_KService)) { KService::Ptr s(KService::Ptr(static_cast(e.data()))); if (s->isApplication() && !s->noDisplay()) { QString shortStorageId = s->storageId().remove(".desktop"); QHash::Iterator it_find = d->seenPrograms.find(shortStorageId); if (it_find == d->seenPrograms.end()) { seenProgramsChanged = true; d->seenPrograms.insert(shortStorageId, QDate::currentDate()); if (!d->newInstalledPrograms.contains(s->storageId())) { d->newInstalledPrograms += s->storageId(); } } else { QDate date = it_find.value(); if (date.isValid()) { if (date.daysTo(QDate::currentDate()) < 3) { if (!d->newInstalledPrograms.contains(s->storageId())) { d->newInstalledPrograms += s->storageId(); } } else { seenProgramsChanged = true; (*it_find) = QDate(); // this entry is not new anymore } } } } } } return seenProgramsChanged; } int ApplicationModel::rowForModelIndex(const QModelIndex &index) const { return index.row(); } /** * FIXME This is a temporary workaround to map the icon names found * in the desktop directory files (from /usr/share/desktop-directories) * into the Oxygen icon names. (Only applies if the Gnome menu files * are also installed) * * This list was compiled from Kubuntu 7.04 with the gnome-menus * package present. * * This needs to be discussed on kde-core-devel and fixed */ QHash ApplicationModelPrivate::iconNameMap() { static QHash map; if (map.isEmpty()) { map.insert("gnome-util", "applications-accessories"); // accessibility Oxygen icon was missing when this list was compiled map.insert("accessibility-directory", "applications-other"); map.insert("gnome-devel", "applications-development"); map.insert("package_edutainment", "applications-education"); map.insert("gnome-joystick", "applications-games"); map.insert("gnome-graphics", "applications-graphics"); map.insert("gnome-globe", "applications-internet"); map.insert("gnome-multimedia", "applications-multimedia"); map.insert("gnome-applications", "applications-office"); map.insert("gnome-system", "applications-system"); } return map; } } // namespace Kickoff