[prev in list] [next in list] [prev in thread] [next in thread] 

List:       kde-commits
Subject:    [plasma-desktop] kcms/keys: User interface for adding launchers as global shortcuts
From:       Marco Martin <notmart () gmail ! com>
Date:       2016-08-04 13:04:15
Message-ID: E1bVIJr-0006rX-5E () code ! kde ! org
[Download RAW message or body]

Git commit 933c21ec44a7b88c37c8c4ed88b1cd653bd6ba0f by Marco Martin.
Committed on 04/08/2016 at 13:04.
Pushed by mart into branch 'master'.

User interface for adding launchers as global shortcuts

Summary:
add a menu to pick applications from the start menu structure to
add launchers to be usable as shortcuts.
If an application has jumplist options they will be available as actions.

still need a dbus action to tell the daemon to reload
and probably launchers vs runtime actions will have to be
separed in the ui as well, that may influence the daemon api

Reviewers: graesslin

Subscribers: plasma-devel

Tags: #plasma

Differential Revision: https://phabricator.kde.org/D2111

M  +2    -0    kcms/keys/CMakeLists.txt
M  +198  -24   kcms/keys/kglobalshortcutseditor.cpp
M  +71   -13   kcms/keys/kglobalshortcutseditor.ui
A  +77   -0    kcms/keys/select_application.ui

http://commits.kde.org/plasma-desktop/933c21ec44a7b88c37c8c4ed88b1cd653bd6ba0f

diff --git a/kcms/keys/CMakeLists.txt b/kcms/keys/CMakeLists.txt
index 5f8a372..0086b36 100644
--- a/kcms/keys/CMakeLists.txt
+++ b/kcms/keys/CMakeLists.txt
@@ -15,6 +15,7 @@ set(kcm_keys_PART_SRCS
 ki18n_wrap_ui( kcm_keys_PART_SRCS
     export_scheme_dialog.ui
     kglobalshortcutseditor.ui
+    select_application.ui
     select_scheme_dialog.ui )
 
 set(kglobalaccel_xml \
${KGLOBALACCEL_DBUS_INTERFACES_DIR}/kf5_org.kde.KGlobalAccel.xml) @@ -35,6 +36,7 @@ \
target_link_libraries(kcm_keys  KF5::KIOWidgets
     KF5::XmlGui
     KF5::KDELibs4Support
+    KF5::ItemModels
 )
 
 install(TARGETS kcm_keys  DESTINATION ${PLUGIN_INSTALL_DIR} )
diff --git a/kcms/keys/kglobalshortcutseditor.cpp \
b/kcms/keys/kglobalshortcutseditor.cpp index 8edb6f4..235dc4f 100644
--- a/kcms/keys/kglobalshortcutseditor.cpp
+++ b/kcms/keys/kglobalshortcutseditor.cpp
@@ -18,6 +18,7 @@
 #include "kglobalshortcutseditor.h"
 
 #include "ui_kglobalshortcutseditor.h"
+#include "ui_select_application.h"
 #include "export_scheme_dialog.h"
 #include "select_scheme_dialog.h"
 #include "globalshortcuts.h"
@@ -32,6 +33,12 @@
 #include <KMessageBox>
 #include <KStringHandler>
 #include <KLocalizedString>
+#include <KService>
+#include <KRecursiveFilterProxyModel>
+#include <KServiceGroup>
+#include <KDesktopFile>
+#include <KCategorizedSortFilterProxyModel>
+#include <KCategoryDrawer>
 
 #include <QStackedWidget>
 #include <QMenu>
@@ -144,33 +151,183 @@ public:
 
     KGlobalShortcutsEditor *q;
     Ui::KGlobalShortcutsEditor ui;
+    Ui::SelectApplicationDialog selectApplicationDialogUi;
+    QDialog *selectApplicationDialog;
     QStackedWidget *stack;
     KShortcutsEditor::ActionTypes actionTypes;
     QHash<QString, ComponentData*> components;
     QDBusConnection bus;
     QStandardItemModel *model;
+    KCategorizedSortFilterProxyModel *proxyModel;
 };
 
+void loadAppsCategory(KServiceGroup::Ptr group, QStandardItemModel *model, \
QStandardItem *item) +{
+    if (group && group->isValid()) {
+        KServiceGroup::List list = group->entries();
+
+        for( KServiceGroup::List::ConstIterator it = list.constBegin();
+             it != list.constEnd(); ++it) {
+            const KSycocaEntry::Ptr p = (*it);
+
+            if (p->isType(KST_KService)) {
+                const KService::Ptr service(static_cast<KService*>(p.data()));
+
+                if (!service->noDisplay()) {
+                    QString genericName = service->genericName();
+                    if (genericName.isNull()) {
+                        genericName = service->comment();
+                    }
+                    QString description;
+                    if (!service->genericName().isEmpty() && service->genericName() \
!= service->name()) { +                        description = service->genericName();
+                    } else if (!service->comment().isEmpty()) {
+                        description = service->comment();
+                    }
+
+                    QStandardItem *subItem = new \
QStandardItem(QIcon::fromTheme(service->icon()), service->name()); +                  \
subItem->setData(service->entryPath()); +                    if (item) {
+                        item->appendRow(subItem);
+                    } else {
+                        model->appendRow(subItem);
+                    }
+                }
+
+            } else if (p->isType(KST_KServiceGroup)) {
+                KServiceGroup::Ptr subGroup(static_cast<KServiceGroup*>(p.data()));
+
+                if (!subGroup->noDisplay() && subGroup->childCount() > 0) {
+                    if (item) {
+                        loadAppsCategory(subGroup, model, item);
+                    } else {
+                        QStandardItem *subItem = new \
QStandardItem(QIcon::fromTheme(subGroup->icon()), subGroup->caption()); +             \
model->appendRow(subItem); +                        loadAppsCategory(subGroup, model, \
subItem); +                    }
+                }
+            }
+        }
+    }
+}
 
 void KGlobalShortcutsEditor::KGlobalShortcutsEditorPrivate::initGUI()
 {
     ui.setupUi(q);
+    selectApplicationDialog = new QDialog();
+    selectApplicationDialogUi.setupUi(selectApplicationDialog);
     // Create a stacked widget.
     stack = new QStackedWidget(q);
-    q->layout()->addWidget(stack);
+    ui.currentComponentLayout->addWidget(stack);
+    //HACK to make those two un-alignable components, aligned
+    ui.componentLabel->setMinimumHeight(ui.lineEditSpacer->sizeHint().height());
+    ui.lineEditSpacer->setVisible(false);
+    ui.addButton->setIcon(QIcon::fromTheme("list-add"));
+    ui.removeButton->setIcon(QIcon::fromTheme("list-remove"));
+    ui.components->setCategoryDrawer(new KCategoryDrawer(ui.components));
+    ui.components->setModelColumn(0);
 
     // Connect our components
-    connect(ui.components, SIGNAL(activated(QString)),
-            q, SLOT(activateComponent(QString)));
+    connect(ui.components, &QListView::activated,
+            q, [this](const QModelIndex &index) {
+                QString name = proxyModel->data(index).toString();
+                q->activateComponent(name);
+            });
 
     // Build the menu
     QMenu *menu = new QMenu(q);
     menu->addAction( QIcon::fromTheme(QStringLiteral("document-import")), \
                i18n("Import Scheme..."), q, SLOT(importScheme()));
     menu->addAction( QIcon::fromTheme(QStringLiteral("document-export")), \
                i18n("Export Scheme..."), q, SLOT(exportScheme()));
     menu->addAction( i18n("Set All Shortcuts to None"), q, \
                SLOT(clearConfiguration()));
-    QAction *action = menu->addAction( \
                QIcon::fromTheme(QStringLiteral("edit-delete")), i18n("Remove \
                Component"));
-    connect(action, &QAction::triggered, [this]() {
-        QString name = ui.components->currentText();
+    
+    connect(ui.addButton, &QToolButton::clicked, [this]() {
+        if (!selectApplicationDialogUi.treeView->model()) {
+            KRecursiveFilterProxyModel *filterModel = new \
KRecursiveFilterProxyModel(selectApplicationDialogUi.treeView); +            \
QStandardItemModel *appModel = new \
QStandardItemModel(selectApplicationDialogUi.treeView); +            \
selectApplicationDialogUi.kfilterproxysearchline->setProxy(filterModel); +            \
filterModel->setSourceModel(appModel); +            \
appModel->setHorizontalHeaderLabels({i18n("Applications")}); +
+            loadAppsCategory(KServiceGroup::root(), appModel, nullptr);
+
+            selectApplicationDialogUi.treeView->setModel(filterModel);
+        }
+        selectApplicationDialog->show();
+    });
+
+    connect(selectApplicationDialog, &QDialog::accepted, [this]() {
+        if (selectApplicationDialogUi.treeView->selectionModel()->selectedIndexes().length() \
== 1) { +            const QString desktopPath = \
selectApplicationDialogUi.treeView->model()->data(selectApplicationDialogUi.treeView->selectionModel()->selectedIndexes().first(), \
Qt::UserRole+1).toString(); +
+            if (!desktopPath.isEmpty() &&QFile::exists(desktopPath) ) {
+                const QString desktopFile = desktopPath.split(QChar('/')).last();
+
+                if (!desktopPath.isEmpty()) {
+                    KDesktopFile sourceDF(desktopPath);
+                    KDesktopFile *destinationDF = \
sourceDF.copyTo(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) \
+ QStringLiteral("/kglobalaccel/") + desktopFile); +                    \
qWarning()<<QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + \
QStringLiteral("/kglobalaccel/") + desktopFile; +                    \
destinationDF->sync(); +                    //TODO: a DBUS call to tell the daemon to \
refresh desktop files +
+
+                    // Create a action collection for our current component:context
+                    KActionCollection *col = new KActionCollection(q, desktopFile);
+
+                    foreach(const QString &actionId, sourceDF.readActions()) {
+
+                        const QString friendlyName = \
sourceDF.actionGroup(actionId).readEntry(QStringLiteral("Name")); +                   \
QAction *action = col->addAction(actionId); +                        \
action->setProperty("isConfigurationAction", QVariant(true)); // see \
KAction::~KAction +                        \
action->setProperty("componentDisplayName", friendlyName); +                        \
action->setText(friendlyName); +
+                        KGlobalAccel::self()->setShortcut(action, \
QList<QKeySequence>()); +                        
+                        QStringList sequencesStrings = \
sourceDF.actionGroup(actionId).readEntry(QStringLiteral("X-KDE-Shortcuts"), \
QString()).split(QChar(',')); +                        QList<QKeySequence> sequences;
+                        if (sequencesStrings.length() > 0) {
+                            Q_FOREACH (const QString &seqString, sequencesStrings) {
+                                sequences.append(QKeySequence(seqString));
+                            }
+                        }
+
+                        if (sequences.count() > 0) {
+                            KGlobalAccel::self()->setDefaultShortcut(action, \
sequences); +                        }
+                    }
+                    //Global launch action
+                    {
+                        const QString friendlyName = i18n("Launch %1", \
sourceDF.readName()); +                        QAction *action = \
col->addAction(QStringLiteral("_launch")); +                        \
action->setProperty("isConfigurationAction", QVariant(true)); // see \
KAction::~KAction +                        \
action->setProperty("componentDisplayName", friendlyName); +                        \
action->setText(friendlyName); +
+                        KGlobalAccel::self()->setShortcut(action, \
QList<QKeySequence>()); +                        
+                        QStringList sequencesStrings = \
sourceDF.desktopGroup().readEntry(QStringLiteral("X-KDE-Shortcuts"), \
QString()).split(QChar(',')); +                        QList<QKeySequence> sequences;
+                        if (sequencesStrings.length() > 0) {
+                            Q_FOREACH (const QString &seqString, sequencesStrings) {
+                                sequences.append(QKeySequence(seqString));
+                            }
+                        }
+
+                        if (sequences.count() > 0) {
+                            KGlobalAccel::self()->setDefaultShortcut(action, \
sequences); +                        }
+                    }
+qWarning()<<"WWWWW"<<desktopFile<<sourceDF.readName();
+                    q->addCollection(col, QDBusObjectPath(), desktopFile, \
sourceDF.readName()); +                }
+            }
+        }
+    });
+
+    connect(ui.removeButton, &QToolButton::clicked, [this]() {
+        //TODO: different way to remove components that are desktop files
+        //disabled desktop files need Hidden=true key
+        QString name = proxyModel->data(ui.components->currentIndex()).toString();
         QString componentUnique = components.value(name)->uniqueName();
 
         // The confirmation text is different when the component is active
@@ -213,7 +370,8 @@ void \
KGlobalShortcutsEditor::KGlobalShortcutsEditorPrivate::initGUI()  
     ui.menu_button->setMenu(menu);
 
-    QSortFilterProxyModel *proxyModel = new QSortFilterProxyModel(q);
+    proxyModel = new KCategorizedSortFilterProxyModel(q);
+    proxyModel->setCategorizedModel(true);
     model = new QStandardItemModel(0, 1, proxyModel);
     proxyModel->setSourceModel(model);
     proxyModel->setSortCaseSensitivity(Qt::CaseInsensitive);
@@ -235,6 +393,7 @@ KGlobalShortcutsEditor::~KGlobalShortcutsEditor()
 {
     // Before closing the door, undo all changes
     undo();
+    delete d->selectApplicationDialog;
     qDeleteAll(d->components);
     delete d;
 }
@@ -247,11 +406,11 @@ void KGlobalShortcutsEditor::activateComponent(const QString \
&component)  Q_ASSERT(iter != d->components.end());
         return;
     } else {
-        int index = d->ui.components->findText(component);
-        Q_ASSERT(index != -1);
-        if (index > -1) {
+        QModelIndexList results = d->proxyModel->match(d->proxyModel->index(0, 0), \
Qt::DisplayRole, component); +        Q_ASSERT(results.isEmpty());
+        if (results.first().isValid()) {
             // Known component. Get it.
-            d->ui.components->setCurrentIndex(index);
+            d->ui.components->setCurrentIndex(results.first());
             d->stack->setCurrentWidget((*iter)->editor());
         }
     }
@@ -275,15 +434,29 @@ void KGlobalShortcutsEditor::addCollection(
         // try to find one appropriate icon ( allowing NULL pixmap to be returned)
         QPixmap pixmap = KIconLoader::global()->loadIcon(id, KIconLoader::Small, 0,
                                   KIconLoader::DefaultState, QStringList(), 0, \
true); +        if (pixmap.isNull()) {
+            KService::Ptr service = KService::serviceByStorageId(id);
+            if(service) {
+                pixmap = KIconLoader::global()->loadIcon(service->icon(), \
KIconLoader::Small, 0, +                                  KIconLoader::DefaultState, \
QStringList(), 0, true); +            }
+        }
         // if NULL pixmap is returned, use the F.D.O "system-run" icon
         if (pixmap.isNull()) {
             pixmap = KIconLoader::global()->loadIcon(QStringLiteral("system-run"), \
KIconLoader::Small);  }
 
-        // Add to the component combobox
-        //FIXME: QCombobox.addItem apparently breaks with sort() in Qt5
-        d->model->appendRow(new QStandardItem(pixmap, friendlyName));
-        d->ui.components->model()->sort(0);
+        // Add to the component list
+        QStandardItem *item = new QStandardItem(pixmap, friendlyName);
+        if (id.endsWith(QStringLiteral(".desktop"))) {
+            item->setData(i18n("Application Launchers"), \
KCategorizedSortFilterProxyModel::CategoryDisplayRole); +            item->setData(0, \
KCategorizedSortFilterProxyModel::CategorySortRole); +        } else {
+            item->setData(i18n("Other Shortcuts"), \
KCategorizedSortFilterProxyModel::CategoryDisplayRole); +            item->setData(1, \
KCategorizedSortFilterProxyModel::CategorySortRole); +        }
+        d->model->appendRow(item);
+        d->proxyModel->sort(0);
 
         // Add to our component registry
         ComponentData *cd = new ComponentData(id, objectPath, editor);
@@ -298,16 +471,17 @@ void KGlobalShortcutsEditor::addCollection(
     // Add the collection to the editor of the component
     editor->addCollection(collection, friendlyName);
 
-    if (d->ui.components->count() > -1) {
-        d->ui.components->setCurrentIndex(0);
-        activateComponent(d->ui.components->itemText(0));
+    if (d->proxyModel->rowCount() > -1) {
+        d->ui.components->setCurrentIndex(d->proxyModel->index(0, 0));
+        QString name = d->proxyModel->data(d->proxyModel->index(0, 0)).toString();
+        activateComponent(name);
     }
 }
 
 
 void KGlobalShortcutsEditor::clearConfiguration()
 {
-    QString name = d->ui.components->currentText();
+    QString name = d->proxyModel->data(d->ui.components->currentIndex()).toString();
     d->components[name]->editor()->clearConfiguration();
 }
 
@@ -324,7 +498,7 @@ void KGlobalShortcutsEditor::defaults(ComponentScope scope)
             break;
 
         case CurrentComponent: {
-            QString name = d->ui.components->currentText();
+            QString name = \
d->proxyModel->data(d->ui.components->currentIndex()).toString();  // The editors are \
responsible for the reset  d->components[name]->editor()->allDefault();
             }
@@ -341,7 +515,7 @@ void KGlobalShortcutsEditor::clear()
     // Remove all components and their associated editors
     qDeleteAll(d->components);
     d->components.clear();
-    d->ui.components->clear();
+    d->model->clear();
 }
 
 
@@ -659,9 +833,9 @@ void \
KGlobalShortcutsEditor::KGlobalShortcutsEditorPrivate::removeComponent(  if \
(components.value(text)->uniqueName() == componentUnique)  {
             // Remove from QComboBox
-            int index = ui.components->findText(text);
-            Q_ASSERT(index != -1);
-            ui.components->removeItem(index);
+            QModelIndexList results = proxyModel->match(proxyModel->index(0, 0), \
Qt::DisplayRole, text); +            Q_ASSERT(results.isEmpty());
+            model->removeRow(proxyModel->mapToSource(results.first()).row());
 
             // Remove from QStackedWidget
             stack->removeWidget(components[text]->editor());
diff --git a/kcms/keys/kglobalshortcutseditor.ui \
b/kcms/keys/kglobalshortcutseditor.ui index 444103e..bc1a544 100644
--- a/kcms/keys/kglobalshortcutseditor.ui
+++ b/kcms/keys/kglobalshortcutseditor.ui
@@ -6,30 +6,89 @@
    <rect>
     <x>0</x>
     <y>0</y>
-    <width>612</width>
-    <height>516</height>
+    <width>660</width>
+    <height>572</height>
    </rect>
   </property>
-  <layout class="QVBoxLayout" name="verticalLayout">
-   <item>
-    <layout class="QHBoxLayout" name="horizontalLayout">
+  <layout class="QGridLayout" name="gridLayout">
+   <item row="0" column="0">
+    <layout class="QHBoxLayout" name="horizontalLayout_2">
      <item>
-      <widget class="QLabel" name="label">
+      <widget class="QLabel" name="componentLabel">
+       <property name="sizePolicy">
+        <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+         <horstretch>0</horstretch>
+         <verstretch>0</verstretch>
+        </sizepolicy>
+       </property>
        <property name="text">
-        <string>KDE component:</string>
+        <string>Component:</string>
        </property>
       </widget>
      </item>
      <item>
-      <widget class="KComboBox" name="components">
+      <widget class="QLineEdit" name="lineEditSpacer">
        <property name="sizePolicy">
-        <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+        <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
          <horstretch>0</horstretch>
          <verstretch>0</verstretch>
         </sizepolicy>
        </property>
       </widget>
      </item>
+    </layout>
+   </item>
+   <item row="0" column="1" rowspan="2">
+    <layout class="QVBoxLayout" name="currentComponentLayout"/>
+   </item>
+   <item row="1" column="0">
+    <widget class="KCategorizedView" name="components">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Minimum" vsizetype="Expanding">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+     <property name="verticalScrollMode">
+      <enum>QAbstractItemView::ScrollPerPixel</enum>
+     </property>
+    </widget>
+   </item>
+   <item row="2" column="0" colspan="2">
+    <layout class="QHBoxLayout" name="horizontalLayout">
+     <item>
+      <widget class="QToolButton" name="addButton">
+       <property name="toolTip">
+        <string>Add a new shortcut to an Application...</string>
+       </property>
+       <property name="text">
+        <string>...</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QToolButton" name="removeButton">
+       <property name="toolTip">
+        <string>Remove the selected component</string>
+       </property>
+       <property name="text">
+        <string>...</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <spacer name="horizontalSpacer">
+       <property name="orientation">
+        <enum>Qt::Horizontal</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>40</width>
+         <height>20</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
      <item>
       <widget class="QPushButton" name="menu_button">
        <property name="text">
@@ -43,13 +102,12 @@
  </widget>
  <customwidgets>
   <customwidget>
-   <class>KComboBox</class>
-   <extends>QComboBox</extends>
-   <header>kcombobox.h</header>
+   <class>KCategorizedView</class>
+   <extends>QListView</extends>
+   <header>kcategorizedview.h</header>
   </customwidget>
  </customwidgets>
  <tabstops>
-  <tabstop>components</tabstop>
   <tabstop>menu_button</tabstop>
  </tabstops>
  <resources/>
diff --git a/kcms/keys/select_application.ui b/kcms/keys/select_application.ui
new file mode 100644
index 0000000..74a27b4
--- /dev/null
+++ b/kcms/keys/select_application.ui
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>SelectApplicationDialog</class>
+ <widget class="QDialog" name="SelectApplicationDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>410</width>
+    <height>421</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Select Application</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <widget class="KFilterProxySearchLine" name="kfilterproxysearchline"/>
+   </item>
+   <item>
+    <widget class="QTreeView" name="treeView"/>
+   </item>
+   <item>
+    <widget class="QDialogButtonBox" name="buttonBox">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+     <property name="standardButtons">
+      <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>KFilterProxySearchLine</class>
+   <extends>QWidget</extends>
+   <header>kfilterproxysearchline.h</header>
+  </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>accepted()</signal>
+   <receiver>SelectApplicationDialog</receiver>
+   <slot>accept()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>248</x>
+     <y>254</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>157</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>rejected()</signal>
+   <receiver>SelectApplicationDialog</receiver>
+   <slot>reject()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>316</x>
+     <y>260</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>286</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>


[prev in list] [next in list] [prev in thread] [next in thread] 

Configure | About | News | Add a list | Sponsored by KoreLogic