File indexing completed on 2024-04-28 16:54:23

0001 /*
0002     SPDX-FileCopyrightText: 2000 Carsten Pfeiffer <pfeiffer@kde.org>
0003     SPDX-FileCopyrightText: 2008-2009 Dmitry Suzdalev <dimsuz@gmail.com>
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "configdialog.h"
0009 
0010 #include <qbuttongroup.h>
0011 #include <qcheckbox.h>
0012 #include <qfontdatabase.h>
0013 #include <qformlayout.h>
0014 #include <qgridlayout.h>
0015 #include <qheaderview.h>
0016 #include <qlabel.h>
0017 #include <qpushbutton.h>
0018 #include <qradiobutton.h>
0019 #include <qtooltip.h>
0020 #include <qwindow.h>
0021 
0022 #include <KConfigSkeleton>
0023 #include <KEditListWidget>
0024 #include <KShortcutsEditor>
0025 #include <kconfigskeleton.h>
0026 #include <kglobalaccel.h>
0027 #include <kmessagebox.h>
0028 #include <kmessagewidget.h>
0029 #include <kpluralhandlingspinbox.h>
0030 #include <kwindowconfig.h>
0031 
0032 #include "klipper_debug.h"
0033 
0034 #include "actionstreewidget.h"
0035 #include "editactiondialog.h"
0036 #include "klipper.h"
0037 #include "klippersettings.h"
0038 
0039 /* static */ QLabel *ConfigDialog::createHintLabel(const QString &text, QWidget *parent)
0040 {
0041     QLabel *hintLabel = new QLabel(text, parent);
0042     hintLabel->setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont));
0043     hintLabel->setWordWrap(true);
0044     hintLabel->setAlignment(Qt::AlignLeft | Qt::AlignTop);
0045 
0046     // The minimum width needs to be set so that QLabel will wrap the text
0047     // to fill that width.  Otherwise, it will try to adjust the text wrapping
0048     // width so that the text is laid out in a pleasing looking rectangle,
0049     // which unfortunately leaves a lot of white space below the actual text.
0050     // See the "tryWidth" block in QLabelPrivate::sizeForWidth().
0051     hintLabel->setMinimumWidth(400);
0052 
0053     return (hintLabel);
0054 }
0055 
0056 /* static */ QLabel *ConfigDialog::createHintLabel(const KConfigSkeletonItem *item, QWidget *parent)
0057 {
0058     return (createHintLabel(item->whatsThis(), parent));
0059 }
0060 
0061 /* static */ QString ConfigDialog::manualShortcutString()
0062 {
0063     const QList<QKeySequence> keys = KGlobalAccel::self()->globalShortcut(QCoreApplication::applicationName(), QStringLiteral("repeat_action"));
0064     return (keys.value(0).toString(QKeySequence::NativeText));
0065 }
0066 
0067 //////////////////////////
0068 //  GeneralWidget   //
0069 //////////////////////////
0070 
0071 GeneralWidget::GeneralWidget(QWidget *parent)
0072     : QWidget(parent)
0073 {
0074     QFormLayout *layout = new QFormLayout(this);
0075 
0076     // Synchronise selection and clipboard
0077     const KConfigSkeletonItem *item = KlipperSettings::self()->syncClipboardsItem();
0078     m_syncClipboardsCb = new QCheckBox(item->label(), this);
0079     m_syncClipboardsCb->setObjectName(QLatin1String("kcfg_SyncClipboards"));
0080     layout->addRow(i18n("Selection and Clipboard:"), m_syncClipboardsCb);
0081 
0082     QLabel *hint = ConfigDialog::createHintLabel(item, this);
0083     layout->addRow(QString(), hint);
0084     connect(hint, &QLabel::linkActivated, this, [this, hint]() {
0085         QToolTip::showText(QCursor::pos(),
0086                            xi18nc("@info:tooltip",
0087                                   "When text or an area of the screen is highlighted with the mouse or keyboard, \
0088 this is the <emphasis>selection</emphasis>. It can be pasted using the middle mouse button.\
0089 <nl/>\
0090 <nl/>\
0091 If the selection is explicitly copied using a <interface>Copy</interface> or <interface>Cut</interface> action, \
0092 it is saved to the <emphasis>clipboard</emphasis>. It can be pasted using a <interface>Paste</interface> action. \
0093 <nl/>\
0094 <nl/>\
0095 When turned on this option keeps the selection and the clipboard the same, so that any selection is immediately available to paste by any means. \
0096 If it is turned off, the selection may still be saved in the clipboard history (subject to the options below), but it can only be pasted using the middle mouse button."),
0097                            hint);
0098     });
0099 
0100     layout->addRow(QString(), new QLabel(this));
0101 
0102     // Retain clipboard history
0103     item = KlipperSettings::self()->keepClipboardContentsItem();
0104     m_enableHistoryCb = new QCheckBox(item->label(), this);
0105     m_enableHistoryCb->setObjectName(QLatin1String("kcfg_KeepClipboardContents"));
0106     layout->addRow(i18n("Clipboard history:"), m_enableHistoryCb);
0107 
0108     // Clipboard history size
0109     item = KlipperSettings::self()->maxClipItemsItem();
0110     m_historySizeSb = new KPluralHandlingSpinBox(this);
0111     m_historySizeSb->setObjectName(QLatin1String("kcfg_MaxClipItems"));
0112     m_historySizeSb->setSuffix(ki18ncp("Number of entries", " entry", " entries"));
0113     layout->addRow(item->label(), m_historySizeSb);
0114 
0115     layout->addRow(QString(), new QLabel(this));
0116 
0117     // Radio button group: Storing text selections in history
0118     //
0119     // These two options correspond to the 'ignoreSelection' internal
0120     // Klipper setting.
0121     //
0122     // The 'Always' option is not available if selection/clipboard synchronisation
0123     // is turned off - in this case the selection is never automatically saved
0124     // in the clipboard history.
0125 
0126     QButtonGroup *buttonGroup = new QButtonGroup(this);
0127 
0128     // This widget is not managed by KConfigDialogManager, but
0129     // the other radio button is.  That is sufficient for the
0130     // manager to handle widget changes, the Apply button etc.
0131     m_alwaysTextRb = new QRadioButton(i18n("Always save in history"), this);
0132     m_alwaysTextRb->setChecked(true); // may be updated from settings later
0133     connect(m_alwaysTextRb, &QAbstractButton::toggled, this, &GeneralWidget::widgetChanged);
0134     buttonGroup->addButton(m_alwaysTextRb);
0135     layout->addRow(i18n("Text selection:"), m_alwaysTextRb);
0136 
0137     m_copiedTextRb = new QRadioButton(i18n("Only when explicitly copied"), this);
0138     m_copiedTextRb->setObjectName(QLatin1String("kcfg_IgnoreSelection"));
0139     buttonGroup->addButton(m_copiedTextRb);
0140     layout->addRow(QString(), m_copiedTextRb);
0141 
0142     layout->addRow(QString(), ConfigDialog::createHintLabel(i18n("Whether text selections are saved in the clipboard history."), this));
0143 
0144     // Radio button group: Storing non-text selections in history
0145     //
0146     // The truth table for the 4 possible combinations of internal Klipper
0147     // settings (of which two are equivalent, making 3 user-visible options)
0148     // controlling what is stored in the clipboard history is:
0149     //
0150     // selectionTextOnly  ignoreImages   Selected   Selected    Copied    Copied      Option
0151     //                                     text    image/other   text   image/other
0152     //
0153     //        false          false          yes       yes         yes       yes         1
0154     //        true           false          yes       no          yes       yes         2
0155     //        false          true           yes       no          yes       no          3
0156     //        true           true           yes       no          yes       no          3
0157     //
0158     // Option 1:  Always store images in history
0159     //        2:  Only when explicitly copied
0160     //        3:  Never store images in history
0161     //
0162     // The 'Always' option is not available if selection/clipboard synchronisation
0163     // is turned off.
0164 
0165     buttonGroup = new QButtonGroup(this);
0166 
0167     // This widget is not managed by KConfigDialogManager,
0168     // but the other two radio buttons are.
0169     m_alwaysImageRb = new QRadioButton(i18n("Always save in history"), this);
0170     m_alwaysImageRb->setChecked(true); // may be updated from settings later
0171     connect(m_alwaysImageRb, &QAbstractButton::toggled, this, &GeneralWidget::widgetChanged);
0172     buttonGroup->addButton(m_alwaysImageRb);
0173     layout->addRow(i18n("Non-text selection:"), m_alwaysImageRb);
0174 
0175     m_copiedImageRb = new QRadioButton(i18n("Only when explicitly copied"), this);
0176     m_copiedImageRb->setObjectName(QLatin1String("kcfg_SelectionTextOnly"));
0177     buttonGroup->addButton(m_copiedImageRb);
0178     layout->addRow(QString(), m_copiedImageRb);
0179 
0180     m_neverImageRb = new QRadioButton(i18n("Never save in history"), this);
0181     m_neverImageRb->setObjectName(QLatin1String("kcfg_IgnoreImages"));
0182     buttonGroup->addButton(m_neverImageRb);
0183     layout->addRow(QString(), m_neverImageRb);
0184 
0185     layout->addRow(QString(), ConfigDialog::createHintLabel(i18n("Whether non-text selections (such as images) are saved in the clipboard history."), this));
0186 
0187     m_havePrevAlwaysImageTextConfig = false;
0188 }
0189 
0190 void GeneralWidget::updateWidgets()
0191 {
0192     // Initialise widgets which are not managed by KConfigDialogManager
0193     // from the application settings.
0194 
0195     // SelectionTextOnly takes precedence over IgnoreImages,
0196     // see Klipper::checkClipData().  Give that radio button
0197     // priority too.
0198     if (KlipperSettings::selectionTextOnly()) {
0199         KlipperSettings::setIgnoreImages(false);
0200     }
0201 }
0202 
0203 void GeneralWidget::initWidgetStates()
0204 {
0205     Q_ASSERT(!m_havePrevAlwaysImageTextConfig);
0206     // During dialog setup, disable / change some widgets according to current settings to achieve the same
0207     // internal consistency as settings changes made by the user after opening the dialog.
0208     slotWidgetModified();
0209     m_havePrevAlwaysImageTextConfig = false;
0210 }
0211 
0212 void GeneralWidget::slotWidgetModified()
0213 {
0214     // A setting widget has been changed.  Update the state of
0215     // any other widgets that depend on it.
0216 
0217     if (m_syncClipboardsCb->isChecked()) {
0218         m_alwaysImageRb->setEnabled(true);
0219         m_alwaysTextRb->setEnabled(true);
0220         m_copiedTextRb->setEnabled(true);
0221 
0222         if (m_havePrevAlwaysImageTextConfig) {
0223             m_alwaysTextRb->setChecked(m_prevAlwaysText);
0224             m_alwaysImageRb->setChecked(m_prevAlwaysImage);
0225             m_havePrevAlwaysImageTextConfig = false;
0226         }
0227     } else {
0228         m_prevAlwaysText = m_alwaysTextRb->isChecked();
0229         m_prevAlwaysImage = m_alwaysImageRb->isChecked();
0230         m_havePrevAlwaysImageTextConfig = true;
0231 
0232         if (m_alwaysImageRb->isChecked()) {
0233             m_copiedImageRb->setChecked(true);
0234         }
0235 
0236         if (m_alwaysTextRb->isChecked()) {
0237             m_copiedTextRb->setChecked(true);
0238         }
0239 
0240         m_alwaysImageRb->setEnabled(false);
0241         m_alwaysTextRb->setEnabled(false);
0242         m_copiedTextRb->setEnabled(false);
0243     }
0244 }
0245 
0246 //////////////////////////
0247 //  PopupWidget     //
0248 //////////////////////////
0249 
0250 PopupWidget::PopupWidget(QWidget *parent)
0251     : QWidget(parent)
0252 {
0253     QFormLayout *layout = new QFormLayout(this);
0254 
0255     // Automatic popup
0256     const KConfigSkeletonItem *item = KlipperSettings::self()->uRLGrabberEnabledItem();
0257     m_enablePopupCb = new QCheckBox(item->label(), this);
0258     m_enablePopupCb->setObjectName(QLatin1String("kcfg_URLGrabberEnabled"));
0259     layout->addRow(i18n("Show action popup menu:"), m_enablePopupCb);
0260 
0261     // Replay from history popup
0262     item = KlipperSettings::self()->replayActionInHistoryItem();
0263     m_historyPopupCb = new QCheckBox(item->label(), this);
0264     m_historyPopupCb->setObjectName(QLatin1String("kcfg_ReplayActionInHistory"));
0265     layout->addRow(QString(), m_historyPopupCb);
0266 
0267     const QList<QKeySequence> keys = KGlobalAccel::self()->globalShortcut(QCoreApplication::applicationName(), QStringLiteral("repeat_action"));
0268     QLabel *hint = ConfigDialog::createHintLabel(xi18nc("@info",
0269                                                         "When text that matches an action pattern is selected or is chosen from \
0270 the clipboard history, automatically show the popup menu with applicable actions. \
0271 If the automatic menu is turned off here, or it is not shown for an excluded window, \
0272 then it can be shown by using the <shortcut>%1</shortcut> key shortcut.",
0273                                                         ConfigDialog::manualShortcutString()),
0274                                                  this);
0275     layout->addRow(QString(), hint);
0276 
0277     // Exclusions
0278     QPushButton *exclusionsButton = new QPushButton(QIcon::fromTheme(QStringLiteral("configure")), i18n("Exclude Windows..."), this);
0279     connect(exclusionsButton, &QPushButton::clicked, this, &PopupWidget::onAdvanced);
0280 
0281     // Right align the push button, regardless of the QFormLayout style
0282     QHBoxLayout *hb = new QHBoxLayout;
0283     hb->setContentsMargins(0, 0, 0, 0);
0284     hb->addStretch(1);
0285     hb->addWidget(exclusionsButton);
0286     layout->addRow(QString(), hb);
0287 
0288     // Action popup time
0289     item = KlipperSettings::self()->timeoutForActionPopupsItem();
0290     m_actionTimeoutSb = new KPluralHandlingSpinBox(this);
0291     m_actionTimeoutSb->setObjectName(QLatin1String("kcfg_TimeoutForActionPopups"));
0292     m_actionTimeoutSb->setSuffix(ki18ncp("Unit of time", " second", " seconds"));
0293     m_actionTimeoutSb->setSpecialValueText(i18nc("No timeout", "None"));
0294     layout->addRow(item->label(), m_actionTimeoutSb);
0295 
0296     layout->addRow(QString(), new QLabel(this));
0297 
0298     // Remove whitespace
0299     item = KlipperSettings::self()->stripWhiteSpaceItem();
0300     m_stripWhitespaceCb = new QCheckBox(item->label(), this);
0301     m_stripWhitespaceCb->setObjectName(QLatin1String("kcfg_StripWhiteSpace"));
0302     layout->addRow(i18n("Options:"), m_stripWhitespaceCb);
0303     layout->addRow(QString(), ConfigDialog::createHintLabel(item, this));
0304 
0305     // MIME actions
0306     item = KlipperSettings::self()->enableMagicMimeActionsItem();
0307     m_mimeActionsCb = new QCheckBox(item->label(), this);
0308     m_mimeActionsCb->setObjectName(QLatin1String("kcfg_EnableMagicMimeActions"));
0309     layout->addRow(QString(), m_mimeActionsCb);
0310     layout->addRow(QString(), ConfigDialog::createHintLabel(item, this));
0311 
0312     layout->addRow(QString(), new QLabel(this));
0313 }
0314 
0315 void PopupWidget::setExcludedWMClasses(const QStringList &excludedWMClasses)
0316 {
0317     m_exclWMClasses = excludedWMClasses;
0318 }
0319 
0320 QStringList PopupWidget::excludedWMClasses() const
0321 {
0322     return m_exclWMClasses;
0323 }
0324 
0325 void PopupWidget::onAdvanced()
0326 {
0327     QDialog dlg(this);
0328     dlg.setModal(true);
0329     dlg.setWindowTitle(i18n("Exclude Windows"));
0330     QDialogButtonBox *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dlg);
0331     buttons->button(QDialogButtonBox::Ok)->setShortcut(Qt::CTRL | Qt::Key_Return);
0332     connect(buttons, &QDialogButtonBox::accepted, &dlg, &QDialog::accept);
0333     connect(buttons, &QDialogButtonBox::rejected, &dlg, &QDialog::reject);
0334 
0335     AdvancedWidget *widget = new AdvancedWidget(&dlg);
0336     widget->setWMClasses(m_exclWMClasses);
0337 
0338     QVBoxLayout *layout = new QVBoxLayout(&dlg);
0339     layout->addWidget(widget);
0340     layout->addWidget(buttons);
0341 
0342     if (dlg.exec() == QDialog::Accepted) {
0343         m_exclWMClasses = widget->wmClasses();
0344     }
0345 }
0346 
0347 //////////////////////////
0348 //  ActionsWidget   //
0349 //////////////////////////
0350 
0351 ActionsWidget::ActionsWidget(QWidget *parent)
0352     : QWidget(parent)
0353 {
0354     QGridLayout *layout = new QGridLayout(this);
0355     layout->setContentsMargins(0, 0, 0, 0);
0356 
0357     // General information label
0358     QLabel *hint = ConfigDialog::createHintLabel(xi18nc("@info",
0359                                                         "When a <interface>match pattern</interface> \
0360 matches the clipboard contents, its <interface>commands</interface> \
0361 appear in the Klipper popup menu and can be executed."),
0362                                                  this);
0363     layout->addWidget(hint, 0, 0, 1, -1);
0364 
0365     // Scrolling list
0366     m_actionsTree = new ActionsTreeWidget(this);
0367     m_actionsTree->setColumnCount(2);
0368     m_actionsTree->setHeaderLabels({i18nc("@title:column", "Match pattern and commands"), i18nc("@title:column", "Description")});
0369 
0370     layout->addWidget(m_actionsTree, 1, 0, 1, -1);
0371     layout->setRowStretch(1, 1);
0372 
0373     // Action buttons
0374     m_addActionButton = new QPushButton(QIcon::fromTheme(QStringLiteral("list-add")), i18n("Add Action..."), this);
0375     connect(m_addActionButton, &QPushButton::clicked, this, &ActionsWidget::onAddAction);
0376     layout->addWidget(m_addActionButton, 2, 0);
0377 
0378     m_editActionButton = new QPushButton(QIcon::fromTheme(QStringLiteral("document-edit")), i18n("Edit Action..."), this);
0379     connect(m_editActionButton, &QPushButton::clicked, this, &ActionsWidget::onEditAction);
0380     layout->addWidget(m_editActionButton, 2, 1);
0381     layout->setColumnStretch(2, 1);
0382 
0383     m_deleteActionButton = new QPushButton(QIcon::fromTheme(QStringLiteral("list-remove")), i18n("Delete Action"), this);
0384     connect(m_deleteActionButton, &QPushButton::clicked, this, &ActionsWidget::onDeleteAction);
0385     layout->addWidget(m_deleteActionButton, 2, 3);
0386 
0387     // Where to configure the action options
0388     if (KlipperSettings::actionsInfoMessageShown()) {
0389         KMessageWidget *msg = new KMessageWidget(xi18nc("@info",
0390                                                         "These actions appear in the popup menu \
0391 which can be configured on the <interface>Action Menu</interface> page."),
0392                                                  this);
0393         msg->setMessageType(KMessageWidget::Information);
0394         msg->setIcon(QIcon::fromTheme(QStringLiteral("dialog-information")));
0395         msg->setWordWrap(true);
0396         msg->setCloseButtonVisible(true);
0397 
0398         connect(msg, &KMessageWidget::hideAnimationFinished, this, []() {
0399             KlipperSettings::setActionsInfoMessageShown(false);
0400         });
0401         layout->addWidget(msg, 3, 0, 1, -1);
0402     }
0403 
0404     // Add some vertical space between our buttons and the dialogue buttons
0405     layout->setRowMinimumHeight(4, 16);
0406 
0407     const KConfigGroup grp = KSharedConfig::openConfig()->group(metaObject()->className());
0408     QByteArray hdrState = grp.readEntry("ColumnState", QByteArray());
0409     if (!hdrState.isEmpty()) {
0410         qCDebug(KLIPPER_LOG) << "Restoring column state";
0411         m_actionsTree->header()->restoreState(QByteArray::fromBase64(hdrState));
0412     } else {
0413         m_actionsTree->header()->resizeSection(0, 250);
0414     }
0415 
0416     connect(m_actionsTree, &QTreeWidget::itemSelectionChanged, this, &ActionsWidget::onSelectionChanged);
0417     connect(m_actionsTree, &QTreeWidget::itemDoubleClicked, this, &ActionsWidget::onEditAction);
0418 
0419     onSelectionChanged();
0420 }
0421 
0422 void ActionsWidget::setActionList(const ActionList &list)
0423 {
0424     qDeleteAll(m_actionList);
0425     m_actionList.clear();
0426 
0427     for (const ClipAction *action : list) {
0428         if (!action) {
0429             qCDebug(KLIPPER_LOG) << "action is null!";
0430             continue;
0431         }
0432 
0433         // make a copy for us to work with from now on
0434         m_actionList.append(new ClipAction(*action));
0435     }
0436 
0437     updateActionListView();
0438 }
0439 
0440 void ActionsWidget::updateActionListView()
0441 {
0442     m_actionsTree->clear();
0443 
0444     for (const ClipAction *action : m_actionList) {
0445         if (!action) {
0446             qCDebug(KLIPPER_LOG) << "action is null!";
0447             continue;
0448         }
0449 
0450         QTreeWidgetItem *item = new QTreeWidgetItem;
0451         updateActionItem(item, action);
0452 
0453         m_actionsTree->addTopLevelItem(item);
0454     }
0455 
0456     // after all actions loaded, reset modified state of tree widget.
0457     // Needed because tree widget reacts on item changed events to tell if it is changed
0458     // this will ensure that apply button state will be correctly changed
0459     m_actionsTree->resetModifiedState();
0460 }
0461 
0462 void ActionsWidget::updateActionItem(QTreeWidgetItem *item, const ClipAction *action)
0463 {
0464     if (!item || !action) {
0465         qCDebug(KLIPPER_LOG) << "null pointer passed to function, nothing done";
0466         return;
0467     }
0468 
0469     // clear children if any
0470     item->takeChildren();
0471     item->setText(0, action->actionRegexPattern());
0472     item->setText(1, action->description());
0473 
0474     for (const ClipCommand &command : action->commands()) {
0475         QStringList cmdProps;
0476         cmdProps << command.command << command.description;
0477         QTreeWidgetItem *child = new QTreeWidgetItem(item, cmdProps);
0478         child->setIcon(0, QIcon::fromTheme(command.icon.isEmpty() ? QStringLiteral("system-run") : command.icon));
0479     }
0480 }
0481 
0482 ActionList ActionsWidget::actionList() const
0483 {
0484     // return a copy of our action list
0485     ActionList list;
0486     for (const ClipAction *action : m_actionList) {
0487         if (!action) {
0488             qCDebug(KLIPPER_LOG) << "action is null";
0489             continue;
0490         }
0491 
0492         list.append(new ClipAction(*action));
0493     }
0494 
0495     return list;
0496 }
0497 
0498 void ActionsWidget::resetModifiedState()
0499 {
0500     m_actionsTree->resetModifiedState();
0501 
0502     qCDebug(KLIPPER_LOG) << "Saving column state";
0503     KConfigGroup grp = KSharedConfig::openConfig()->group(metaObject()->className());
0504     grp.writeEntry("ColumnState", m_actionsTree->header()->saveState().toBase64());
0505 }
0506 
0507 void ActionsWidget::onSelectionChanged()
0508 {
0509     const bool itemIsSelected = !m_actionsTree->selectedItems().isEmpty();
0510     m_editActionButton->setEnabled(itemIsSelected);
0511     m_deleteActionButton->setEnabled(itemIsSelected);
0512 }
0513 
0514 void ActionsWidget::onAddAction()
0515 {
0516     EditActionDialog dlg(this);
0517     ClipAction *newAct = new ClipAction;
0518     dlg.setAction(newAct);
0519 
0520     if (dlg.exec() == QDialog::Accepted) {
0521         m_actionList.append(newAct);
0522 
0523         QTreeWidgetItem *item = new QTreeWidgetItem;
0524         updateActionItem(item, newAct);
0525         m_actionsTree->addTopLevelItem(item);
0526         emit widgetChanged();
0527     }
0528 }
0529 
0530 void ActionsWidget::onEditAction()
0531 {
0532     QTreeWidgetItem *item = m_actionsTree->currentItem();
0533     if (!item) {
0534         return;
0535     }
0536 
0537     int commandIdx = -1;
0538     if (item->parent()) {
0539         commandIdx = item->parent()->indexOfChild(item);
0540         item = item->parent(); // interested in toplevel action
0541     }
0542 
0543     int idx = m_actionsTree->indexOfTopLevelItem(item);
0544     ClipAction *action = m_actionList.at(idx);
0545 
0546     if (!action) {
0547         qCDebug(KLIPPER_LOG) << "action is null";
0548         return;
0549     }
0550 
0551     EditActionDialog dlg(this);
0552     dlg.setAction(action, commandIdx);
0553     // dialog will save values into action if user hits OK
0554     if (dlg.exec() == QDialog::Accepted) {
0555         updateActionItem(item, action);
0556         emit widgetChanged();
0557     }
0558 }
0559 
0560 void ActionsWidget::onDeleteAction()
0561 {
0562     QTreeWidgetItem *item = m_actionsTree->currentItem();
0563     if (!item) {
0564         return;
0565     }
0566 
0567     // If the item has a parent, then it is a command (the second level
0568     // of the tree).  Find the complete action.
0569     if (item->parent()) {
0570         item = item->parent();
0571     }
0572 
0573     if (KMessageBox::warningContinueCancel(this,
0574                                            xi18nc("@info", "Delete the selected action <resource>%1</resource><nl/>and all of its commands?", item->text(1)),
0575                                            i18n("Confirm Delete Action"),
0576                                            KStandardGuiItem::del(),
0577                                            KStandardGuiItem::cancel(),
0578                                            QStringLiteral("deleteAction"),
0579                                            KMessageBox::Dangerous)
0580         == KMessageBox::Continue) {
0581         int idx = m_actionsTree->indexOfTopLevelItem(item);
0582         m_actionList.removeAt(idx);
0583         delete item;
0584         emit widgetChanged();
0585     }
0586 }
0587 
0588 bool ActionsWidget::hasChanged() const
0589 {
0590     return (m_actionsTree->actionsChanged() != -1);
0591 }
0592 
0593 //////////////////////////
0594 //  ConfigDialog    //
0595 //////////////////////////
0596 
0597 ConfigDialog::ConfigDialog(QWidget *parent, KConfigSkeleton *skeleton, Klipper *klipper, KActionCollection *collection)
0598     : KConfigDialog(parent, QStringLiteral("preferences"), skeleton)
0599     , m_generalPage(new GeneralWidget(this))
0600     , m_popupPage(new PopupWidget(this))
0601     , m_actionsPage(new ActionsWidget(this))
0602     , m_klipper(klipper)
0603 {
0604     addPage(m_generalPage, i18nc("General Config", "General"), QStringLiteral("klipper"), i18n("General Configuration"));
0605     addPage(m_popupPage, i18nc("Popup Menu Config", "Action Menu"), QStringLiteral("open-menu-symbolic"), i18n("Action Menu"));
0606     addPage(m_actionsPage, i18nc("Actions Config", "Actions Configuration"), QStringLiteral("system-run"), i18n("Actions Configuration"));
0607 
0608     m_shortcutsWidget = new KShortcutsEditor(collection, this, KShortcutsEditor::GlobalAction);
0609     addPage(m_shortcutsWidget, i18nc("Shortcuts Config", "Shortcuts"), QStringLiteral("preferences-desktop-keyboard"), i18n("Shortcuts Configuration"));
0610 
0611     connect(m_generalPage, &GeneralWidget::widgetChanged, this, &ConfigDialog::settingsChangedSlot);
0612     connect(m_actionsPage, &ActionsWidget::widgetChanged, this, &ConfigDialog::settingsChangedSlot);
0613     connect(this, &KConfigDialog::widgetModified, m_generalPage, &GeneralWidget::slotWidgetModified);
0614     m_generalPage->initWidgetStates();
0615 
0616     // from KWindowConfig::restoreWindowSize() API documentation
0617     (void)winId();
0618     const KConfigGroup grp = KSharedConfig::openConfig()->group(metaObject()->className());
0619     KWindowConfig::restoreWindowSize(windowHandle(), grp);
0620     resize(windowHandle()->size());
0621 
0622     setMinimumHeight(550);
0623 }
0624 
0625 void ConfigDialog::updateSettings()
0626 {
0627     // The user clicked "OK" or "Apply".  Save the settings from the widgets
0628     // to the application settings.
0629 
0630     if (!m_klipper) {
0631         qCDebug(KLIPPER_LOG) << "Klipper object is null";
0632         return;
0633     }
0634 
0635     m_shortcutsWidget->save();
0636     m_actionsPage->resetModifiedState();
0637 
0638     m_klipper->setURLGrabberEnabled(KlipperSettings::uRLGrabberEnabled());
0639     m_klipper->urlGrabber()->setActionList(m_actionsPage->actionList());
0640     m_klipper->urlGrabber()->setExcludedWMClasses(m_popupPage->excludedWMClasses());
0641     m_klipper->saveSettings();
0642 
0643     KlipperSettings::self()->save();
0644 
0645     KConfigGroup grp = KSharedConfig::openConfig()->group("ConfigDialog");
0646     KWindowConfig::saveWindowSize(windowHandle(), grp);
0647 }
0648 
0649 void ConfigDialog::updateWidgets()
0650 {
0651     // The dialogue is being shown.  Initialise widgets which are not
0652     // managed by KConfigDialogManager from the application settings.
0653 
0654     if (m_klipper && m_klipper->urlGrabber()) {
0655         m_actionsPage->setActionList(m_klipper->urlGrabber()->actionList());
0656         m_popupPage->setExcludedWMClasses(m_klipper->urlGrabber()->excludedWMClasses());
0657     } else {
0658         qCDebug(KLIPPER_LOG) << "Klipper or grabber object is null";
0659         return;
0660     }
0661 
0662     m_generalPage->updateWidgets();
0663 }
0664 
0665 void ConfigDialog::updateWidgetsDefault()
0666 {
0667     // The user clicked "Defaults".  Restore the default values for
0668     // widgets which are not managed by KConfigDialogManager.  The
0669     // settings of "Actions Configuration" and "Excluded Windows"
0670     // are not reset to the default.
0671 
0672     m_shortcutsWidget->allDefault();
0673 }
0674 
0675 bool ConfigDialog::hasChanged()
0676 {
0677     return (m_actionsPage->hasChanged() || m_shortcutsWidget->isModified());
0678 }
0679 
0680 //////////////////////////
0681 //  AdvancedWidget  //
0682 //////////////////////////
0683 
0684 AdvancedWidget::AdvancedWidget(QWidget *parent)
0685     : QWidget(parent)
0686 {
0687     QVBoxLayout *mainLayout = new QVBoxLayout(this);
0688 
0689     QLabel *hint = ConfigDialog::createHintLabel(xi18nc("@info",
0690                                                         "The action popup will not be shown automatically for these windows, \
0691 even if it is enabled. This is because, for example, a web browser may highlight a URL \
0692 in the address bar while typing, so the menu would show for every keystroke.\
0693 <nl/>\
0694 <nl/>\
0695 If the action menu appears unexpectedly when using a particular application, then add it to this list. \
0696 <link>How to find the name to enter</link>."),
0697                                                  this);
0698 
0699     mainLayout->addWidget(hint);
0700     connect(hint, &QLabel::linkActivated, this, [this, hint]() {
0701         QToolTip::showText(QCursor::pos(),
0702                            xi18nc("@info:tooltip",
0703                                   "The name that needs to be entered here is the WM_CLASS name of the window to be excluded. \
0704 To find the WM_CLASS name for a window, in another terminal window enter the command:\
0705 <nl/>\
0706 <nl/>\
0707 &nbsp;&nbsp;<icode>xprop | grep WM_CLASS</icode>\
0708 <nl/>\
0709 <nl/>\
0710 and click on the window that you want to exclude. \
0711 The first name that it displays after the equal sign is the one that you need to enter."),
0712                            hint);
0713     });
0714 
0715     mainLayout->addWidget(hint);
0716     mainLayout->addWidget(new QLabel(this));
0717 
0718     m_editListBox = new KEditListWidget(this);
0719     m_editListBox->setButtons(KEditListWidget::Add | KEditListWidget::Remove);
0720     m_editListBox->setCheckAtEntering(true);
0721     mainLayout->addWidget(m_editListBox);
0722 
0723     m_editListBox->setFocus();
0724 }
0725 
0726 void AdvancedWidget::setWMClasses(const QStringList &items)
0727 {
0728     m_editListBox->setItems(items);
0729 }
0730 
0731 QStringList AdvancedWidget::wmClasses() const
0732 {
0733     return m_editListBox->items();
0734 }