[plasma/plasma-workspace] klipper: Klipper: Tidy up the "Actions" configuration page and editing

Jonathan Marten null at kde.org
Wed May 18 14:46:15 BST 2022


Git commit 3dfe82e69c26979a84aeb6c5270bbe5d9e921a0f by Jonathan Marten.
Committed on 18/05/2022 at 13:46.
Pushed by marten into branch 'master'.

Klipper: Tidy up the "Actions" configuration page and editing

This is the remaining part of the Klipper configuration dialogue,
the "Actions Configuration" page.  Now that the options have been
moved to the "Popup" page, this is a simple tree view with action
buttons.  The explanations and help links have been moved to the
"Edit Action" page, because that is where they are most applicable.

The "Action Properties" dialogue uses a QFormLayout, and includes
explanation text for the "Automatic" check box and the regexp help link.

Instead of editing in place, the "Edit" button opens a further dialogue
to edit the current entry.  This is more discoverable and should be easier
on small displays than editing in place, although a double click is still
accepted.  This dialogue has explanation text for substitutions.

The icon for a command can be set explicitly, for those cases where the
automatic detection (from the first word of the command) does not work.

Confirmation is requested when deleting a command or an action.

GUI:
I18N:

M  +2    -1    klipper/CMakeLists.txt
D  +0    -90   klipper/actionsconfig.ui
M  +150  -85   klipper/configdialog.cpp
M  +14   -5    klipper/configdialog.h
M  +198  -161  klipper/editactiondialog.cpp
M  +15   -8    klipper/editactiondialog.h
D  +0    -220  klipper/editactiondialog.ui
A  +186  -0    klipper/editcommanddialog.cpp     [License: GPL(v2.0+)]
A  +59   -0    klipper/editcommanddialog.h     [License: GPL(v2.0+)]
M  +5    -0    klipper/klipper.cpp
M  +7    -1    klipper/klipper.kcfg

https://invent.kde.org/plasma/plasma-workspace/commit/3dfe82e69c26979a84aeb6c5270bbe5d9e921a0f

diff --git a/klipper/CMakeLists.txt b/klipper/CMakeLists.txt
index 825533db2..c7a3eea24 100644
--- a/klipper/CMakeLists.txt
+++ b/klipper/CMakeLists.txt
@@ -17,6 +17,7 @@ set(libklipper_common_SRCS
     historyurlitem.cpp
     actionstreewidget.cpp
     editactiondialog.cpp
+    editcommanddialog.cpp
     clipcommandprocess.cpp
     utils.cpp
 )
@@ -25,7 +26,6 @@ ecm_qt_declare_logging_category(libklipper_common_SRCS HEADER klipper_debug.h ID
 
 configure_file(config-klipper.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-klipper.h )
 
-ki18n_wrap_ui(libklipper_common_SRCS actionsconfig.ui editactiondialog.ui)
 kconfig_add_kcfg_files(libklipper_common_SRCS klippersettings.kcfgc)
 
 add_library(libklipper_common_static STATIC ${libklipper_common_SRCS})
@@ -48,6 +48,7 @@ target_link_libraries(libklipper_common_static
     KF5::WidgetsAddons
     KF5::XmlGui
     KF5::WaylandClient
+    KF5::IconThemes
     ${ZLIB_LIBRARY})
 if (X11_FOUND)
     target_link_libraries(libklipper_common_static XCB::XCB)
diff --git a/klipper/actionsconfig.ui b/klipper/actionsconfig.ui
deleted file mode 100644
index ac2760219..000000000
--- a/klipper/actionsconfig.ui
+++ /dev/null
@@ -1,90 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<ui version="4.0">
- <class>ActionsWidget</class>
- <widget class="QWidget" name="ActionsWidget">
-  <property name="geometry">
-   <rect>
-    <x>0</x>
-    <y>0</y>
-    <width>458</width>
-    <height>360</height>
-   </rect>
-  </property>
-  <layout class="QVBoxLayout" name="verticalLayout">
-   <item>
-    <widget class="QLabel" name="label">
-     <property name="text">
-      <string>Action list:</string>
-     </property>
-    </widget>
-   </item>
-   <item>
-    <widget class="ActionsTreeWidget" name="kcfg_ActionList">
-     <property name="contextMenuPolicy">
-      <enum>Qt::CustomContextMenu</enum>
-     </property>
-     <column>
-      <property name="text">
-       <string>Regular Expression</string>
-      </property>
-     </column>
-     <column>
-      <property name="text">
-       <string>Description</string>
-      </property>
-     </column>
-    </widget>
-   </item>
-   <item>
-    <layout class="QHBoxLayout" name="horizontalLayout">
-     <item>
-      <widget class="QPushButton" name="pbAddAction">
-       <property name="text">
-        <string>Add Action...</string>
-       </property>
-      </widget>
-     </item>
-     <item>
-      <widget class="QPushButton" name="pbEditAction">
-       <property name="text">
-        <string>Edit Action...</string>
-       </property>
-      </widget>
-     </item>
-     <item>
-      <widget class="QPushButton" name="pbDelAction">
-       <property name="text">
-        <string>Delete Action</string>
-       </property>
-      </widget>
-     </item>
-    </layout>
-   </item>
-   <item>
-    <widget class="QLabel" name="label_2">
-     <property name="text">
-      <string>Click on a highlighted item's column to change it. "%s" in a command will be replaced with the clipboard contents.<br>For more information about regular expressions, you could have a look at the <a href="https://en.wikipedia.org/wiki/Regular_expression">Wikipedia entry about this topic</a>.</string>
-     </property>
-     <property name="textFormat">
-      <enum>Qt::RichText</enum>
-     </property>
-     <property name="openExternalLinks">
-      <bool>true</bool>
-     </property>
-     <property name="wordWrap">
-      <bool>true</bool>
-     </property>
-    </widget>
-   </item>
-  </layout>
- </widget>
- <customwidgets>
-  <customwidget>
-   <class>ActionsTreeWidget</class>
-   <extends>QTreeWidget</extends>
-   <header>actionstreewidget.h</header>
-  </customwidget>
- </customwidgets>
- <resources/>
- <connections/>
-</ui>
diff --git a/klipper/configdialog.cpp b/klipper/configdialog.cpp
index 32c62a588..5262a595f 100644
--- a/klipper/configdialog.cpp
+++ b/klipper/configdialog.cpp
@@ -11,6 +11,10 @@
 #include <qcheckbox.h>
 #include <qfontdatabase.h>
 #include <qformlayout.h>
+#include <qgridlayout.h>
+#include <qheaderview.h>
+#include <qlabel.h>
+#include <qpushbutton.h>
 #include <qradiobutton.h>
 #include <qtooltip.h>
 #include <qwindow.h>
@@ -20,17 +24,19 @@
 #include <KShortcutsEditor>
 #include <kconfigskeleton.h>
 #include <kglobalaccel.h>
+#include <kmessagebox.h>
+#include <kmessagewidget.h>
 #include <kpluralhandlingspinbox.h>
 #include <kwindowconfig.h>
-#include <kmessagewidget.h>
 
 #include "klipper_debug.h"
 
+#include "actionstreewidget.h"
 #include "editactiondialog.h"
 #include "klipper.h"
 #include "klippersettings.h"
 
-static QLabel *createHintLabel(const QString &text, QWidget *parent)
+/* static */ QLabel *ConfigDialog::createHintLabel(const QString &text, QWidget *parent)
 {
     QLabel *hintLabel = new QLabel(text, parent);
     hintLabel->setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont));
@@ -47,11 +53,17 @@ static QLabel *createHintLabel(const QString &text, QWidget *parent)
     return (hintLabel);
 }
 
-static QLabel *createHintLabel(const KConfigSkeletonItem *item, QWidget *parent)
+/* static */ QLabel *ConfigDialog::createHintLabel(const KConfigSkeletonItem *item, QWidget *parent)
 {
     return (createHintLabel(item->whatsThis(), parent));
 }
 
+/* static */ QString ConfigDialog::manualShortcutString()
+{
+    const QList<QKeySequence> keys = KGlobalAccel::self()->globalShortcut(QCoreApplication::applicationName(), QStringLiteral("repeat_action"));
+    return (keys.value(0).toString(QKeySequence::NativeText));
+}
+
 //////////////////////////
 //  GeneralWidget	//
 //////////////////////////
@@ -67,7 +79,7 @@ GeneralWidget::GeneralWidget(QWidget *parent)
     m_syncClipboardsCb->setChecked(KlipperSettings::syncClipboards());
     layout->addRow(i18n("Selection and Clipboard:"), m_syncClipboardsCb);
 
-    QLabel *hint = createHintLabel(item, this);
+    QLabel *hint = ConfigDialog::createHintLabel(item, this);
     layout->addRow(QString(), hint);
     connect(hint, &QLabel::linkActivated, this, [this, hint]() {
         QToolTip::showText(QCursor::pos(),
@@ -114,19 +126,19 @@ If it is turned off, the selection may still be saved in the clipboard history (
     // is turned off - in this case the selection is never automatically saved
     // in the clipboard history.
 
-    QButtonGroup *bg = new QButtonGroup(this);
+    QButtonGroup *buttonGroup = new QButtonGroup(this);
 
     m_alwaysTextRb = new QRadioButton(i18n("Always save in history"), this);
     m_alwaysTextRb->setChecked(!KlipperSettings::ignoreSelection());
-    bg->addButton(m_alwaysTextRb);
+    buttonGroup->addButton(m_alwaysTextRb);
     layout->addRow(i18n("Text selection:"), m_alwaysTextRb);
 
     m_copiedTextRb = new QRadioButton(i18n("Only when explicitly copied"), this);
     m_copiedTextRb->setChecked(KlipperSettings::ignoreSelection());
-    bg->addButton(m_copiedTextRb);
+    buttonGroup->addButton(m_copiedTextRb);
     layout->addRow(QString(), m_copiedTextRb);
 
-    layout->addRow(QString(), createHintLabel(i18n("Whether text selections are saved in the clipboard history."), this));
+    layout->addRow(QString(), ConfigDialog::createHintLabel(i18n("Whether text selections are saved in the clipboard history."), this));
 
     // Radio button group: Storing non-text selections in history
     //
@@ -149,24 +161,24 @@ If it is turned off, the selection may still be saved in the clipboard history (
     // The 'Always' option is not available if selection/clipboard synchronisation
     // is turned off.
 
-    bg = new QButtonGroup(this);
+    buttonGroup = new QButtonGroup(this);
 
     m_alwaysImageRb = new QRadioButton(i18n("Always save in history"), this);
     m_alwaysImageRb->setChecked(!KlipperSettings::ignoreImages() && !KlipperSettings::selectionTextOnly());
-    bg->addButton(m_alwaysImageRb);
+    buttonGroup->addButton(m_alwaysImageRb);
     layout->addRow(i18n("Non-text selection:"), m_alwaysImageRb);
 
     m_copiedImageRb = new QRadioButton(i18n("Only when explicitly copied"), this);
     m_copiedImageRb->setChecked(!KlipperSettings::ignoreImages() && KlipperSettings::selectionTextOnly());
-    bg->addButton(m_copiedImageRb);
+    buttonGroup->addButton(m_copiedImageRb);
     layout->addRow(QString(), m_copiedImageRb);
 
     m_neverImageRb = new QRadioButton(i18n("Never save in history"), this);
     m_neverImageRb->setChecked(KlipperSettings::ignoreImages());
-    bg->addButton(m_neverImageRb);
+    buttonGroup->addButton(m_neverImageRb);
     layout->addRow(QString(), m_neverImageRb);
 
-    layout->addRow(QString(), createHintLabel(i18n("Whether non-text selections (such as images) are saved in the clipboard history."), this));
+    layout->addRow(QString(), ConfigDialog::createHintLabel(i18n("Whether non-text selections (such as images) are saved in the clipboard history."), this));
 
     layout->addRow(QString(), new QLabel(this));
 
@@ -242,14 +254,15 @@ PopupWidget::PopupWidget(QWidget *parent)
     layout->addRow(QString(), m_historyPopupCb);
 
     const QList<QKeySequence> keys = KGlobalAccel::self()->globalShortcut(QCoreApplication::applicationName(), QStringLiteral("repeat_action"));
-    layout->addRow(QString(),
-                   createHintLabel(xi18nc("@info",
-                                          "When text that matches an action pattern is selected or is chosen from \
+    QLabel *hint = ConfigDialog::createHintLabel(xi18nc("@info",
+                                                        "When text that matches an action pattern is selected or is chosen from \
 the clipboard history, automatically show the popup menu with applicable actions. \
 If the automatic menu is turned off here, or it is not shown for an excluded window, \
 then it can be shown by using the <shortcut>%1</shortcut> key shortcut.",
-                                          keys.value(0).toString(QKeySequence::NativeText)),
-                                   this));
+                                                        ConfigDialog::manualShortcutString()),
+                                                 this);
+    layout->addRow(QString(), hint);
+
     // Exclusions
     QPushButton *exclusionsButton = new QPushButton(QIcon::fromTheme(QStringLiteral("configure")), i18n("Exclude Windows..."), this);
     connect(exclusionsButton, &QPushButton::clicked, this, &PopupWidget::onAdvanced);
@@ -278,20 +291,19 @@ then it can be shown by using the <shortcut>%1</shortcut> key shortcut.",
     m_stripWhitespaceCb = new QCheckBox(item->label(), this);
     m_stripWhitespaceCb->setChecked(KlipperSettings::stripWhiteSpace());
     layout->addRow(i18n("Options:"), m_stripWhitespaceCb);
-    layout->addRow(QString(), createHintLabel(item, this));
+    layout->addRow(QString(), ConfigDialog::createHintLabel(item, this));
 
     // MIME actions
     item = KlipperSettings::self()->enableMagicMimeActionsItem();
     m_mimeActionsCb = new QCheckBox(item->label(), this);
     m_mimeActionsCb->setChecked(KlipperSettings::enableMagicMimeActions());
     layout->addRow(QString(), m_mimeActionsCb);
-    layout->addRow(QString(), createHintLabel(item, this));
+    layout->addRow(QString(), ConfigDialog::createHintLabel(item, this));
 
     layout->addRow(QString(), new QLabel(this));
 
     // Where to configure the actions
-    if (!KlipperSettings::popupInfoMessageHidden())
-    {
+    if (KlipperSettings::popupInfoMessageShown()) {
         KMessageWidget *msg = new KMessageWidget(xi18nc("@info", "The actions shown in the popup menu \
 can be configured on the <interface>Actions Configuration</interface> page."), this);
         msg->setMessageType(KMessageWidget::Information);
@@ -300,7 +312,7 @@ can be configured on the <interface>Actions Configuration</interface> page."), t
         msg->setCloseButtonVisible(true);
 
         connect(msg, &KMessageWidget::hideAnimationFinished, this, []() {
-            KlipperSettings::setPopupInfoMessageHidden(true);
+            KlipperSettings::setPopupInfoMessageShown(false);
         });
         layout->addRow(msg);
     }
@@ -355,29 +367,71 @@ void PopupWidget::save()
 
 ActionsWidget::ActionsWidget(QWidget *parent)
     : QWidget(parent)
-    , m_editActDlg(nullptr)
 {
-    m_ui.setupUi(this);
+    QGridLayout *layout = new QGridLayout(this);
+    layout->setContentsMargins(0, 0, 0, 0);
+
+    // General information label
+    QLabel *hint = ConfigDialog::createHintLabel(xi18nc("@info",
+                                                        "Whan a <interface>match pattern</interface> \
+matches the clipboard contents, its <interface>commands</interface> \
+appear in the Klipper popup menu and can be executed."),
+                                                 this);
+    layout->addWidget(hint, 0, 0, 1, -1);
+
+    // Scrolling list
+    m_actionsTree = new ActionsTreeWidget(this);
+    m_actionsTree->setColumnCount(2);
+    m_actionsTree->setHeaderLabels({i18nc("@title:column", "Match pattern and commands"), i18nc("@title:column", "Description")});
+
+    layout->addWidget(m_actionsTree, 1, 0, 1, -1);
+    layout->setRowStretch(1, 1);
+
+    // Action buttons
+    m_addActionButton = new QPushButton(QIcon::fromTheme(QStringLiteral("list-add")), i18n("Add Action..."), this);
+    connect(m_addActionButton, &QPushButton::clicked, this, &ActionsWidget::onAddAction);
+    layout->addWidget(m_addActionButton, 2, 0);
+
+    m_editActionButton = new QPushButton(QIcon::fromTheme(QStringLiteral("document-edit")), i18n("Edit Action..."), this);
+    connect(m_editActionButton, &QPushButton::clicked, this, &ActionsWidget::onEditAction);
+    layout->addWidget(m_editActionButton, 2, 1);
+    layout->setColumnStretch(2, 1);
+
+    m_deleteActionButton = new QPushButton(QIcon::fromTheme(QStringLiteral("list-remove")), i18n("Delete Action"), this);
+    connect(m_deleteActionButton, &QPushButton::clicked, this, &ActionsWidget::onDeleteAction);
+    layout->addWidget(m_deleteActionButton, 2, 3);
+
+    // Where to configure the action options
+    if (KlipperSettings::actionsInfoMessageShown()) {
+        KMessageWidget *msg = new KMessageWidget(xi18nc("@info",
+                                                        "These actions appear in the popup menu \
+which can be configured on the <interface>Action Menu</interface> page."),
+                                                 this);
+        msg->setMessageType(KMessageWidget::Information);
+        msg->setIcon(QIcon::fromTheme(QStringLiteral("dialog-information")));
+        msg->setWordWrap(true);
+        msg->setCloseButtonVisible(true);
 
-    m_ui.pbAddAction->setIcon(QIcon::fromTheme(QStringLiteral("list-add")));
-    m_ui.pbDelAction->setIcon(QIcon::fromTheme(QStringLiteral("list-remove")));
-    m_ui.pbEditAction->setIcon(QIcon::fromTheme(QStringLiteral("document-edit")));
+        connect(msg, &KMessageWidget::hideAnimationFinished, this, []() {
+            KlipperSettings::setActionsInfoMessageShown(false);
+        });
+        layout->addWidget(msg, 3, 0, 1, -1);
+    }
 
-    const KConfigGroup grp = KSharedConfig::openConfig()->group("ActionsWidget");
+    // Add some vertical space between our buttons and the dialogue buttons
+    layout->setRowMinimumHeight(4, 16);
+
+    const KConfigGroup grp = KSharedConfig::openConfig()->group(metaObject()->className());
     QByteArray hdrState = grp.readEntry("ColumnState", QByteArray());
     if (!hdrState.isEmpty()) {
         qCDebug(KLIPPER_LOG) << "Restoring column state";
-        m_ui.kcfg_ActionList->header()->restoreState(QByteArray::fromBase64(hdrState));
+        m_actionsTree->header()->restoreState(QByteArray::fromBase64(hdrState));
     } else {
-        m_ui.kcfg_ActionList->header()->resizeSection(0, 250);
+        m_actionsTree->header()->resizeSection(0, 250);
     }
 
-    connect(m_ui.kcfg_ActionList, &ActionsTreeWidget::itemSelectionChanged, this, &ActionsWidget::onSelectionChanged);
-    connect(m_ui.kcfg_ActionList, &ActionsTreeWidget::itemDoubleClicked, this, &ActionsWidget::onEditAction);
-
-    connect(m_ui.pbAddAction, &QPushButton::clicked, this, &ActionsWidget::onAddAction);
-    connect(m_ui.pbEditAction, &QPushButton::clicked, this, &ActionsWidget::onEditAction);
-    connect(m_ui.pbDelAction, &QPushButton::clicked, this, &ActionsWidget::onDeleteAction);
+    connect(m_actionsTree, &QTreeWidget::itemSelectionChanged, this, &ActionsWidget::onSelectionChanged);
+    connect(m_actionsTree, &QTreeWidget::itemDoubleClicked, this, &ActionsWidget::onEditAction);
 
     onSelectionChanged();
 }
@@ -387,7 +441,7 @@ void ActionsWidget::setActionList(const ActionList &list)
     qDeleteAll(m_actionList);
     m_actionList.clear();
 
-    for (ClipAction *action : list) {
+    for (const ClipAction *action : list) {
         if (!action) {
             qCDebug(KLIPPER_LOG) << "action is null!";
             continue;
@@ -402,9 +456,9 @@ void ActionsWidget::setActionList(const ActionList &list)
 
 void ActionsWidget::updateActionListView()
 {
-    m_ui.kcfg_ActionList->clear();
+    m_actionsTree->clear();
 
-    foreach (ClipAction *action, m_actionList) {
+    for (const ClipAction *action : m_actionList) {
         if (!action) {
             qCDebug(KLIPPER_LOG) << "action is null!";
             continue;
@@ -413,16 +467,16 @@ void ActionsWidget::updateActionListView()
         QTreeWidgetItem *item = new QTreeWidgetItem;
         updateActionItem(item, action);
 
-        m_ui.kcfg_ActionList->addTopLevelItem(item);
+        m_actionsTree->addTopLevelItem(item);
     }
 
     // after all actions loaded, reset modified state of tree widget.
     // Needed because tree widget reacts on item changed events to tell if it is changed
     // this will ensure that apply button state will be correctly changed
-    m_ui.kcfg_ActionList->resetModifiedState();
+    m_actionsTree->resetModifiedState();
 }
 
-void ActionsWidget::updateActionItem(QTreeWidgetItem *item, ClipAction *action)
+void ActionsWidget::updateActionItem(QTreeWidgetItem *item, const ClipAction *action)
 {
     if (!item || !action) {
         qCDebug(KLIPPER_LOG) << "null pointer passed to function, nothing done";
@@ -434,7 +488,7 @@ void ActionsWidget::updateActionItem(QTreeWidgetItem *item, ClipAction *action)
     item->setText(0, action->actionRegexPattern());
     item->setText(1, action->description());
 
-    foreach (const ClipCommand &command, action->commands()) {
+    for (const ClipCommand &command : action->commands()) {
         QStringList cmdProps;
         cmdProps << command.command << command.description;
         QTreeWidgetItem *child = new QTreeWidgetItem(item, cmdProps);
@@ -446,11 +500,12 @@ ActionList ActionsWidget::actionList() const
 {
     // return a copy of our action list
     ActionList list;
-    foreach (ClipAction *action, m_actionList) {
+    for (const ClipAction *action : m_actionList) {
         if (!action) {
             qCDebug(KLIPPER_LOG) << "action is null";
             continue;
         }
+
         list.append(new ClipAction(*action));
     }
 
@@ -459,79 +514,89 @@ ActionList ActionsWidget::actionList() const
 
 void ActionsWidget::resetModifiedState()
 {
-    m_ui.kcfg_ActionList->resetModifiedState();
+    m_actionsTree->resetModifiedState();
 
     qCDebug(KLIPPER_LOG) << "Saving column state";
-    KConfigGroup grp = KSharedConfig::openConfig()->group("ActionsWidget");
-    grp.writeEntry("ColumnState", m_ui.kcfg_ActionList->header()->saveState().toBase64());
+    KConfigGroup grp = KSharedConfig::openConfig()->group(metaObject()->className());
+    grp.writeEntry("ColumnState", m_actionsTree->header()->saveState().toBase64());
 }
 
 void ActionsWidget::onSelectionChanged()
 {
-    bool itemIsSelected = !m_ui.kcfg_ActionList->selectedItems().isEmpty();
-    m_ui.pbEditAction->setEnabled(itemIsSelected);
-    m_ui.pbDelAction->setEnabled(itemIsSelected);
+    const bool itemIsSelected = !m_actionsTree->selectedItems().isEmpty();
+    m_editActionButton->setEnabled(itemIsSelected);
+    m_deleteActionButton->setEnabled(itemIsSelected);
 }
 
 void ActionsWidget::onAddAction()
 {
-    if (!m_editActDlg) {
-        m_editActDlg = new EditActionDialog(this);
-    }
-
+    EditActionDialog dlg(this);
     ClipAction *newAct = new ClipAction;
-    m_editActDlg->setAction(newAct);
-    if (m_editActDlg->exec() == QDialog::Accepted) {
+    dlg.setAction(newAct);
+
+    if (dlg.exec() == QDialog::Accepted) {
         m_actionList.append(newAct);
 
         QTreeWidgetItem *item = new QTreeWidgetItem;
         updateActionItem(item, newAct);
-        m_ui.kcfg_ActionList->addTopLevelItem(item);
+        m_actionsTree->addTopLevelItem(item);
     }
 }
 
 void ActionsWidget::onEditAction()
 {
-    if (!m_editActDlg) {
-        m_editActDlg = new EditActionDialog(this);
+    QTreeWidgetItem *item = m_actionsTree->currentItem();
+    if (!item) {
+        return;
     }
 
-    QTreeWidgetItem *item = m_ui.kcfg_ActionList->currentItem();
     int commandIdx = -1;
-    if (item) {
-        if (item->parent()) {
-            commandIdx = item->parent()->indexOfChild(item);
-            item = item->parent(); // interested in toplevel action
-        }
+    if (item->parent()) {
+        commandIdx = item->parent()->indexOfChild(item);
+        item = item->parent(); // interested in toplevel action
+    }
 
-        int idx = m_ui.kcfg_ActionList->indexOfTopLevelItem(item);
-        ClipAction *action = m_actionList.at(idx);
+    int idx = m_actionsTree->indexOfTopLevelItem(item);
+    ClipAction *action = m_actionList.at(idx);
 
-        if (!action) {
-            qCDebug(KLIPPER_LOG) << "action is null";
-            return;
-        }
-
-        m_editActDlg->setAction(action, commandIdx);
-        // dialog will save values into action if user hits OK
-        m_editActDlg->exec();
+    if (!action) {
+        qCDebug(KLIPPER_LOG) << "action is null";
+        return;
+    }
 
+    EditActionDialog dlg(this);
+    dlg.setAction(action, commandIdx);
+    // dialog will save values into action if user hits OK
+    if (dlg.exec() == QDialog::Accepted) {
         updateActionItem(item, action);
     }
 }
 
 void ActionsWidget::onDeleteAction()
 {
-    QTreeWidgetItem *item = m_ui.kcfg_ActionList->currentItem();
-    if (item && item->parent())
+    QTreeWidgetItem *item = m_actionsTree->currentItem();
+    if (!item) {
+        return;
+    }
+
+    // If the item has a parent, then it is a command (the second level
+    // of the tree).  Find the complete action.
+    if (item->parent()) {
         item = item->parent();
+    }
 
-    if (item) {
-        int idx = m_ui.kcfg_ActionList->indexOfTopLevelItem(item);
+    if (KMessageBox::warningContinueCancel(this,
+                                           xi18nc("@info", "Delete the selected action <resource>%1</resource><nl/>and all of its commands?", item->text(1)),
+                                           i18n("Confirm Delete Action"),
+                                           KStandardGuiItem::del(),
+                                           KStandardGuiItem::cancel(),
+                                           QStringLiteral("deleteAction"),
+                                           KMessageBox::Dangerous)
+        == KMessageBox::Continue) {
+        int idx = m_actionsTree->indexOfTopLevelItem(item);
         m_actionList.removeAt(idx);
+        delete item;
     }
-
-    delete item;
 }
 
 //////////////////////////
@@ -618,15 +683,15 @@ AdvancedWidget::AdvancedWidget(QWidget *parent)
 {
     QVBoxLayout *mainLayout = new QVBoxLayout(this);
 
-    QLabel *hint = createHintLabel(xi18nc("@info",
-                                          "The action popup menu will not be shown automatically for these windows, \
+    QLabel *hint = ConfigDialog::createHintLabel(xi18nc("@info",
+                                                        "The action popup will not be shown automatically for these windows, \
 even if it is enabled. This is because, for example, a web browser may highlight a URL \
 in the address bar while typing, so the menu would show for every keystroke.\
 <nl/>\
 <nl/>\
 If the action menu appears unexpectedly when using a particular application, then add it to this list. \
 <link>How to find the name to enter</link>."),
-                                   this);
+                                                 this);
 
     mainLayout->addWidget(hint);
     connect(hint, &QLabel::linkActivated, this, [this, hint]() {
diff --git a/klipper/configdialog.h b/klipper/configdialog.h
index 35d2eee37..395de50cb 100644
--- a/klipper/configdialog.h
+++ b/klipper/configdialog.h
@@ -10,9 +10,8 @@
 
 #include "urlgrabber.h"
 
-#include "ui_actionsconfig.h"
-
 class KConfigSkeleton;
+class KConfigSkeletonItem;
 class KShortcutsEditor;
 class Klipper;
 class KEditListWidget;
@@ -21,6 +20,9 @@ class KPluralHandlingSpinBox;
 class EditActionDialog;
 class QCheckBox;
 class QRadioButton;
+class QTreeWidgetItem;
+class QLabel;
+class ActionsTreeWidget;
 
 class GeneralWidget : public QWidget
 {
@@ -106,11 +108,14 @@ private Q_SLOTS:
     void onDeleteAction();
 
 private:
-    void updateActionItem(QTreeWidgetItem *item, ClipAction *action);
+    void updateActionItem(QTreeWidgetItem *item, const ClipAction *action);
     void updateActionListView();
 
-    Ui::ActionsWidget m_ui;
-    EditActionDialog *m_editActDlg;
+private:
+    ActionsTreeWidget *m_actionsTree;
+    QPushButton *m_addActionButton;
+    QPushButton *m_editActionButton;
+    QPushButton *m_deleteActionButton;
 
     /**
      * List of actions this page works with
@@ -142,6 +147,10 @@ public:
     ConfigDialog(QWidget *parent, KConfigSkeleton *config, Klipper *klipper, KActionCollection *collection);
     ~ConfigDialog() override = default;
 
+    static QLabel *createHintLabel(const QString &text, QWidget *parent);
+    static QLabel *createHintLabel(const KConfigSkeletonItem *item, QWidget *parent);
+    static QString manualShortcutString();
+
 protected slots:
     // reimp
     void updateWidgets() override;
diff --git a/klipper/editactiondialog.cpp b/klipper/editactiondialog.cpp
index 89b6b0d48..88d5113a8 100644
--- a/klipper/editactiondialog.cpp
+++ b/klipper/editactiondialog.cpp
@@ -6,20 +6,27 @@
 
 #include "editactiondialog.h"
 
-#include "klipper_debug.h"
-#include <QComboBox>
 #include <QDialogButtonBox>
-#include <QIcon>
-#include <QItemDelegate>
-
-#include <KWindowConfig>
+#include <qcheckbox.h>
+#include <qcoreapplication.h>
+#include <qformlayout.h>
+#include <qgridlayout.h>
+#include <qheaderview.h>
+#include <qlabel.h>
+#include <qlineedit.h>
+#include <qpushbutton.h>
+#include <qtableview.h>
+#include <qwindow.h>
+
+#include <klocalizedstring.h>
+#include <kmessagebox.h>
 #include <kwindowconfig.h>
 
-#include "ui_editactiondialog.h"
-#include "urlgrabber.h"
+#include "klipper_debug.h"
+
+#include "configdialog.h"
+#include "editcommanddialog.h"
 
-namespace
-{
 static QString output2text(ClipCommand::Output output)
 {
     switch (output) {
@@ -33,54 +40,15 @@ static QString output2text(ClipCommand::Output output)
     return QString();
 }
 
-}
-
-/**
- * Show dropdown of editing Output part of commands
- */
-class ActionOutputDelegate : public QItemDelegate
-{
-public:
-    ActionOutputDelegate(QObject *parent = nullptr)
-        : QItemDelegate(parent)
-    {
-    }
-
-    QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem & /*option*/, const QModelIndex & /*index*/) const override
-    {
-        QComboBox *editor = new QComboBox(parent);
-        editor->setInsertPolicy(QComboBox::NoInsert);
-        editor->addItem(output2text(ClipCommand::IGNORE), QVariant::fromValue<ClipCommand::Output>(ClipCommand::IGNORE));
-        editor->addItem(output2text(ClipCommand::REPLACE), QVariant::fromValue<ClipCommand::Output>(ClipCommand::REPLACE));
-        editor->addItem(output2text(ClipCommand::ADD), QVariant::fromValue<ClipCommand::Output>(ClipCommand::ADD));
-        return editor;
-    }
-
-    void setEditorData(QWidget *editor, const QModelIndex &index) const override
-    {
-        QComboBox *ed = static_cast<QComboBox *>(editor);
-        QVariant data(index.model()->data(index, Qt::EditRole));
-        ed->setCurrentIndex(static_cast<int>(data.value<ClipCommand::Output>()));
-    }
-
-    void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override
-    {
-        QComboBox *ed = static_cast<QComboBox *>(editor);
-        model->setData(index, ed->itemData(ed->currentIndex()));
-    }
-
-    void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex & /*index*/) const override
-    {
-        editor->setGeometry(option.rect);
-    }
-};
+//////////////////////////
+//  ActionDetailModel	//
+//////////////////////////
 
 class ActionDetailModel : public QAbstractTableModel
 {
 public:
-    ActionDetailModel(ClipAction *action, QObject *parent = nullptr);
+    explicit ActionDetailModel(ClipAction *action, QObject *parent = nullptr);
     QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
-    bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
     Qt::ItemFlags flags(const QModelIndex &index) const override;
     int rowCount(const QModelIndex &parent = QModelIndex()) const override;
     int columnCount(const QModelIndex &parent) const override;
@@ -90,15 +58,14 @@ public:
         return m_commands;
     }
     void addCommand(const ClipCommand &command);
-    void removeCommand(const QModelIndex &index);
+    void removeCommand(const QModelIndex &idx);
+    void replaceCommand(const ClipCommand &command, const QModelIndex &idx);
 
 private:
     enum column_t { COMMAND_COL = 0, OUTPUT_COL = 1, DESCRIPTION_COL = 2 };
     QList<ClipCommand> m_commands;
     QVariant displayData(ClipCommand *command, column_t column) const;
-    QVariant editData(ClipCommand *command, column_t column) const;
     QVariant decorationData(ClipCommand *command, column_t column) const;
-    void setIconForCommand(ClipCommand &cmd);
 };
 
 ActionDetailModel::ActionDetailModel(ClipAction *action, QObject *parent)
@@ -109,46 +76,7 @@ ActionDetailModel::ActionDetailModel(ClipAction *action, QObject *parent)
 
 Qt::ItemFlags ActionDetailModel::flags(const QModelIndex & /*index*/) const
 {
-    return Qt::ItemIsEditable | Qt::ItemIsEnabled | Qt::ItemIsSelectable;
-}
-
-void ActionDetailModel::setIconForCommand(ClipCommand &cmd)
-{
-    // let's try to update icon of the item according to command
-    QString command = cmd.command;
-    if (command.contains(QLatin1Char(' '))) {
-        // get first word
-        command = command.section(QLatin1Char(' '), 0, 0);
-    }
-
-    if (QIcon::hasThemeIcon(command)) {
-        cmd.icon = command;
-    } else {
-        cmd.icon.clear();
-    }
-}
-
-bool ActionDetailModel::setData(const QModelIndex &index, const QVariant &value, int role)
-{
-    if (role == Qt::EditRole) {
-        ClipCommand cmd = m_commands.at(index.row());
-        switch (static_cast<column_t>(index.column())) {
-        case COMMAND_COL:
-            cmd.command = value.toString();
-            setIconForCommand(cmd);
-            break;
-        case OUTPUT_COL:
-            cmd.output = value.value<ClipCommand::Output>();
-            break;
-        case DESCRIPTION_COL:
-            cmd.description = value.toString();
-            break;
-        }
-        m_commands.replace(index.row(), cmd);
-        Q_EMIT dataChanged(index, index);
-        return true;
-    }
-    return false;
+    return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
 }
 
 int ActionDetailModel::columnCount(const QModelIndex & /*parent*/) const
@@ -186,19 +114,6 @@ QVariant ActionDetailModel::decorationData(ClipCommand *command, ActionDetailMod
     return QVariant();
 }
 
-QVariant ActionDetailModel::editData(ClipCommand *command, ActionDetailModel::column_t column) const
-{
-    switch (column) {
-    case COMMAND_COL:
-        return command->command;
-    case OUTPUT_COL:
-        return QVariant::fromValue<ClipCommand::Output>(command->output);
-    case DESCRIPTION_COL:
-        return command->description;
-    }
-    return QVariant();
-}
-
 QVariant ActionDetailModel::headerData(int section, Qt::Orientation orientation, int role) const
 {
     if (orientation == Qt::Horizontal && role == Qt::DisplayRole) {
@@ -206,7 +121,7 @@ QVariant ActionDetailModel::headerData(int section, Qt::Orientation orientation,
         case COMMAND_COL:
             return i18n("Command");
         case OUTPUT_COL:
-            return i18n("Output Handling");
+            return i18n("Output");
         case DESCRIPTION_COL:
             return i18n("Description");
         }
@@ -224,8 +139,6 @@ QVariant ActionDetailModel::data(const QModelIndex &index, int role) const
         return displayData(&cmd, static_cast<column_t>(column));
     case Qt::DecorationRole:
         return decorationData(&cmd, static_cast<column_t>(column));
-    case Qt::EditRole:
-        return editData(&cmd, static_cast<column_t>(column));
     }
     return QVariant();
 }
@@ -237,14 +150,29 @@ void ActionDetailModel::addCommand(const ClipCommand &command)
     endInsertRows();
 }
 
-void ActionDetailModel::removeCommand(const QModelIndex &index)
+void ActionDetailModel::replaceCommand(const ClipCommand &command, const QModelIndex &idx)
+{
+    if (!idx.isValid())
+        return;
+    const int row = idx.row();
+    m_commands[row] = command;
+    emit dataChanged(index(row, static_cast<int>(COMMAND_COL)), index(row, static_cast<int>(DESCRIPTION_COL)));
+}
+
+void ActionDetailModel::removeCommand(const QModelIndex &idx)
 {
-    int row = index.row();
+    if (!idx.isValid())
+        return;
+    const int row = idx.row();
     beginRemoveRows(QModelIndex(), row, row);
     m_commands.removeAt(row);
     endRemoveRows();
 }
 
+//////////////////////////
+//  EditActionDialog	//
+//////////////////////////
+
 EditActionDialog::EditActionDialog(QWidget *parent)
     : QDialog(parent)
 {
@@ -254,52 +182,131 @@ EditActionDialog::EditActionDialog(QWidget *parent)
     connect(buttons, &QDialogButtonBox::accepted, this, &EditActionDialog::slotAccepted);
     connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
 
-    QWidget *dlgWidget = new QWidget(this);
-    m_ui = new Ui::EditActionDialog;
-    m_ui->setupUi(dlgWidget);
-
-    m_ui->leRegExp->setClearButtonEnabled(true);
-    m_ui->leDescription->setClearButtonEnabled(true);
-
-    m_ui->pbAddCommand->setIcon(QIcon::fromTheme(QStringLiteral("list-add")));
-    m_ui->pbRemoveCommand->setIcon(QIcon::fromTheme(QStringLiteral("list-remove")));
-
-    // For some reason, the default row height is 30 pixel. Set it to the minimum sectionSize instead,
+    // Upper widget: pattern, description and options
+    QWidget *optionsWidget = new QWidget(this);
+    QFormLayout *optionsLayout = new QFormLayout(optionsWidget);
+
+    // General information label
+    QLabel *hint = ConfigDialog::createHintLabel(xi18nc("@info",
+                                                        "An action takes effect when its \
+<interface>match pattern</interface> matches the clipboard contents. \
+When this happens, the action's <interface>commands</interface> appear \
+in the Klipper popup menu; if one of them is chosen, \
+the command is executed."),
+                                                 this);
+    optionsLayout->addRow(hint);
+    optionsLayout->addRow(QString(), new QLabel(optionsWidget));
+
+    // Pattern (regular expression)
+    m_regExpEdit = new QLineEdit(optionsWidget);
+    m_regExpEdit->setClearButtonEnabled(true);
+    m_regExpEdit->setPlaceholderText(i18n("Enter a pattern to match against the clipboard"));
+
+    optionsLayout->addRow(i18n("Match pattern:"), m_regExpEdit);
+
+    hint = ConfigDialog::createHintLabel(xi18nc("@info",
+                                                "The match pattern is a regular expression. \
+For more information see the \
+<link url=\"https://en.wikipedia.org/wiki/Regular_expression\">Wikipedia entry</link> \
+for this topic."),
+                                         this);
+    hint->setOpenExternalLinks(true);
+    optionsLayout->addRow(QString(), hint);
+
+    // Description
+    m_descriptionEdit = new QLineEdit(optionsWidget);
+    m_descriptionEdit->setClearButtonEnabled(true);
+    m_descriptionEdit->setPlaceholderText(i18n("Enter a description for the action"));
+    optionsLayout->addRow(i18n("Description:"), m_descriptionEdit);
+
+    // Include in automatic popup
+    m_automaticCheck = new QCheckBox(i18n("Include in automatic popup"), optionsWidget);
+    optionsLayout->addRow(QString(), m_automaticCheck);
+
+    hint = ConfigDialog::createHintLabel(xi18nc("@info",
+                                                "The commands \
+for this match will be included in the automatic action popup, if it is enabled in \
+the <interface>Action Menu</interface> page. If this option is turned off, the commands for \
+this match will not be included in the automatic popup but they will be included if the \
+popup is activated manually with the <shortcut>%1</shortcut> key shortcut.",
+                                                ConfigDialog::manualShortcutString()),
+                                         this);
+    optionsLayout->addRow(QString(), hint);
+
+    optionsLayout->addRow(QString(), new QLabel(optionsWidget));
+
+    // Lower widget: command list and action buttons
+    QWidget *listWidget = new QWidget(this);
+    QGridLayout *listLayout = new QGridLayout(listWidget);
+    listLayout->setContentsMargins(0, 0, 0, 0);
+
+    // Command list
+    m_commandList = new QTableView(listWidget);
+    m_commandList->setAlternatingRowColors(true);
+    m_commandList->setSelectionMode(QAbstractItemView::SingleSelection);
+    m_commandList->setSelectionBehavior(QAbstractItemView::SelectRows);
+    m_commandList->setShowGrid(false);
+    m_commandList->setWordWrap(false);
+    m_commandList->horizontalHeader()->setStretchLastSection(true);
+    m_commandList->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft);
+    m_commandList->verticalHeader()->setVisible(false);
+    // For some reason, the default row height is 30 pixels.
+    // Set it to the minimumSectionSize instead,
     // which is the font height+struts.
-    m_ui->twCommandList->verticalHeader()->setDefaultSectionSize(m_ui->twCommandList->verticalHeader()->minimumSectionSize());
-    m_ui->twCommandList->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft);
-
-    QVBoxLayout *layout = new QVBoxLayout(this);
-    layout->addWidget(dlgWidget);
-    layout->addWidget(buttons);
-
-    connect(m_ui->pbAddCommand, &QPushButton::clicked, this, &EditActionDialog::onAddCommand);
-    connect(m_ui->pbRemoveCommand, &QPushButton::clicked, this, &EditActionDialog::onRemoveCommand);
-
-    const KConfigGroup grp = KSharedConfig::openConfig()->group("EditActionDialog");
+    m_commandList->verticalHeader()->setDefaultSectionSize(m_commandList->verticalHeader()->minimumSectionSize());
+
+    listLayout->addWidget(m_commandList, 0, 0, 1, -1);
+    listLayout->setRowStretch(0, 1);
+
+    // "Add" button
+    m_addCommandPb = new QPushButton(QIcon::fromTheme(QStringLiteral("list-add")), i18n("Add Command..."), listWidget);
+    connect(m_addCommandPb, &QPushButton::clicked, this, &EditActionDialog::onAddCommand);
+    listLayout->addWidget(m_addCommandPb, 1, 0);
+
+    // "Edit" button
+    m_editCommandPb = new QPushButton(QIcon::fromTheme(QStringLiteral("document-edit")), i18n("Edit Command..."), this);
+    connect(m_editCommandPb, &QPushButton::clicked, this, &EditActionDialog::onEditCommand);
+    listLayout->addWidget(m_editCommandPb, 1, 1);
+    listLayout->setColumnStretch(2, 1);
+
+    // "Delete" button
+    m_removeCommandPb = new QPushButton(QIcon::fromTheme(QStringLiteral("list-remove")), i18n("Delete Command"), this);
+    connect(m_removeCommandPb, &QPushButton::clicked, this, &EditActionDialog::onRemoveCommand);
+    listLayout->addWidget(m_removeCommandPb, 1, 3);
+
+    // Add some vertical space between our buttons and the dialogue buttons
+    listLayout->setRowMinimumHeight(2, 16);
+
+    // Main dialogue layout
+    QVBoxLayout *mainLayout = new QVBoxLayout(this);
+    mainLayout->addWidget(optionsWidget);
+    mainLayout->addWidget(listWidget);
+    mainLayout->setStretch(1, 1);
+    mainLayout->addWidget(buttons);
+
+    (void)winId();
+    windowHandle()->resize(540, 560); // default, if there is no saved size
+    const KConfigGroup grp = KSharedConfig::openConfig()->group(metaObject()->className());
     KWindowConfig::restoreWindowSize(windowHandle(), grp);
+    resize(windowHandle()->size());
+
     QByteArray hdrState = grp.readEntry("ColumnState", QByteArray());
     if (!hdrState.isEmpty()) {
         qCDebug(KLIPPER_LOG) << "Restoring column state";
-        m_ui->twCommandList->horizontalHeader()->restoreState(QByteArray::fromBase64(hdrState));
+        m_commandList->horizontalHeader()->restoreState(QByteArray::fromBase64(hdrState));
     }
     // do this after restoreState()
-    m_ui->twCommandList->horizontalHeader()->setHighlightSections(false);
+    m_commandList->horizontalHeader()->setHighlightSections(false);
 }
 
-EditActionDialog::~EditActionDialog()
-{
-    delete m_ui;
-}
 
 void EditActionDialog::setAction(ClipAction *act, int commandIdxToSelect)
 {
     m_action = act;
     m_model = new ActionDetailModel(act, this);
-    m_ui->twCommandList->setModel(m_model);
-    m_ui->twCommandList->setItemDelegateForColumn(1, new ActionOutputDelegate);
-    connect(m_ui->twCommandList->selectionModel(), &QItemSelectionModel::selectionChanged, this, &EditActionDialog::onSelectionChanged);
-
+    m_commandList->setModel(m_model);
+    connect(m_commandList->selectionModel(), &QItemSelectionModel::selectionChanged, this, &EditActionDialog::onSelectionChanged);
+    connect(m_commandList, &QAbstractItemView::doubleClicked, this, &EditActionDialog::onEditCommand);
     updateWidgets(commandIdxToSelect);
 }
 
@@ -310,16 +317,15 @@ void EditActionDialog::updateWidgets(int commandIdxToSelect)
         return;
     }
 
-    m_ui->leRegExp->setText(m_action->actionRegexPattern());
-    m_ui->automatic->setChecked(m_action->automatic());
-    m_ui->leDescription->setText(m_action->description());
+    m_regExpEdit->setText(m_action->actionRegexPattern());
+    m_descriptionEdit->setText(m_action->description());
+    m_automaticCheck->setChecked(m_action->automatic());
 
     if (commandIdxToSelect != -1) {
-        m_ui->twCommandList->setCurrentIndex(m_model->index(commandIdxToSelect, 0));
+        m_commandList->setCurrentIndex(m_model->index(commandIdxToSelect, 0));
     }
 
-    // update Remove button
-    onSelectionChanged();
+    onSelectionChanged(); // update Remove/Edit buttons
 }
 
 void EditActionDialog::saveAction()
@@ -329,9 +335,9 @@ void EditActionDialog::saveAction()
         return;
     }
 
-    m_action->setActionRegexPattern(m_ui->leRegExp->text());
-    m_action->setDescription(m_ui->leDescription->text());
-    m_action->setAutomatic(m_ui->automatic->isChecked());
+    m_action->setActionRegexPattern(m_regExpEdit->text());
+    m_action->setDescription(m_descriptionEdit->text());
+    m_action->setAutomatic(m_automaticCheck->isChecked());
 
     m_action->clearCommands();
 
@@ -345,24 +351,55 @@ void EditActionDialog::slotAccepted()
     saveAction();
 
     qCDebug(KLIPPER_LOG) << "Saving dialogue state";
-    KConfigGroup grp = KSharedConfig::openConfig()->group("EditActionDialog");
+    KConfigGroup grp = KSharedConfig::openConfig()->group(metaObject()->className());
     KWindowConfig::saveWindowSize(windowHandle(), grp);
-    grp.writeEntry("ColumnState", m_ui->twCommandList->horizontalHeader()->saveState().toBase64());
+    grp.writeEntry("ColumnState", m_commandList->horizontalHeader()->saveState().toBase64());
     accept();
 }
 
 void EditActionDialog::onAddCommand()
 {
-    m_model->addCommand(ClipCommand(i18n("new command"), i18n("Command Description"), true, QLatin1String("")));
-    m_ui->twCommandList->edit(m_model->index(m_model->rowCount() - 1, 0));
+    ClipCommand command(QString(), QString(), true, QLatin1String(""));
+    EditCommandDialog dlg(command, this);
+    if (dlg.exec() != QDialog::Accepted)
+        return;
+    m_model->addCommand(dlg.command());
+}
+
+void EditActionDialog::onEditCommand()
+{
+    QPersistentModelIndex commandIndex(m_commandList->selectionModel()->currentIndex());
+    if (!commandIndex.isValid())
+        return;
+
+    EditCommandDialog dlg(m_model->commands().at(commandIndex.row()), this);
+    if (dlg.exec() != QDialog::Accepted)
+        return;
+    m_model->replaceCommand(dlg.command(), commandIndex);
 }
 
 void EditActionDialog::onRemoveCommand()
 {
-    m_model->removeCommand(m_ui->twCommandList->selectionModel()->currentIndex());
+    QPersistentModelIndex commandIndex(m_commandList->selectionModel()->currentIndex());
+    if (!commandIndex.isValid())
+        return;
+
+    if (KMessageBox::warningContinueCancel(
+            this,
+            xi18nc("@info", "Delete the selected command <resource>%1</resource>?", m_model->commands().at(commandIndex.row()).description),
+            i18n("Confirm Delete Command"),
+            KStandardGuiItem::del(),
+            KStandardGuiItem::cancel(),
+            QStringLiteral("deleteCommand"),
+            KMessageBox::Dangerous)
+        == KMessageBox::Continue) {
+        m_model->removeCommand(commandIndex);
+    }
 }
 
 void EditActionDialog::onSelectionChanged()
 {
-    m_ui->pbRemoveCommand->setEnabled(m_ui->twCommandList->selectionModel() && m_ui->twCommandList->selectionModel()->hasSelection());
+    const bool itemIsSelected = (m_commandList->selectionModel() && m_commandList->selectionModel()->hasSelection());
+    m_removeCommandPb->setEnabled(itemIsSelected);
+    m_editCommandPb->setEnabled(itemIsSelected);
 }
diff --git a/klipper/editactiondialog.h b/klipper/editactiondialog.h
index 219443d33..e27df6af2 100644
--- a/klipper/editactiondialog.h
+++ b/klipper/editactiondialog.h
@@ -8,10 +8,9 @@
 
 #include <QDialog>
 
-namespace Ui
-{
-class EditActionDialog;
-}
+class QLineEdit;
+class QCheckBox;
+class QTableView;
 
 class ClipAction;
 class ActionDetailModel;
@@ -19,9 +18,10 @@ class ActionDetailModel;
 class EditActionDialog : public QDialog
 {
     Q_OBJECT
+
 public:
     explicit EditActionDialog(QWidget *parent);
-    ~EditActionDialog() override;
+    ~EditActionDialog() override = default;
 
     /**
      * Sets the action this dialog will work with
@@ -30,10 +30,10 @@ public:
 
 private Q_SLOTS:
     void onAddCommand();
+    void onEditCommand();
     void onRemoveCommand();
     void onSelectionChanged();
     void slotAccepted();
-    //    void onItemChanged( QTreeWidgetItem*, int );
 
 private:
     /**
@@ -49,8 +49,15 @@ private:
     void saveAction();
 
 private:
-    Ui::EditActionDialog *m_ui;
+    QLineEdit *m_regExpEdit;
+    QLineEdit *m_descriptionEdit;
+    QCheckBox *m_automaticCheck;
+
+    QTableView *m_commandList;
+    QPushButton *m_addCommandPb;
+    QPushButton *m_editCommandPb;
+    QPushButton *m_removeCommandPb;
 
     ClipAction *m_action;
     ActionDetailModel *m_model;
-};
\ No newline at end of file
+};
diff --git a/klipper/editactiondialog.ui b/klipper/editactiondialog.ui
deleted file mode 100644
index 5e03c8b31..000000000
--- a/klipper/editactiondialog.ui
+++ /dev/null
@@ -1,220 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<ui version="4.0">
- <class>EditActionDialog</class>
- <widget class="QWidget" name="EditActionDialog">
-  <property name="geometry">
-   <rect>
-    <x>0</x>
-    <y>0</y>
-    <width>362</width>
-    <height>329</height>
-   </rect>
-  </property>
-  <layout class="QGridLayout" name="gridLayout_2">
-   <property name="margin">
-    <number>0</number>
-   </property>
-   <item row="0" column="0" colspan="2">
-    <widget class="QLabel" name="label_3">
-     <property name="text">
-      <string>Action properties:</string>
-     </property>
-    </widget>
-   </item>
-   <item row="1" column="1">
-    <layout class="QGridLayout" name="gridLayout">
-     <item row="0" column="2">
-      <widget class="QLineEdit" name="leRegExp">
-       <property name="minimumSize">
-        <size>
-         <width>200</width>
-         <height>0</height>
-        </size>
-       </property>
-      </widget>
-     </item>
-     <item row="2" column="2">
-      <widget class="QLineEdit" name="leDescription"/>
-     </item>
-     <item row="0" column="1">
-      <widget class="QLabel" name="label_2">
-       <property name="text">
-        <string>Regular expression:</string>
-       </property>
-       <property name="alignment">
-        <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
-       </property>
-      </widget>
-     </item>
-     <item row="2" column="1">
-      <widget class="QLabel" name="label">
-       <property name="text">
-        <string>Description:</string>
-       </property>
-       <property name="alignment">
-        <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
-       </property>
-      </widget>
-     </item>
-     <item row="1" column="1">
-      <widget class="QLabel" name="label_5">
-       <property name="text">
-        <string>Automatic:</string>
-       </property>
-       <property name="alignment">
-        <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
-       </property>
-       <property name="buddy">
-        <cstring>automatic</cstring>
-       </property>
-      </widget>
-     </item>
-     <item row="1" column="2">
-      <widget class="QCheckBox" name="automatic">
-       <property name="layoutDirection">
-        <enum>Qt::LeftToRight</enum>
-       </property>
-       <property name="text">
-        <string/>
-       </property>
-      </widget>
-     </item>
-    </layout>
-   </item>
-   <item row="3" column="0" colspan="2">
-    <widget class="QLabel" name="label_4">
-     <property name="text">
-      <string>List of commands for this action:</string>
-     </property>
-    </widget>
-   </item>
-   <item row="1" column="0">
-    <spacer name="horizontalSpacer">
-     <property name="orientation">
-      <enum>Qt::Horizontal</enum>
-     </property>
-     <property name="sizeType">
-      <enum>QSizePolicy::Fixed</enum>
-     </property>
-     <property name="sizeHint" stdset="0">
-      <size>
-       <width>15</width>
-       <height>20</height>
-      </size>
-     </property>
-    </spacer>
-   </item>
-   <item row="4" column="0">
-    <spacer name="horizontalSpacer_2">
-     <property name="orientation">
-      <enum>Qt::Horizontal</enum>
-     </property>
-     <property name="sizeType">
-      <enum>QSizePolicy::Fixed</enum>
-     </property>
-     <property name="sizeHint" stdset="0">
-      <size>
-       <width>15</width>
-       <height>20</height>
-      </size>
-     </property>
-    </spacer>
-   </item>
-   <item row="2" column="0" colspan="2">
-    <spacer name="verticalSpacer">
-     <property name="orientation">
-      <enum>Qt::Vertical</enum>
-     </property>
-     <property name="sizeType">
-      <enum>QSizePolicy::Fixed</enum>
-     </property>
-     <property name="sizeHint" stdset="0">
-      <size>
-       <width>0</width>
-       <height>15</height>
-      </size>
-     </property>
-    </spacer>
-   </item>
-   <item row="6" column="1">
-    <layout class="QHBoxLayout" name="horizontalLayout">
-     <item>
-      <widget class="QPushButton" name="pbAddCommand">
-       <property name="text">
-        <string>Add Command</string>
-       </property>
-      </widget>
-     </item>
-     <item>
-      <widget class="QPushButton" name="pbRemoveCommand">
-       <property name="text">
-        <string>Remove Command</string>
-       </property>
-      </widget>
-     </item>
-    </layout>
-   </item>
-   <item row="7" column="1">
-    <spacer name="verticalSpacer_2">
-     <property name="orientation">
-      <enum>Qt::Vertical</enum>
-     </property>
-     <property name="sizeType">
-      <enum>QSizePolicy::Fixed</enum>
-     </property>
-     <property name="sizeHint" stdset="0">
-      <size>
-       <width>20</width>
-       <height>15</height>
-      </size>
-     </property>
-    </spacer>
-   </item>
-   <item row="4" column="1">
-    <widget class="QTableView" name="twCommandList">
-     <property name="toolTip">
-      <string>Double-click an item to edit</string>
-     </property>
-     <property name="alternatingRowColors">
-      <bool>true</bool>
-     </property>
-     <property name="selectionMode">
-      <enum>QAbstractItemView::SingleSelection</enum>
-     </property>
-     <property name="selectionBehavior">
-      <enum>QAbstractItemView::SelectRows</enum>
-     </property>
-     <property name="showGrid">
-      <bool>false</bool>
-     </property>
-     <property name="gridStyle">
-      <enum>Qt::NoPen</enum>
-     </property>
-     <property name="wordWrap">
-      <bool>false</bool>
-     </property>
-     <attribute name="horizontalHeaderStretchLastSection">
-      <bool>true</bool>
-     </attribute>
-     <attribute name="verticalHeaderVisible">
-      <bool>false</bool>
-     </attribute>
-     <attribute name="verticalHeaderStretchLastSection">
-      <bool>false</bool>
-     </attribute>
-    </widget>
-   </item>
-  </layout>
- </widget>
- <tabstops>
-  <tabstop>twCommandList</tabstop>
-  <tabstop>leRegExp</tabstop>
-  <tabstop>automatic</tabstop>
-  <tabstop>leDescription</tabstop>
-  <tabstop>pbRemoveCommand</tabstop>
-  <tabstop>pbAddCommand</tabstop>
- </tabstops>
- <customwidgets/>
- <resources/>
- <connections/>
-</ui>
diff --git a/klipper/editcommanddialog.cpp b/klipper/editcommanddialog.cpp
new file mode 100644
index 000000000..2c5b9395e
--- /dev/null
+++ b/klipper/editcommanddialog.cpp
@@ -0,0 +1,186 @@
+/*
+    SPDX-FileCopyrightText: 2022 Jonathan Marten <jjm at keelhaul.me.uk>
+    SPDX-License-Identifier: GPL-2.0-or-later
+*/
+
+#include "editcommanddialog.h"
+
+#include <qbuttongroup.h>
+#include <qdialogbuttonbox.h>
+#include <qformlayout.h>
+#include <qlabel.h>
+#include <qlineedit.h>
+#include <qpushbutton.h>
+#include <qradiobutton.h>
+#include <qwindow.h>
+
+#include <kiconbutton.h>
+#include <klocalizedstring.h>
+#include <kstandardguiitem.h>
+#include <kwindowconfig.h>
+
+#include "klipper_debug.h"
+
+#include "configdialog.h"
+#include "urlgrabber.h"
+
+static void setIconForCommand(ClipCommand *cmd)
+{
+    // let's try to update icon of the item according to command
+    QString command = cmd->command;
+    if (command.contains(QLatin1Char(' '))) {
+        // get first word
+        command = command.section(QLatin1Char(' '), 0, 0);
+    }
+
+    if (QIcon::hasThemeIcon(command)) {
+        cmd->icon = command;
+    } else {
+        cmd->icon.clear();
+    }
+}
+
+EditCommandDialog::EditCommandDialog(const ClipCommand &command, QWidget *parent)
+    : QDialog(parent)
+    , m_command(command)
+{
+    setWindowTitle(i18n("Command Properties"));
+    QDialogButtonBox *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
+    m_okButton = buttons->button(QDialogButtonBox::Ok);
+    m_okButton->setShortcut(Qt::CTRL | Qt::Key_Return);
+    connect(buttons, &QDialogButtonBox::accepted, this, &EditCommandDialog::slotAccepted);
+    connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
+
+    QWidget *optionsWidget = new QWidget(this);
+    QFormLayout *optionsLayout = new QFormLayout(optionsWidget);
+
+    // Command
+    m_commandEdit = new QLineEdit(optionsWidget);
+    m_commandEdit->setClearButtonEnabled(true);
+    m_commandEdit->setPlaceholderText(i18n("Enter the command and arguments"));
+    connect(m_commandEdit, &QLineEdit::textEdited, this, &EditCommandDialog::slotUpdateButtons);
+
+    optionsLayout->addRow(i18n("Command:"), m_commandEdit);
+
+    // See ClipCommandProcess::ClipCommandProcess() for the
+    // substitutions made.  "%0" is the complete text matched by the
+    // regular expression, which may only match a part of the clipboard
+    // contents, so it is mentioned here.  However, "%u"/"%U" and "%f"/"%F"
+    // are exactly equivalent to "%s", the complete clipboard contents,
+    // so there is no point mentioning them here.
+    QLabel *hint = ConfigDialog::createHintLabel(xi18nc("@info",
+                                                        "A <placeholder>%s</placeholder> in the command will be replaced by the \
+complete clipboard contents. <placeholder>%0</placeholder> through \
+<placeholder>%9</placeholder> will be replaced by the corresponding \
+captured texts from the match pattern."),
+                                                 optionsWidget);
+    optionsLayout->addRow(QString(), hint);
+
+    // Description
+    m_descriptionEdit = new QLineEdit(optionsWidget);
+    m_descriptionEdit->setClearButtonEnabled(true);
+    m_descriptionEdit->setPlaceholderText(i18n("Enter a description for the command"));
+    connect(m_descriptionEdit, &QLineEdit::textEdited, this, &EditCommandDialog::slotUpdateButtons);
+    optionsLayout->addRow(i18n("Description:"), m_descriptionEdit);
+    optionsLayout->addRow(QString(), new QLabel(this));
+
+    // Radio button group: Output handling
+    QButtonGroup *buttonGroup = new QButtonGroup(this);
+
+    m_ignoreRadio = new QRadioButton(i18n("Ignore"), this);
+    buttonGroup->addButton(m_ignoreRadio);
+    optionsLayout->addRow(i18n("Output from command:"), m_ignoreRadio);
+
+    m_replaceRadio = new QRadioButton(i18n("Replace current clipboard"), this);
+    buttonGroup->addButton(m_replaceRadio);
+    optionsLayout->addRow(QString(), m_replaceRadio);
+
+    m_appendRadio = new QRadioButton(i18n("Append to clipboard"), this);
+    buttonGroup->addButton(m_appendRadio);
+    optionsLayout->addRow(QString(), m_appendRadio);
+
+    optionsLayout->addRow(QString(), ConfigDialog::createHintLabel(i18n("What happens to the standard output of the command executed."), this));
+
+    optionsLayout->addRow(QString(), new QLabel(this));
+
+    // Icon and reset button
+    QHBoxLayout *hb = new QHBoxLayout;
+    hb->setContentsMargins(0, 0, 0, 0);
+
+    m_iconButton = new KIconButton(this);
+    m_iconButton->setIconSize(KIconLoader::SizeSmall);
+    hb->addWidget(m_iconButton);
+
+    QPushButton *resetButton = new QPushButton(this);
+    KStandardGuiItem::assign(resetButton, KStandardGuiItem::Reset);
+    resetButton->setToolTip(i18n("Reset the icon to the default for the command"));
+    connect(resetButton, &QAbstractButton::clicked, this, [this]() {
+        setIconForCommand(&m_command);
+        m_iconButton->setIcon(m_command.icon);
+    });
+    hb->addWidget(resetButton);
+    optionsLayout->addRow(i18n("Icon:"), hb);
+
+    // Main dialogue layout
+    QVBoxLayout *mainLayout = new QVBoxLayout(this);
+    mainLayout->addWidget(optionsWidget);
+    mainLayout->addStretch();
+    mainLayout->addWidget(buttons);
+
+    (void)winId();
+    windowHandle()->resize(560, 440); // default, if there is no saved size
+    const KConfigGroup grp = KSharedConfig::openConfig()->group(metaObject()->className());
+    KWindowConfig::restoreWindowSize(windowHandle(), grp);
+    resize(windowHandle()->size());
+
+    updateWidgets();
+}
+
+void EditCommandDialog::slotUpdateButtons()
+{
+    m_okButton->setEnabled(!m_commandEdit->text().isEmpty() && !m_descriptionEdit->text().isEmpty());
+}
+
+void EditCommandDialog::updateWidgets()
+{
+    m_commandEdit->setText(m_command.command);
+    m_descriptionEdit->setText(m_command.description);
+
+    m_replaceRadio->setChecked(m_command.output == ClipCommand::REPLACE);
+    m_appendRadio->setChecked(m_command.output == ClipCommand::ADD);
+    m_ignoreRadio->setChecked(m_command.output == ClipCommand::IGNORE);
+
+    m_iconButton->setIcon(m_command.icon);
+
+    slotUpdateButtons();
+}
+
+void EditCommandDialog::saveCommand()
+{
+    m_command.command = m_commandEdit->text();
+    m_command.description = m_descriptionEdit->text();
+
+    if (m_replaceRadio->isChecked()) {
+        m_command.output = ClipCommand::REPLACE;
+    } else if (m_appendRadio->isChecked()) {
+        m_command.output = ClipCommand::ADD;
+    } else {
+        m_command.output = ClipCommand::IGNORE;
+    }
+
+    const QString icon = m_iconButton->icon();
+    if (!icon.isEmpty()) {
+        m_command.icon = icon;
+    } else {
+        setIconForCommand(&m_command);
+    }
+}
+
+void EditCommandDialog::slotAccepted()
+{
+    saveCommand();
+
+    KConfigGroup grp = KSharedConfig::openConfig()->group(metaObject()->className());
+    KWindowConfig::saveWindowSize(windowHandle(), grp);
+    accept();
+}
diff --git a/klipper/editcommanddialog.h b/klipper/editcommanddialog.h
new file mode 100644
index 000000000..4f22fbdce
--- /dev/null
+++ b/klipper/editcommanddialog.h
@@ -0,0 +1,59 @@
+/*
+    SPDX-FileCopyrightText: 2022 Jonathan Marten <jjm at keelhaul.me.uk>
+    SPDX-License-Identifier: GPL-2.0-or-later
+*/
+
+#pragma once
+
+#include <qdialog.h>
+
+#include "urlgrabber.h"
+
+class QLineEdit;
+class QRadioButton;
+class KIconButton;
+
+class EditCommandDialog : public QDialog
+{
+    Q_OBJECT
+
+public:
+    explicit EditCommandDialog(const ClipCommand &command, QWidget *parent);
+    ~EditCommandDialog() override = default;
+
+    /**
+     * Retrieves the updated command
+     */
+    const ClipCommand &command() const
+    {
+        return (m_command);
+    }
+
+private Q_SLOTS:
+    void slotAccepted();
+    void slotUpdateButtons();
+
+private:
+    /**
+     * Updates dialog's widgets according to values in m_command
+     */
+    void updateWidgets();
+
+    /**
+     * Saves values from widgets to m_command
+     */
+    void saveCommand();
+
+private:
+    ClipCommand m_command;
+
+    QLineEdit *m_commandEdit;
+    QLineEdit *m_descriptionEdit;
+
+    QRadioButton *m_ignoreRadio;
+    QRadioButton *m_appendRadio;
+    QRadioButton *m_replaceRadio;
+    KIconButton *m_iconButton;
+
+    QPushButton *m_okButton;
+};
diff --git a/klipper/klipper.cpp b/klipper/klipper.cpp
index d8c556deb..d8ee8553a 100644
--- a/klipper/klipper.cpp
+++ b/klipper/klipper.cpp
@@ -13,16 +13,21 @@
 #include <zlib.h>
 
 #include "klipper_debug.h"
+#include <QApplication>
+#include <QBoxLayout>
 #include <QDBusConnection>
 #include <QDialog>
 #include <QDir>
+#include <QLabel>
 #include <QMenu>
 #include <QMessageBox>
+#include <QPushButton>
 #include <QSaveFile>
 #include <QtConcurrent>
 
 #include <KActionCollection>
 #include <KGlobalAccel>
+#include <KLocalizedString>
 #include <KMessageBox>
 #include <KNotification>
 #include <KSystemClipboard>
diff --git a/klipper/klipper.kcfg b/klipper/klipper.kcfg
index 9d654d92b..ef3b07443 100644
--- a/klipper/klipper.kcfg
+++ b/klipper/klipper.kcfg
@@ -74,7 +74,13 @@
         <label>Dummy entry for indicating changes in an action's tree widget</label>
         <default>-1</default>
     </entry>
-    <entry name="PopupInfoMessageHidden" type="Bool">
+    <entry name="PopupInfoMessageShown" type="Bool">
+        <default>true</default>
+    </entry>
+    <entry name="ActionsInfoMessageShown" type="Bool">
+        <default>true</default>
+    </entry>
+    <entry name="ActionsInfoMessageHidden" type="Bool">
         <default>false</default>
     </entry>
   </group>


More information about the kde-doc-english mailing list