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.
*/
#include "applicationmodel.h"
#include <QtCore/QtAlgorithms>
#include <QDebug>
#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 <QIcon>
#include <assert.h>
#include <stdlib.h>
#include <kconfiggroup.h>
#include "kickoffadaptor.h"
#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),
displayOrder(NameAfterDescription),
allowSeparators(_allowSeparators),
showRecentlyInstalled(true),
sortAppsByName(false)
{
systemApplications = Kickoff::systemApplicationList();
}
~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;
bool sortAppsByName;
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 = 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 ,
true ,
allowSeparators ,
!sortAppsByName );
QHash<QString, KService::Ptr> existingServices;
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 = KService::Ptr(static_cast<KService*>(p.data()));
if (service->noDisplay()) {
continue;
}
icon = service->icon();
appName = service->name();
genericName = service->genericName();
desktopEntry = service->entryPath();
if (duplicatePolicy == ApplicationModel::ShowLatestOnlyPolicy &&
existingServices.contains(appName)) {
if (Kickoff::isLaterVersion(existingServices[appName], service)) {
continue;
} else {
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())) {
continue;
}
existingServices[appName] = service;
} else if (p->isType(KST_KServiceGroup)) {
const KServiceGroup::Ptr serviceGroup = KServiceGroup::Ptr(static_cast<KServiceGroup*>(p.data()));
if (serviceGroup->noDisplay() || serviceGroup->childCount() == 0) {
continue;
}
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<AppNode*> 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);
}
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)));
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::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<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();
} 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;
}
d->fillNode(node->relPath, node);
node->fetched = true;
}
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::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;
}
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) {
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(KServiceGroup::Ptr(static_cast<KServiceGroup*>(e.data())));
if (!g->noDisplay()) {
if (createNewProgramListForPath(g->relPath()))
seenProgramsChanged = true;
}
} else if (e->isType(KST_KService)) {
KService::Ptr s(KService::Ptr(static_cast<KService*>(e.data())));
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();
}
}
}
}
}
}
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<QString, QString> ApplicationModelPrivate::iconNameMap()
{
static QHash<QString, QString> map;
if (map.isEmpty()) {
map.insert("gnome-util", "applications-accessories");
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;
}
}