Commit 3dfe82e6 authored by Jonathan Marten's avatar Jonathan Marten
Browse files

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:
parent 9bd533c6
Pipeline #177989 passed with stage
in 11 minutes and 6 seconds
......@@ -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)
......
<?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. &quot;%s&quot; in a command will be replaced with the clipboard contents.&lt;br&gt;For more information about regular expressions, you could have a look at the &lt;a href=&quot;https://en.wikipedia.org/wiki/Regular_expression&quot;&gt;Wikipedia entry about this topic&lt;/a&gt;.</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>
This diff is collapsed.
......@@ -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;
......
......@@ -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));