mirror-kde-plasma-desktop/applets/kickoff/core/applicationmodel.cpp
Aleix Pol 88ae0b60b8 Initial import from the monolithic kde-workspace.
This is the beginning of revision history for this module. If you
want to look at revision history older than this, please refer to the
techbase wiki for how to use Git history grafting. At the time of
writing, this wiki is located here:

http://community.kde.org/Frameworks/GitOldHistory

If you have already performed the grafting and you don't see any
history beyond this commit, try running "git log" with the "--follow"
argument.

Branched from the monolithic repo kde-workspace, frameworks  branch, at commit
049113e719dd2fc4446d054fa1a3aada330094f0
2014-03-31 04:23:39 +02:00

696 lines
21 KiB
C++

/*
Copyright 2007 Pino Toscano <pino@kde.org>
Copyright 2007 Robert Knight <robertknight@gmail.com>
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 <QtCore/QtAlgorithms>
#include <QDebug>
// KDE
#include <kauthorized.h>
#include <khistorycombobox.h>
#include <kdesktopfile.h>
#include <klineedit.h>
#include <klocale.h>
#include <kiconloader.h>
#include <krun.h>
#include <kstandarddirs.h>
#include <kstringhandler.h>
#include <kmimetypetrader.h>
#include <kurlcompletion.h>
#include <kmimetype.h>
#include <kservicegroup.h>
#include <ksycoca.h>
#include <kglobalsettings.h>
#include <KLocalizedString>
#include <KIcon>
#include <assert.h>
#include <stdlib.h>
#include <kconfiggroup.h>
#include "kickoffadaptor.h"
// Local
#include "models.h"
#include <Plasma/Applet>
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<AppNode*> 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)
{
systemApplications = Kickoff::systemApplicationList();
reloadTimer = new QTimer(qq);
reloadTimer->setSingleShot(true);
QObject::connect(reloadTimer, SIGNAL(timeout()), qq, SLOT(delayedReloadMenu()));
}
~ApplicationModelPrivate()
{
delete root;
}
void fillNode(const QString &relPath, AppNode *node);
static QHash<QString, QString> iconNameMap();
ApplicationModel *q;
QWeakPointer<Plasma::Applet> applet;
AppNode *root;
ApplicationModel::DuplicatePolicy duplicatePolicy;
ApplicationModel::SystemApplicationPolicy systemApplicationPolicy;
QStringList systemApplications;
DisplayOrder displayOrder;
bool allowSeparators;
bool showRecentlyInstalled;
QTimer *reloadTimer;
QStringList newInstalledPrograms;
QHash<QString, QDate> 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 = KIcon(p->icon());
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 */,
true /* sort by generic name */);
// application name <-> service map for detecting duplicate entries
QHash<QString, KService::Ptr> existingServices;
// generic name <-> node mapping to determinate duplicate generic names
QHash<QString,QList<AppNode*> > 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 = p;
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<AppNode*> 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 = p;
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 = KIcon(icon);
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<AppNode*> list = genericNames.value(s);
list.append(newnode);
genericNames[s] = list;
}
}
if (showRecentlyInstalled && _relPath.isEmpty() && !newInstalledPrograms.isEmpty()) {
AppNode *newnode = new AppNode();
newnode->icon = KIcon("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<AppNode*> &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<int, QByteArray> 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<AppNode*>(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;
}
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<AppNode*>(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<AppNode*>(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<QPersistentModelIndex>() << parent);
d->fillNode(node->relPath, node);
node->fetched = true;
// changePersistentIndex(parent, parent);
// emit layoutChanged(QList<QPersistentModelIndex>() << parent);
}
bool ApplicationModel::hasChildren(const QModelIndex &parent) const
{
if (!parent.isValid()) {
return true;
}
AppNode *node = static_cast<AppNode*>(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<AppNode*>(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<AppNode*>(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<AppNode*>(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::delayedReloadMenu()
{
if (!d->reloadTimer->isActive()) {
d->reloadTimer->start(200);
}
}
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<QString, QDate>::Iterator it = d->seenPrograms.begin(); it != d->seenPrograms.end(); ++it)
*it = QDate();
d->newInstalledPrograms.clear();
}
if (seenProgramsChanged) {
for (QHash<QString, QDate>::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(e);
if (!g->noDisplay()) {
if (createNewProgramListForPath(g->relPath()))
seenProgramsChanged = true;
}
} else if (e->isType(KST_KService)) {
KService::Ptr s(e);
if (s->isApplication() && !s->noDisplay()) {
QString shortStorageId = s->storageId().remove(".desktop");
QHash<QString, QDate>::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;
}
/**
* 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<QString, QString> ApplicationModelPrivate::iconNameMap()
{
static QHash<QString, QString> 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
#include "applicationmodel.moc"