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

0001 /*
0002     SPDX-FileCopyrightText: 2009 Dmitry Suzdalev <dimsuz@gmail.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "editactiondialog.h"
0008 
0009 #include <QDialogButtonBox>
0010 #include <qcheckbox.h>
0011 #include <qcoreapplication.h>
0012 #include <qformlayout.h>
0013 #include <qgridlayout.h>
0014 #include <qheaderview.h>
0015 #include <qlabel.h>
0016 #include <qlineedit.h>
0017 #include <qpushbutton.h>
0018 #include <qtableview.h>
0019 #include <qwindow.h>
0020 
0021 #include <klocalizedstring.h>
0022 #include <kmessagebox.h>
0023 #include <kwindowconfig.h>
0024 
0025 #include "klipper_debug.h"
0026 
0027 #include "configdialog.h"
0028 #include "editcommanddialog.h"
0029 
0030 static QString output2text(ClipCommand::Output output)
0031 {
0032     switch (output) {
0033     case ClipCommand::IGNORE:
0034         return QString(i18n("Ignore"));
0035     case ClipCommand::REPLACE:
0036         return QString(i18n("Replace Clipboard"));
0037     case ClipCommand::ADD:
0038         return QString(i18n("Add to Clipboard"));
0039     }
0040     return QString();
0041 }
0042 
0043 //////////////////////////
0044 //  ActionDetailModel   //
0045 //////////////////////////
0046 
0047 class ActionDetailModel : public QAbstractTableModel
0048 {
0049 public:
0050     explicit ActionDetailModel(ClipAction *action, QObject *parent = nullptr);
0051     QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
0052     Qt::ItemFlags flags(const QModelIndex &index) const override;
0053     int rowCount(const QModelIndex &parent = QModelIndex()) const override;
0054     int columnCount(const QModelIndex &parent) const override;
0055     QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
0056     const QList<ClipCommand> &commands() const
0057     {
0058         return m_commands;
0059     }
0060     void addCommand(const ClipCommand &command);
0061     void removeCommand(const QModelIndex &idx);
0062     void replaceCommand(const ClipCommand &command, const QModelIndex &idx);
0063 
0064 private:
0065     enum column_t { COMMAND_COL = 0, OUTPUT_COL = 1, DESCRIPTION_COL = 2 };
0066     QList<ClipCommand> m_commands;
0067     QVariant displayData(ClipCommand *command, column_t column) const;
0068     QVariant decorationData(ClipCommand *command, column_t column) const;
0069 };
0070 
0071 ActionDetailModel::ActionDetailModel(ClipAction *action, QObject *parent)
0072     : QAbstractTableModel(parent)
0073     , m_commands(action->commands())
0074 {
0075 }
0076 
0077 Qt::ItemFlags ActionDetailModel::flags(const QModelIndex & /*index*/) const
0078 {
0079     return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
0080 }
0081 
0082 int ActionDetailModel::columnCount(const QModelIndex & /*parent*/) const
0083 {
0084     return 3;
0085 }
0086 
0087 int ActionDetailModel::rowCount(const QModelIndex &) const
0088 {
0089     return m_commands.count();
0090 }
0091 
0092 QVariant ActionDetailModel::displayData(ClipCommand *command, ActionDetailModel::column_t column) const
0093 {
0094     switch (column) {
0095     case COMMAND_COL:
0096         return command->command;
0097     case OUTPUT_COL:
0098         return output2text(command->output);
0099     case DESCRIPTION_COL:
0100         return command->description;
0101     }
0102     return QVariant();
0103 }
0104 
0105 QVariant ActionDetailModel::decorationData(ClipCommand *command, ActionDetailModel::column_t column) const
0106 {
0107     switch (column) {
0108     case COMMAND_COL:
0109         return command->icon.isEmpty() ? QIcon::fromTheme(QStringLiteral("system-run")) : QIcon::fromTheme(command->icon);
0110     case OUTPUT_COL:
0111     case DESCRIPTION_COL:
0112         break;
0113     }
0114     return QVariant();
0115 }
0116 
0117 QVariant ActionDetailModel::headerData(int section, Qt::Orientation orientation, int role) const
0118 {
0119     if (orientation == Qt::Horizontal && role == Qt::DisplayRole) {
0120         switch (static_cast<column_t>(section)) {
0121         case COMMAND_COL:
0122             return i18n("Command");
0123         case OUTPUT_COL:
0124             return i18n("Output");
0125         case DESCRIPTION_COL:
0126             return i18n("Description");
0127         }
0128     }
0129     return QAbstractTableModel::headerData(section, orientation, role);
0130 }
0131 
0132 QVariant ActionDetailModel::data(const QModelIndex &index, int role) const
0133 {
0134     const int column = index.column();
0135     const int row = index.row();
0136     ClipCommand cmd = m_commands.at(row);
0137     switch (role) {
0138     case Qt::DisplayRole:
0139         return displayData(&cmd, static_cast<column_t>(column));
0140     case Qt::DecorationRole:
0141         return decorationData(&cmd, static_cast<column_t>(column));
0142     }
0143     return QVariant();
0144 }
0145 
0146 void ActionDetailModel::addCommand(const ClipCommand &command)
0147 {
0148     beginInsertRows(QModelIndex(), rowCount(), rowCount());
0149     m_commands << command;
0150     endInsertRows();
0151 }
0152 
0153 void ActionDetailModel::replaceCommand(const ClipCommand &command, const QModelIndex &idx)
0154 {
0155     if (!idx.isValid())
0156         return;
0157     const int row = idx.row();
0158     m_commands[row] = command;
0159     emit dataChanged(index(row, static_cast<int>(COMMAND_COL)), index(row, static_cast<int>(DESCRIPTION_COL)));
0160 }
0161 
0162 void ActionDetailModel::removeCommand(const QModelIndex &idx)
0163 {
0164     if (!idx.isValid())
0165         return;
0166     const int row = idx.row();
0167     beginRemoveRows(QModelIndex(), row, row);
0168     m_commands.removeAt(row);
0169     endRemoveRows();
0170 }
0171 
0172 //////////////////////////
0173 //  EditActionDialog    //
0174 //////////////////////////
0175 
0176 EditActionDialog::EditActionDialog(QWidget *parent)
0177     : QDialog(parent)
0178 {
0179     setWindowTitle(i18n("Action Properties"));
0180     QDialogButtonBox *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
0181     buttons->button(QDialogButtonBox::Ok)->setShortcut(Qt::CTRL | Qt::Key_Return);
0182     connect(buttons, &QDialogButtonBox::accepted, this, &EditActionDialog::slotAccepted);
0183     connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
0184 
0185     // Upper widget: pattern, description and options
0186     QWidget *optionsWidget = new QWidget(this);
0187     QFormLayout *optionsLayout = new QFormLayout(optionsWidget);
0188 
0189     // General information label
0190     QLabel *hint = ConfigDialog::createHintLabel(xi18nc("@info",
0191                                                         "An action takes effect when its \
0192 <interface>match pattern</interface> matches the clipboard contents. \
0193 When this happens, the action's <interface>commands</interface> appear \
0194 in the Klipper popup menu; if one of them is chosen, \
0195 the command is executed."),
0196                                                  this);
0197     optionsLayout->addRow(hint);
0198     optionsLayout->addRow(QString(), new QLabel(optionsWidget));
0199 
0200     // Pattern (regular expression)
0201     m_regExpEdit = new QLineEdit(optionsWidget);
0202     m_regExpEdit->setClearButtonEnabled(true);
0203     m_regExpEdit->setPlaceholderText(i18n("Enter a pattern to match against the clipboard"));
0204 
0205     optionsLayout->addRow(i18n("Match pattern:"), m_regExpEdit);
0206 
0207     hint = ConfigDialog::createHintLabel(xi18nc("@info",
0208                                                 "The match pattern is a regular expression. \
0209 For more information see the \
0210 <link url=\"https://en.wikipedia.org/wiki/Regular_expression\">Wikipedia entry</link> \
0211 for this topic."),
0212                                          this);
0213     hint->setOpenExternalLinks(true);
0214     optionsLayout->addRow(QString(), hint);
0215 
0216     // Description
0217     m_descriptionEdit = new QLineEdit(optionsWidget);
0218     m_descriptionEdit->setClearButtonEnabled(true);
0219     m_descriptionEdit->setPlaceholderText(i18n("Enter a description for the action"));
0220     optionsLayout->addRow(i18n("Description:"), m_descriptionEdit);
0221 
0222     // Include in automatic popup
0223     m_automaticCheck = new QCheckBox(i18n("Include in automatic popup"), optionsWidget);
0224     optionsLayout->addRow(QString(), m_automaticCheck);
0225 
0226     hint = ConfigDialog::createHintLabel(xi18nc("@info",
0227                                                 "The commands \
0228 for this match will be included in the automatic action popup, if it is enabled in \
0229 the <interface>Action Menu</interface> page. If this option is turned off, the commands for \
0230 this match will not be included in the automatic popup but they will be included if the \
0231 popup is activated manually with the <shortcut>%1</shortcut> key shortcut.",
0232                                                 ConfigDialog::manualShortcutString()),
0233                                          this);
0234     optionsLayout->addRow(QString(), hint);
0235 
0236     optionsLayout->addRow(QString(), new QLabel(optionsWidget));
0237 
0238     // Lower widget: command list and action buttons
0239     QWidget *listWidget = new QWidget(this);
0240     QGridLayout *listLayout = new QGridLayout(listWidget);
0241     listLayout->setContentsMargins(0, 0, 0, 0);
0242 
0243     // Command list
0244     m_commandList = new QTableView(listWidget);
0245     m_commandList->setAlternatingRowColors(true);
0246     m_commandList->setSelectionMode(QAbstractItemView::SingleSelection);
0247     m_commandList->setSelectionBehavior(QAbstractItemView::SelectRows);
0248     m_commandList->setShowGrid(false);
0249     m_commandList->setWordWrap(false);
0250     m_commandList->horizontalHeader()->setStretchLastSection(true);
0251     m_commandList->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft);
0252     m_commandList->verticalHeader()->setVisible(false);
0253     // For some reason, the default row height is 30 pixels.
0254     // Set it to the minimumSectionSize instead,
0255     // which is the font height+struts.
0256     m_commandList->verticalHeader()->setDefaultSectionSize(m_commandList->verticalHeader()->minimumSectionSize());
0257 
0258     listLayout->addWidget(m_commandList, 0, 0, 1, -1);
0259     listLayout->setRowStretch(0, 1);
0260 
0261     // "Add" button
0262     m_addCommandPb = new QPushButton(QIcon::fromTheme(QStringLiteral("list-add")), i18n("Add Command..."), listWidget);
0263     connect(m_addCommandPb, &QPushButton::clicked, this, &EditActionDialog::onAddCommand);
0264     listLayout->addWidget(m_addCommandPb, 1, 0);
0265 
0266     // "Edit" button
0267     m_editCommandPb = new QPushButton(QIcon::fromTheme(QStringLiteral("document-edit")), i18n("Edit Command..."), this);
0268     connect(m_editCommandPb, &QPushButton::clicked, this, &EditActionDialog::onEditCommand);
0269     listLayout->addWidget(m_editCommandPb, 1, 1);
0270     listLayout->setColumnStretch(2, 1);
0271 
0272     // "Delete" button
0273     m_removeCommandPb = new QPushButton(QIcon::fromTheme(QStringLiteral("list-remove")), i18n("Delete Command"), this);
0274     connect(m_removeCommandPb, &QPushButton::clicked, this, &EditActionDialog::onRemoveCommand);
0275     listLayout->addWidget(m_removeCommandPb, 1, 3);
0276 
0277     // Add some vertical space between our buttons and the dialogue buttons
0278     listLayout->setRowMinimumHeight(2, 16);
0279 
0280     // Main dialogue layout
0281     QVBoxLayout *mainLayout = new QVBoxLayout(this);
0282     mainLayout->addWidget(optionsWidget);
0283     mainLayout->addWidget(listWidget);
0284     mainLayout->setStretch(1, 1);
0285     mainLayout->addWidget(buttons);
0286 
0287     (void)winId();
0288     windowHandle()->resize(540, 560); // default, if there is no saved size
0289     const KConfigGroup grp = KSharedConfig::openConfig()->group(metaObject()->className());
0290     KWindowConfig::restoreWindowSize(windowHandle(), grp);
0291     resize(windowHandle()->size());
0292 
0293     QByteArray hdrState = grp.readEntry("ColumnState", QByteArray());
0294     if (!hdrState.isEmpty()) {
0295         qCDebug(KLIPPER_LOG) << "Restoring column state";
0296         m_commandList->horizontalHeader()->restoreState(QByteArray::fromBase64(hdrState));
0297     }
0298     // do this after restoreState()
0299     m_commandList->horizontalHeader()->setHighlightSections(false);
0300 }
0301 
0302 void EditActionDialog::setAction(ClipAction *act, int commandIdxToSelect)
0303 {
0304     m_action = act;
0305     m_model = new ActionDetailModel(act, this);
0306     m_commandList->setModel(m_model);
0307     connect(m_commandList->selectionModel(), &QItemSelectionModel::selectionChanged, this, &EditActionDialog::onSelectionChanged);
0308     connect(m_commandList, &QAbstractItemView::doubleClicked, this, &EditActionDialog::onEditCommand);
0309     updateWidgets(commandIdxToSelect);
0310 }
0311 
0312 void EditActionDialog::updateWidgets(int commandIdxToSelect)
0313 {
0314     if (!m_action) {
0315         qCDebug(KLIPPER_LOG) << "no action to edit was set";
0316         return;
0317     }
0318 
0319     m_regExpEdit->setText(m_action->actionRegexPattern());
0320     m_descriptionEdit->setText(m_action->description());
0321     m_automaticCheck->setChecked(m_action->automatic());
0322 
0323     if (commandIdxToSelect != -1) {
0324         m_commandList->setCurrentIndex(m_model->index(commandIdxToSelect, 0));
0325     }
0326 
0327     onSelectionChanged(); // update Remove/Edit buttons
0328 }
0329 
0330 void EditActionDialog::saveAction()
0331 {
0332     if (!m_action) {
0333         qCDebug(KLIPPER_LOG) << "no action to edit was set";
0334         return;
0335     }
0336 
0337     m_action->setActionRegexPattern(m_regExpEdit->text());
0338     m_action->setDescription(m_descriptionEdit->text());
0339     m_action->setAutomatic(m_automaticCheck->isChecked());
0340 
0341     m_action->clearCommands();
0342 
0343     foreach (const ClipCommand &cmd, m_model->commands()) {
0344         m_action->addCommand(cmd);
0345     }
0346 }
0347 
0348 void EditActionDialog::slotAccepted()
0349 {
0350     saveAction();
0351 
0352     qCDebug(KLIPPER_LOG) << "Saving dialogue state";
0353     KConfigGroup grp = KSharedConfig::openConfig()->group(metaObject()->className());
0354     KWindowConfig::saveWindowSize(windowHandle(), grp);
0355     grp.writeEntry("ColumnState", m_commandList->horizontalHeader()->saveState().toBase64());
0356     accept();
0357 }
0358 
0359 void EditActionDialog::onAddCommand()
0360 {
0361     ClipCommand command(QString(), QString(), true, QLatin1String(""));
0362     EditCommandDialog dlg(command, this);
0363     if (dlg.exec() != QDialog::Accepted)
0364         return;
0365     m_model->addCommand(dlg.command());
0366 }
0367 
0368 void EditActionDialog::onEditCommand()
0369 {
0370     QPersistentModelIndex commandIndex(m_commandList->selectionModel()->currentIndex());
0371     if (!commandIndex.isValid())
0372         return;
0373 
0374     EditCommandDialog dlg(m_model->commands().at(commandIndex.row()), this);
0375     if (dlg.exec() != QDialog::Accepted)
0376         return;
0377     m_model->replaceCommand(dlg.command(), commandIndex);
0378 }
0379 
0380 void EditActionDialog::onRemoveCommand()
0381 {
0382     QPersistentModelIndex commandIndex(m_commandList->selectionModel()->currentIndex());
0383     if (!commandIndex.isValid())
0384         return;
0385 
0386     if (KMessageBox::warningContinueCancel(
0387             this,
0388             xi18nc("@info", "Delete the selected command <resource>%1</resource>?", m_model->commands().at(commandIndex.row()).description),
0389             i18n("Confirm Delete Command"),
0390             KStandardGuiItem::del(),
0391             KStandardGuiItem::cancel(),
0392             QStringLiteral("deleteCommand"),
0393             KMessageBox::Dangerous)
0394         == KMessageBox::Continue) {
0395         m_model->removeCommand(commandIndex);
0396     }
0397 }
0398 
0399 void EditActionDialog::onSelectionChanged()
0400 {
0401     const bool itemIsSelected = (m_commandList->selectionModel() && m_commandList->selectionModel()->hasSelection());
0402     m_removeCommandPb->setEnabled(itemIsSelected);
0403     m_editCommandPb->setEnabled(itemIsSelected);
0404 }