diff --git a/klipper/clipcommandprocess.cpp b/klipper/clipcommandprocess.cpp index 493132058..4789d766f 100644 --- a/klipper/clipcommandprocess.cpp +++ b/klipper/clipcommandprocess.cpp @@ -1,83 +1,83 @@ /* Copyright 2009 Esben Mose Hansen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "clipcommandprocess.h" #include #include "history.h" #include "historystringitem.h" #include "urlgrabber.h" ClipCommandProcess::ClipCommandProcess(const ClipAction& action, const ClipCommand& command, const QString& clip, History* history, HistoryItemConstPtr original_item) : KProcess(), m_history(history), m_historyItem(original_item), m_newhistoryItem() { QHash map; map.insert( QLatin1Char('s'), clip ); // support %u, %U (indicates url param(s)) and %f, %F (file param(s)) map.insert( QLatin1Char('u'), clip ); map.insert( QLatin1Char('U'), clip ); map.insert( QLatin1Char('f'), clip ); map.insert( QLatin1Char('F'), clip ); - const QStringList matches = action.regExpMatches(); + const QStringList matches = action.actionCapturedTexts(); // support only %0 and the first 9 matches... const int numMatches = qMin(10, matches.count()); for ( int i = 0; i < numMatches; ++i ) { map.insert( QChar( '0' + i ), matches.at( i ) ); } setOutputChannelMode(OnlyStdoutChannel); setShellCommand(KMacroExpander::expandMacrosShellQuote( command.command, map ).trimmed()); connect(this, SIGNAL(finished(int,QProcess::ExitStatus)), SLOT(slotFinished(int,QProcess::ExitStatus))); if (command.output != ClipCommand::IGNORE) { connect(this, &QIODevice::readyRead, this, &ClipCommandProcess::slotStdOutputAvailable); } if (command.output != ClipCommand::REPLACE) { m_historyItem.clear(); } } void ClipCommandProcess::slotFinished(int /*exitCode*/, QProcess::ExitStatus /*newState*/) { if (m_history) { // If an history item was provided, remove it so that the new item can replace it if (m_historyItem) { m_history->remove(m_historyItem); } if (!m_newhistoryItem.isEmpty()) { m_history->insert(HistoryItemPtr(new HistoryStringItem(m_newhistoryItem))); } } deleteLater(); } void ClipCommandProcess::slotStdOutputAvailable() { m_newhistoryItem.append(QString::fromLocal8Bit(this->readAllStandardOutput())); } diff --git a/klipper/configdialog.cpp b/klipper/configdialog.cpp index b123675b8..336c4238b 100644 --- a/klipper/configdialog.cpp +++ b/klipper/configdialog.cpp @@ -1,388 +1,388 @@ /* This file is part of the KDE project Copyright (C) 2000 by Carsten Pfeiffer Copyright (C) 2008-2009 by Dmitry Suzdalev This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "configdialog.h" #include #include #include #include #include "klipper_debug.h" #include "klipper.h" #include "editactiondialog.h" GeneralWidget::GeneralWidget(QWidget* parent) : QWidget(parent) { m_ui.setupUi(this); m_ui.kcfg_TimeoutForActionPopups->setSuffix(ki18np(" second", " seconds")); m_ui.kcfg_MaxClipItems->setSuffix(ki18np(" entry", " entries")); } void GeneralWidget::updateWidgets() { if (m_ui.kcfg_IgnoreSelection->isChecked()) { m_ui.kcfg_SyncClipboards->setEnabled(false); m_ui.kcfg_SelectionTextOnly->setEnabled(false); } else if (m_ui.kcfg_SyncClipboards->isChecked()) { m_ui.kcfg_IgnoreSelection->setEnabled(false); } } ActionsWidget::ActionsWidget(QWidget* parent) : QWidget(parent), m_editActDlg(nullptr) { m_ui.setupUi(this); 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"))); m_ui.pbAdvanced->setIcon(QIcon::fromTheme(QStringLiteral("configure"))); const KConfigGroup grp = KSharedConfig::openConfig()->group("ActionsWidget"); QByteArray hdrState = grp.readEntry("ColumnState", QByteArray()); if (!hdrState.isEmpty()) { qCDebug(KLIPPER_LOG) << "Restoring column state"; m_ui.kcfg_ActionList->header()->restoreState(QByteArray::fromBase64(hdrState)); } else { m_ui.kcfg_ActionList->header()->resizeSection(0, 250); } #if 0 if ( /*KServiceTypeTrader::self()->query("KRegExpEditor/KRegExpEditor").isEmpty()*/ true) // see notice in configdialog.cpp about KRegExpEditor { cbUseGUIRegExpEditor->hide(); cbUseGUIRegExpEditor->setChecked( false ); } #endif 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_ui.pbAdvanced, &QPushButton::clicked, this, &ActionsWidget::onAdvanced); onSelectionChanged(); } void ActionsWidget::setActionList(const ActionList& list) { qDeleteAll( m_actionList ); m_actionList.clear(); foreach (ClipAction* action, list) { if (!action) { qCDebug(KLIPPER_LOG) << "action is null!"; continue; } // make a copy for us to work with from now on m_actionList.append( new ClipAction( *action ) ); } updateActionListView(); } void ActionsWidget::updateActionListView() { m_ui.kcfg_ActionList->clear(); foreach (ClipAction* action, m_actionList) { if (!action) { qCDebug(KLIPPER_LOG) << "action is null!"; continue; } QTreeWidgetItem *item = new QTreeWidgetItem; updateActionItem( item, action ); m_ui.kcfg_ActionList->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(); } void ActionsWidget::updateActionItem( QTreeWidgetItem* item, ClipAction* action ) { if ( !item || !action ) { qCDebug(KLIPPER_LOG) << "null pointer passed to function, nothing done"; return; } // clear children if any item->takeChildren(); - item->setText( 0, action->regExp() ); + item->setText( 0, action->actionRegexPattern() ); item->setText( 1, action->description() ); foreach (const ClipCommand& command, action->commands()) { QStringList cmdProps; cmdProps << command.command << command.description; QTreeWidgetItem *child = new QTreeWidgetItem(item, cmdProps); child->setIcon(0, QIcon::fromTheme(command.icon.isEmpty() ? QStringLiteral("system-run") : command.icon)); } } void ActionsWidget::setExcludedWMClasses(const QStringList& excludedWMClasses) { m_exclWMClasses = excludedWMClasses; } QStringList ActionsWidget::excludedWMClasses() const { return m_exclWMClasses; } ActionList ActionsWidget::actionList() const { // return a copy of our action list ActionList list; foreach( ClipAction* action, m_actionList ) { if ( !action ) { qCDebug(KLIPPER_LOG) << "action is null"; continue; } list.append( new ClipAction( *action ) ); } return list; } void ActionsWidget::resetModifiedState() { m_ui.kcfg_ActionList->resetModifiedState(); qCDebug(KLIPPER_LOG) << "Saving column state"; KConfigGroup grp = KSharedConfig::openConfig()->group("ActionsWidget"); grp.writeEntry("ColumnState", m_ui.kcfg_ActionList->header()->saveState().toBase64()); } void ActionsWidget::onSelectionChanged() { bool itemIsSelected = !m_ui.kcfg_ActionList->selectedItems().isEmpty(); m_ui.pbEditAction->setEnabled(itemIsSelected); m_ui.pbDelAction->setEnabled(itemIsSelected); } void ActionsWidget::onAddAction() { if (!m_editActDlg) { m_editActDlg = new EditActionDialog(this); } ClipAction* newAct = new ClipAction; m_editActDlg->setAction(newAct); if (m_editActDlg->exec() == QDialog::Accepted) { m_actionList.append( newAct ); QTreeWidgetItem* item = new QTreeWidgetItem; updateActionItem( item, newAct ); m_ui.kcfg_ActionList->addTopLevelItem( item ); } } void ActionsWidget::onEditAction() { if (!m_editActDlg) { m_editActDlg = new EditActionDialog(this); } 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 } int idx = m_ui.kcfg_ActionList->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(); updateActionItem(item, action); } } void ActionsWidget::onDeleteAction() { QTreeWidgetItem *item = m_ui.kcfg_ActionList->currentItem(); if ( item && item->parent() ) item = item->parent(); if ( item ) { int idx = m_ui.kcfg_ActionList->indexOfTopLevelItem( item ); m_actionList.removeAt( idx ); } delete item; } void ActionsWidget::onAdvanced() { QDialog dlg(this); dlg.setModal(true); dlg.setWindowTitle( i18n("Advanced Settings") ); QDialogButtonBox *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dlg); buttons->button(QDialogButtonBox::Ok)->setShortcut(Qt::CTRL | Qt::Key_Return); connect(buttons, &QDialogButtonBox::accepted, &dlg, &QDialog::accept); connect(buttons, &QDialogButtonBox::rejected, &dlg, &QDialog::reject); AdvancedWidget *widget = new AdvancedWidget(&dlg); widget->setWMClasses( m_exclWMClasses ); QVBoxLayout *layout = new QVBoxLayout(&dlg); layout->addWidget(widget); layout->addWidget(buttons); if ( dlg.exec() == QDialog::Accepted ) { m_exclWMClasses = widget->wmClasses(); } } ConfigDialog::ConfigDialog(QWidget* parent, KConfigSkeleton* skeleton, const Klipper* klipper, KActionCollection*collection) : KConfigDialog(parent, QStringLiteral("preferences"), skeleton), m_generalPage(new GeneralWidget(this)), m_actionsPage(new ActionsWidget(this)), m_klipper(klipper) { addPage(m_generalPage, i18nc("General Config", "General"), QStringLiteral("klipper"), i18n("General Configuration")); addPage(m_actionsPage, i18nc("Actions Config", "Actions"), QStringLiteral("system-run"), i18n("Actions Configuration")); QWidget* w = new QWidget(this); m_shortcutsWidget = new KShortcutsEditor( collection, w, KShortcutsEditor::GlobalAction ); addPage(m_shortcutsWidget, i18nc("Shortcuts Config", "Shortcuts"), QStringLiteral("preferences-desktop-keyboard"), i18n("Shortcuts Configuration")); const KConfigGroup grp = KSharedConfig::openConfig()->group("ConfigDialog"); KWindowConfig::restoreWindowSize(windowHandle(), grp); } ConfigDialog::~ConfigDialog() { } void ConfigDialog::updateSettings() { // user clicked Ok or Apply if (!m_klipper) { qCDebug(KLIPPER_LOG) << "Klipper object is null"; return; } m_shortcutsWidget->save(); m_actionsPage->resetModifiedState(); m_klipper->urlGrabber()->setActionList(m_actionsPage->actionList()); m_klipper->urlGrabber()->setExcludedWMClasses(m_actionsPage->excludedWMClasses()); m_klipper->saveSettings(); KConfigGroup grp = KSharedConfig::openConfig()->group("ConfigDialog"); KWindowConfig::saveWindowSize(windowHandle(), grp); } void ConfigDialog::updateWidgets() { // settings were updated, update widgets if (m_klipper && m_klipper->urlGrabber() ) { m_actionsPage->setActionList(m_klipper->urlGrabber()->actionList()); m_actionsPage->setExcludedWMClasses(m_klipper->urlGrabber()->excludedWMClasses()); } else { qCDebug(KLIPPER_LOG) << "Klipper or grabber object is null"; return; } m_generalPage->updateWidgets(); } void ConfigDialog::updateWidgetsDefault() { // default widget values requested m_shortcutsWidget->allDefault(); } AdvancedWidget::AdvancedWidget( QWidget *parent ) : QWidget(parent) { QVBoxLayout *mainLayout = new QVBoxLayout(this); mainLayout->setContentsMargins(0, 0, 0, 0); QGroupBox *groupBox = new QGroupBox(i18n("D&isable Actions for Windows of Type WM_CLASS"), this); groupBox->setLayout(new QVBoxLayout(groupBox)); editListBox = new KEditListWidget(groupBox); editListBox->setButtons(KEditListWidget::Add | KEditListWidget::Remove); editListBox->setCheckAtEntering(true); editListBox->setWhatsThis(i18n("This lets you specify windows in which Klipper should " "not invoke \"actions\". Use

" "
xprop | grep WM_CLASS

" "in a terminal to find out the WM_CLASS of a window. " "Next, click on the window you want to examine. The " "first string it outputs after the equal sign is the one " "you need to enter here.
")); groupBox->layout()->addWidget(editListBox); mainLayout->addWidget(groupBox); editListBox->setFocus(); } AdvancedWidget::~AdvancedWidget() { } void AdvancedWidget::setWMClasses( const QStringList& items ) { editListBox->setItems(items); } QStringList AdvancedWidget::wmClasses() const { return editListBox->items(); } diff --git a/klipper/editactiondialog.cpp b/klipper/editactiondialog.cpp index 66cefcf81..739c84010 100644 --- a/klipper/editactiondialog.cpp +++ b/klipper/editactiondialog.cpp @@ -1,385 +1,385 @@ /* This file is part of the KDE project Copyright (C) 2009 by Dmitry Suzdalev This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "editactiondialog.h" #include #include #include #include "klipper_debug.h" #include #include #include #include "urlgrabber.h" #include "ui_editactiondialog.h" namespace { static QString output2text(ClipCommand::Output output) { switch(output) { case ClipCommand::IGNORE: return QString(i18n("Ignore")); case ClipCommand::REPLACE: return QString(i18n("Replace Clipboard")); case ClipCommand::ADD: return QString(i18n("Add to Clipboard")); } 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::IGNORE)); editor->addItem(output2text(ClipCommand::REPLACE), QVariant::fromValue(ClipCommand::REPLACE)); editor->addItem(output2text(ClipCommand::ADD), QVariant::fromValue(ClipCommand::ADD)); return editor; } void setEditorData(QWidget* editor, const QModelIndex& index) const override { QComboBox* ed = static_cast(editor); QVariant data(index.model()->data(index, Qt::EditRole)); ed->setCurrentIndex(static_cast(data.value())); } void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const override { QComboBox* ed = static_cast(editor); model->setData(index, ed->itemData(ed->currentIndex())); } void updateEditorGeometry(QWidget* editor, const QStyleOptionViewItem& option, const QModelIndex& /*index*/) const override { editor->setGeometry(option.rect); } }; class ActionDetailModel : public QAbstractTableModel { public: 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; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; const QList& commands() const { return m_commands; } void addCommand(const ClipCommand& command); void removeCommand(const QModelIndex& index); private: enum column_t { COMMAND_COL = 0, OUTPUT_COL = 1, DESCRIPTION_COL = 2 }; QList 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): QAbstractTableModel(parent), m_commands(action->commands()) { } 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(index.column())) { case COMMAND_COL: cmd.command = value.toString(); setIconForCommand(cmd); break; case OUTPUT_COL: cmd.output = value.value(); break; case DESCRIPTION_COL: cmd.description = value.toString(); break; } m_commands.replace(index.row(), cmd); emit dataChanged(index, index); return true; } return false; } int ActionDetailModel::columnCount(const QModelIndex& /*parent*/) const { return 3; } int ActionDetailModel::rowCount(const QModelIndex&) const { return m_commands.count(); } QVariant ActionDetailModel::displayData(ClipCommand* command, ActionDetailModel::column_t column) const { switch (column) { case COMMAND_COL: return command->command; case OUTPUT_COL: return output2text(command->output); case DESCRIPTION_COL: return command->description; } return QVariant(); } QVariant ActionDetailModel::decorationData(ClipCommand* command, ActionDetailModel::column_t column) const { switch (column) { case COMMAND_COL: return command->icon.isEmpty() ? QIcon::fromTheme( QStringLiteral("system-run") ) : QIcon::fromTheme( command->icon ); case OUTPUT_COL: case DESCRIPTION_COL: break; } 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(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) { switch(static_cast(section)) { case COMMAND_COL: return i18n("Command"); case OUTPUT_COL: return i18n("Output Handling"); case DESCRIPTION_COL: return i18n("Description"); } } return QAbstractTableModel::headerData(section, orientation, role); } QVariant ActionDetailModel::data(const QModelIndex& index, int role) const { const int column = index.column(); const int row = index.row(); ClipCommand cmd = m_commands.at(row); switch (role) { case Qt::DisplayRole: return displayData(&cmd, static_cast(column)); case Qt::DecorationRole: return decorationData(&cmd, static_cast(column)); case Qt::EditRole: return editData(&cmd, static_cast(column)); } return QVariant(); } void ActionDetailModel::addCommand(const ClipCommand& command) { beginInsertRows(QModelIndex(), rowCount(), rowCount()); m_commands << command; endInsertRows(); } void ActionDetailModel::removeCommand(const QModelIndex& index) { int row = index.row(); beginRemoveRows(QModelIndex(), row, row); m_commands.removeAt(row); endRemoveRows(); } EditActionDialog::EditActionDialog(QWidget* parent) : QDialog(parent) { setWindowTitle(i18n("Action Properties")); QDialogButtonBox *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); buttons->button(QDialogButtonBox::Ok)->setShortcut(Qt::CTRL | Qt::Key_Return); 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, // 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"); KWindowConfig::restoreWindowSize(windowHandle(), grp); QByteArray hdrState = grp.readEntry("ColumnState", QByteArray()); if (!hdrState.isEmpty()) { qCDebug(KLIPPER_LOG) << "Restoring column state"; m_ui->twCommandList->horizontalHeader()->restoreState(QByteArray::fromBase64(hdrState)); } // do this after restoreState() m_ui->twCommandList->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); updateWidgets( commandIdxToSelect ); } void EditActionDialog::updateWidgets(int commandIdxToSelect) { if (!m_action) { qCDebug(KLIPPER_LOG) << "no action to edit was set"; return; } - m_ui->leRegExp->setText(m_action->regExp()); + m_ui->leRegExp->setText(m_action->actionRegexPattern()); m_ui->automatic->setChecked(m_action->automatic()); m_ui->leDescription->setText(m_action->description()); if (commandIdxToSelect != -1) { m_ui->twCommandList->setCurrentIndex( m_model->index( commandIdxToSelect ,0 ) ); } // update Remove button onSelectionChanged(); } void EditActionDialog::saveAction() { if (!m_action) { qCDebug(KLIPPER_LOG) << "no action to edit was set"; return; } - m_action->setRegExp( m_ui->leRegExp->text() ); + m_action->setActionRegexPattern( m_ui->leRegExp->text() ); m_action->setDescription( m_ui->leDescription->text() ); m_action->setAutomatic( m_ui->automatic->isChecked() ); m_action->clearCommands(); foreach ( const ClipCommand& cmd, m_model->commands() ){ m_action->addCommand( cmd ); } } void EditActionDialog::slotAccepted() { saveAction(); qCDebug(KLIPPER_LOG) << "Saving dialogue state"; KConfigGroup grp = KSharedConfig::openConfig()->group("EditActionDialog"); KWindowConfig::saveWindowSize(windowHandle(), grp); grp.writeEntry("ColumnState", m_ui->twCommandList->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 )); } void EditActionDialog::onRemoveCommand() { m_model->removeCommand(m_ui->twCommandList->selectionModel()->currentIndex()); } void EditActionDialog::onSelectionChanged() { m_ui->pbRemoveCommand->setEnabled( m_ui->twCommandList->selectionModel() && m_ui->twCommandList->selectionModel()->hasSelection() ); } diff --git a/klipper/klipperpopup.cpp b/klipper/klipperpopup.cpp index 162a3a32a..d580083c6 100644 --- a/klipper/klipperpopup.cpp +++ b/klipper/klipperpopup.cpp @@ -1,284 +1,285 @@ /* This file is part of the KDE project Copyright (C) 2004 Esben Mose Hansen Copyright (C) by Andrew Stanley-Jones Copyright (C) 2000 by Carsten Pfeiffer This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "klipperpopup.h" #include #include "klipper_debug.h" #include #include #include #include #include #include #include #include "history.h" #include "klipper.h" #include "popupproxy.h" namespace { static const int TOP_HISTORY_ITEM_INDEX = 2; } // #define DEBUG_EVENTS__ #ifdef DEBUG_EVENTS__ kdbgstream& operator<<( kdbgstream& stream, const QKeyEvent& e ) { stream << "(QKeyEvent(text=" << e.text() << ",key=" << e.key() << ( e.isAccepted()?",accepted":",ignored)" ) << ",count=" << e.count(); if ( e.modifiers() & Qt::AltModifier ) { stream << ",ALT"; } if ( e.modifiers() & Qt::ControlModifier ) { stream << ",CTRL"; } if ( e.modifiers() & Qt::MetaModifier ) { stream << ",META"; } if ( e.modifiers() & Qt::ShiftModifier ) { stream << ",SHIFT"; } if ( e.isAutoRepeat() ) { stream << ",AUTOREPEAT"; } stream << ")"; return stream; } #endif KlipperPopup::KlipperPopup( History* history ) : m_dirty( true ), m_textForEmptyHistory( i18n( "" ) ), m_textForNoMatch( i18n( "" ) ), m_history( history ), m_helpMenu( nullptr ), m_popupProxy( nullptr ), m_filterWidget( nullptr ), m_filterWidgetAction( nullptr ), m_nHistoryItems( 0 ), m_showHelp(true), m_lastEvent(nullptr) { ensurePolished(); KWindowInfo windowInfo( winId(), NET::WMGeometry ); QRect geometry = windowInfo.geometry(); QRect screen = qApp->desktop()->screenGeometry(geometry.center()); int menuHeight = ( screen.height() ) * 3/4; int menuWidth = ( screen.width() ) * 1/3; m_popupProxy = new PopupProxy( this, menuHeight, menuWidth ); connect(this, &KlipperPopup::aboutToShow, this, &KlipperPopup::slotAboutToShow); } KlipperPopup::~KlipperPopup() { } void KlipperPopup::slotAboutToShow() { if ( m_filterWidget ) { if ( !m_filterWidget->text().isEmpty() ) { m_dirty = true; m_filterWidget->clear(); } } ensureClean(); } void KlipperPopup::ensureClean() { // If the history is unchanged since last menu build, the is no reason // to rebuild it, if ( m_dirty ) { rebuild(); } } void KlipperPopup::buildFromScratch() { addSection(QIcon::fromTheme(QStringLiteral("klipper")), i18n("Klipper - Clipboard Tool")); m_filterWidget = new KLineEdit(this); m_filterWidget->setFocusPolicy( Qt::NoFocus ); m_filterWidget->setPlaceholderText(i18n("Search...")); m_filterWidgetAction = new QWidgetAction(this); m_filterWidgetAction->setDefaultWidget(m_filterWidget); addAction(m_filterWidgetAction); addSeparator(); for (int i = 0; i < m_actions.count(); i++) { if (i + 1 == m_actions.count() && m_showHelp) { if (!m_helpMenu) { m_helpMenu = new KHelpMenu( this, i18n("KDE cut & paste history utility"), false ); } addMenu(m_helpMenu->menu())->setIcon(QIcon::fromTheme(QStringLiteral("help-contents"))); addSeparator(); } addAction(m_actions.at(i)); } } void KlipperPopup::rebuild( const QString& filter ) { if (actions().isEmpty()) { buildFromScratch(); } else { for ( int i=0; ipalette(); if ( filterexp.isValid() ) { palette.setColor( m_filterWidget->foregroundRole(), palette.color(foregroundRole()) ); } else { palette.setColor( m_filterWidget->foregroundRole(), Qt::red ); } m_nHistoryItems = m_popupProxy->buildParent( TOP_HISTORY_ITEM_INDEX, filterexp ); if ( m_nHistoryItems == 0 ) { if ( m_history->empty() ) { insertAction(actions().at(TOP_HISTORY_ITEM_INDEX), new QAction(m_textForEmptyHistory, this)); } else { palette.setColor( m_filterWidget->foregroundRole(), Qt::red ); insertAction(actions().at(TOP_HISTORY_ITEM_INDEX), new QAction(m_textForNoMatch, this)); } m_nHistoryItems++; } else { if ( history()->topIsUserSelected() ) { actions().at(TOP_HISTORY_ITEM_INDEX)->setCheckable(true); actions().at(TOP_HISTORY_ITEM_INDEX)->setChecked(true); } } m_filterWidget->setPalette( palette ); m_dirty = false; } void KlipperPopup::slotTopIsUserSelectedSet() { if ( !m_dirty && m_nHistoryItems > 0 && history()->topIsUserSelected() ) { actions().at(TOP_HISTORY_ITEM_INDEX)->setCheckable(true); actions().at(TOP_HISTORY_ITEM_INDEX)->setChecked(true); } } void KlipperPopup::plugAction( QAction* action ) { m_actions.append(action); } /* virtual */ void KlipperPopup::keyPressEvent( QKeyEvent* e ) { // Most events are send down directly to the m_filterWidget. // If the m_filterWidget does not handle the event, it will // come back to this method. Remembering the last event stops // the infinite event loop if (m_lastEvent == e) { m_lastEvent= nullptr; return; } m_lastEvent= e; // If alt-something is pressed, select a shortcut // from the menu. Do this by sending a keyPress // without the alt-modifier to the superobject. if ( e->modifiers() & Qt::AltModifier ) { QKeyEvent ke( QEvent::KeyPress, e->key(), e->modifiers() ^ Qt::AltModifier, e->text(), e->isAutoRepeat(), e->count() ); QMenu::keyPressEvent( &ke ); #ifdef DEBUG_EVENTS__ qCDebug(KLIPPER_LOG) << "Passing this event to ancestor (KMenu): " << e << "->" << ke; #endif if (ke.isAccepted()) { e->accept(); return; } else { e->ignore(); } } // Otherwise, send most events to the search // widget, except a few used for navigation: // These go to the superobject. switch( e->key() ) { case Qt::Key_Up: case Qt::Key_Down: case Qt::Key_Right: case Qt::Key_Left: case Qt::Key_Tab: case Qt::Key_Backtab: case Qt::Key_Escape: { #ifdef DEBUG_EVENTS__ qCDebug(KLIPPER_LOG) << "Passing this event to ancestor (KMenu): " << e; #endif QMenu::keyPressEvent(e); break; } case Qt::Key_Return: case Qt::Key_Enter: { QMenu::keyPressEvent(e); this->hide(); if (activeAction() == m_filterWidgetAction) setActiveAction(actions().at(TOP_HISTORY_ITEM_INDEX)); break; } default: { #ifdef DEBUG_EVENTS__ qCDebug(KLIPPER_LOG) << "Passing this event down to child (KLineEdit): " << e; #endif setActiveAction(actions().at(actions().indexOf(m_filterWidgetAction))); QString lastString = m_filterWidget->text(); QApplication::sendEvent(m_filterWidget, e); if (m_filterWidget->text() != lastString) { m_dirty = true; rebuild(m_filterWidget->text()); } break; } //default: } //case m_lastEvent= nullptr; } void KlipperPopup::slotSetTopActive() { if (actions().size() > TOP_HISTORY_ITEM_INDEX) { setActiveAction(actions().at(TOP_HISTORY_ITEM_INDEX)); } } diff --git a/klipper/popupproxy.cpp b/klipper/popupproxy.cpp index 23fe530e6..53b65f3b8 100644 --- a/klipper/popupproxy.cpp +++ b/klipper/popupproxy.cpp @@ -1,179 +1,177 @@ /* This file is part of the KDE project Copyright (C) 2004 Esben Mose Hansen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "popupproxy.h" -#include #include #include #include #include #include "historyitem.h" #include "history.h" #include "klipperpopup.h" - PopupProxy::PopupProxy( KlipperPopup* parent, int menu_height, int menu_width ) : QObject( parent ), m_proxy_for_menu( parent ), m_spill_uuid(), m_menu_height( menu_height ), m_menu_width( menu_width ) { if (!parent->history()->empty()) { m_spill_uuid = parent->history()->first()->uuid(); } connect( parent->history(), &History::changed, this, &PopupProxy::slotHistoryChanged ); connect(m_proxy_for_menu, SIGNAL(triggered(QAction*)), parent->history(), SLOT(slotMoveToTop(QAction*))); } void PopupProxy::slotHistoryChanged() { deleteMoreMenus(); } void PopupProxy::deleteMoreMenus() { const QMenu* myParent = parent(); if ( myParent != m_proxy_for_menu ) { QMenu* delme = m_proxy_for_menu; m_proxy_for_menu = static_cast( m_proxy_for_menu->parent() ); while ( m_proxy_for_menu != myParent ) { delme = m_proxy_for_menu; m_proxy_for_menu = static_cast( m_proxy_for_menu->parent() ); } // We are called probably from within the menus event-handler (triggered=>slotMoveToTop=>changed=>slotHistoryChanged=>deleteMoreMenus) // what can result in a crash if we just delete the menu here (#155196 and #165154) So, delay the delete. delme->deleteLater(); } } -int PopupProxy::buildParent( int index, const QRegExp& filter ) { +int PopupProxy::buildParent( int index, const QRegularExpression &filter ) { deleteMoreMenus(); // Start from top of history (again) m_spill_uuid = parent()->history()->empty() ? QByteArray() : parent()->history()->first()->uuid(); if ( filter.isValid() ) { m_filter = filter; } return insertFromSpill( index ); } KlipperPopup* PopupProxy::parent() { return static_cast( QObject::parent() ); } void PopupProxy::slotAboutToShow() { insertFromSpill(); } void PopupProxy::tryInsertItem( HistoryItem const * const item, int& remainingHeight, const int index ) { QAction *action = new QAction(m_proxy_for_menu); QPixmap image( item->image() ); if ( image.isNull() ) { // Squeeze text strings so that do not take up the entire screen (or more) QString text = m_proxy_for_menu->fontMetrics().elidedText( item->text().simplified(), Qt::ElideMiddle, m_menu_width ); text.replace( QLatin1Char('&'), QLatin1String("&&") ); action->setText(text); } else { #if 0 // not used because QAction#setIcon does not respect this size; it does scale anyway. TODO: find a way to set a bigger image const QSize max_size( m_menu_width,m_menu_height/4 ); if ( image.height() > max_size.height() || image.width() > max_size.width() ) { image = image.scaled( max_size, Qt::KeepAspectRatio, Qt::SmoothTransformation ); } #endif action->setIcon(QIcon(image)); } action->setData(item->uuid()); // if the m_proxy_for_menu is a submenu (aka a "More" menu) then it may the case, that there is no other action in that menu yet. QAction *before = index < m_proxy_for_menu->actions().count() ? m_proxy_for_menu->actions().at(index) : nullptr; // insert the new action to the m_proxy_for_menu m_proxy_for_menu->insertAction(before, action); // Determine height of a menu item. QStyleOptionMenuItem style_options; // It would be much easier to use QMenu::initStyleOptions. But that is protected, so until we have a better // excuse to subclass that, I'd rather implement this manually. // Note 2 properties, tabwidth and maxIconWidth, are not available from the public interface, so those are left out (probably not // important for height. Also, Exclusive checkType is disregarded as I don't think we will ever use it) style_options.initFrom(m_proxy_for_menu); style_options.checkType = action->isCheckable() ? QStyleOptionMenuItem::NonExclusive : QStyleOptionMenuItem::NotCheckable; style_options.checked = action->isChecked(); style_options.font = action->font(); style_options.icon = action->icon(); style_options.menuHasCheckableItems = true; style_options.menuRect = m_proxy_for_menu->rect(); style_options.text = action->text(); int font_height = QFontMetrics(m_proxy_for_menu->fontMetrics()).height(); int itemheight = m_proxy_for_menu->style()->sizeFromContents(QStyle::CT_MenuItem, &style_options, QSize( 0, font_height ), m_proxy_for_menu).height(); // Subtract the used height remainingHeight -= itemheight; } int PopupProxy::insertFromSpill( int index ) { const History* history = parent()->history(); // This menu is going to be filled, so we don't need the aboutToShow() // signal anymore disconnect( m_proxy_for_menu, nullptr, this, nullptr ); // Insert history items into the current m_proxy_for_menu, // discarding any that doesn't match the current filter. // stop when the total number of items equal m_itemsPerMenu; int count = 0; int remainingHeight = m_menu_height - m_proxy_for_menu->sizeHint().height(); auto item = history->find(m_spill_uuid); if (!item) { return count; } do { - if ( m_filter.indexIn( item->text() ) != -1) { + if (m_filter.match(item->text()).hasMatch()) { tryInsertItem( item.data(), remainingHeight, index++ ); count++; } item = history->find(item->next_uuid()); } while ( item && history->first() != item && remainingHeight >= 0); m_spill_uuid = item->uuid(); // If there is more items in the history, insert a new "More..." menu and // make *this a proxy for that menu ('s content). if (history->first() && m_spill_uuid != history->first()->uuid()) { QMenu* moreMenu = new QMenu(i18n("&More"), m_proxy_for_menu); connect(moreMenu, &QMenu::aboutToShow, this, &PopupProxy::slotAboutToShow); QAction *before = index < m_proxy_for_menu->actions().count() ? m_proxy_for_menu->actions().at(index) : nullptr; m_proxy_for_menu->insertMenu(before, moreMenu); m_proxy_for_menu = moreMenu; } // Return the number of items inserted. return count; } diff --git a/klipper/popupproxy.h b/klipper/popupproxy.h index f33f62c11..9eaa9db95 100644 --- a/klipper/popupproxy.h +++ b/klipper/popupproxy.h @@ -1,89 +1,89 @@ /* This file is part of the KDE project Copyright (C) 2004 Esben Mose Hansen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef POPUPPROXY_H #define POPUPPROXY_H #include -#include +#include #include "history.h" class QMenu; class HistoryItem; class KlipperPopup; /** * Proxy helper for the "more" menu item * */ class PopupProxy : public QObject { Q_OBJECT public: /** * Inserts up to itemsPerMenu into parent from parent->youngest(), * and spills any remaining items into a more menu. */ PopupProxy( KlipperPopup* parent, int menu_height, int menu_width ); KlipperPopup* parent(); /** * Called when rebuilding the menu * Deletes any More menus.. and start (re)inserting into the toplevel menu. * @param index Items are inserted at index. * @param filter If non-empty, only insert items that match filter as a regex * @return number of items inserted. */ - int buildParent( int index, const QRegExp& filter = QRegExp() ); + int buildParent( int index, const QRegularExpression &filter = QRegularExpression() ); public Q_SLOTS: void slotAboutToShow(); void slotHistoryChanged(); private: /** * Insert up to m_itemsPerMenu items from spill and a new * more-menu if necessary. * @param index Items are inserted at index * @return number of items inserted. */ int insertFromSpill( int index = 0 ); /** * Insert item into proxy_for_menu at index, * subtracting the items height from remainingHeight */ void tryInsertItem( HistoryItem const * const item, int& remainingHeight, const int index ); /** * Delete all "More..." menus current created. */ void deleteMoreMenus(); private: QMenu* m_proxy_for_menu; QByteArray m_spill_uuid; - QRegExp m_filter; + QRegularExpression m_filter; int m_menu_height; int m_menu_width; }; #endif diff --git a/klipper/urlgrabber.cpp b/klipper/urlgrabber.cpp index 2e479a4a4..070201ac2 100644 --- a/klipper/urlgrabber.cpp +++ b/klipper/urlgrabber.cpp @@ -1,482 +1,486 @@ /* This file is part of the KDE project Copyright (C) (C) 2000,2001,2002 by Carsten Pfeiffer This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "urlgrabber.h" #include #include "klipper_debug.h" #include #include #include #include #include #include #include +#include #include #include #include #include #include #include #include "klippersettings.h" #include "clipcommandprocess.h" // TODO: script-interface? #include "history.h" #include "historystringitem.h" URLGrabber::URLGrabber(History* history): m_myCurrentAction(nullptr), m_myMenu(nullptr), m_myPopupKillTimer(new QTimer( this )), m_myPopupKillTimeout(8), m_stripWhiteSpace(true), m_history(history) { m_myPopupKillTimer->setSingleShot( true ); connect(m_myPopupKillTimer, &QTimer::timeout, this, &URLGrabber::slotKillPopupMenu); // testing /* ClipAction *action; action = new ClipAction( "^http:\\/\\/", "Web-URL" ); action->addCommand("kfmclient exec %s", "Open with Konqi", true); action->addCommand("netscape -no-about-splash -remote \"openURL(%s, new-window)\"", "Open with Netscape", true); m_myActions->append( action ); action = new ClipAction( "^mailto:", "Mail-URL" ); action->addCommand("kmail --composer %s", "Launch kmail", true); m_myActions->append( action ); action = new ClipAction( "^\\/.+\\.jpg$", "Jpeg-Image" ); action->addCommand("kuickshow %s", "Launch KuickShow", true); action->addCommand("kview %s", "Launch KView", true); m_myActions->append( action ); */ } URLGrabber::~URLGrabber() { qDeleteAll(m_myActions); m_myActions.clear(); delete m_myMenu; } // // Called from Klipper::slotRepeatAction, i.e. by pressing Ctrl-Alt-R // shortcut. I.e. never from clipboard monitoring // void URLGrabber::invokeAction( HistoryItemConstPtr item ) { m_myClipItem = item; actionMenu( item, false ); } void URLGrabber::setActionList( const ActionList& list ) { qDeleteAll(m_myActions); m_myActions.clear(); m_myActions = list; } void URLGrabber::matchingMimeActions(const QString& clipData) { QUrl url(clipData); if(!KlipperSettings::enableMagicMimeActions()) { //qCDebug(KLIPPER_LOG) << "skipping mime magic due to configuration"; return; } if(!url.isValid()) { //qCDebug(KLIPPER_LOG) << "skipping mime magic due to invalid url"; return; } if(url.isRelative()) { //openinng a relative path will just not work. what path should be used? //qCDebug(KLIPPER_LOG) << "skipping mime magic due to relative url"; return; } if(url.isLocalFile()) { if ( clipData == QLatin1String("//")) { //qCDebug(KLIPPER_LOG) << "skipping mime magic due to C++ comment //"; return; } if(!QFile::exists(url.toLocalFile())) { //qCDebug(KLIPPER_LOG) << "skipping mime magic due to nonexistent localfile"; return; } } // try to figure out if clipData contains a filename QMimeDatabase db; QMimeType mimetype = db.mimeTypeForUrl(url); // let's see if we found some reasonable mimetype. // If we do we'll populate menu with actions for apps // that can handle that mimetype // first: if clipboard contents starts with http, let's assume it's "text/html". // That is even if we've url like "http://www.kde.org/somescript.pl", we'll // still treat that as html page, because determining a mimetype using kio // might take a long time, and i want this function to be quick! if ( ( clipData.startsWith( QLatin1String("http://") ) || clipData.startsWith( QLatin1String("https://") ) ) && mimetype.name() != QLatin1String("text/html") ) { mimetype = db.mimeTypeForName(QStringLiteral("text/html")); } if ( !mimetype.isDefault() ) { KService::List lst = KMimeTypeTrader::self()->query( mimetype.name(), QStringLiteral("Application") ); if ( !lst.isEmpty() ) { ClipAction* action = new ClipAction( QString(), mimetype.comment() ); foreach( const KService::Ptr &service, lst ) { action->addCommand( ClipCommand( QString(), service->name(), true, service->icon(), ClipCommand::IGNORE, service->storageId() ) ); } m_myMatches.append( action ); } } } const ActionList& URLGrabber::matchingActions( const QString& clipData, bool automatically_invoked ) { m_myMatches.clear(); matchingMimeActions(clipData); - // now look for matches in custom user actions + QRegularExpression re; foreach (ClipAction* action, m_myActions) { - if ( action->matches( clipData ) && (action->automatic() || !automatically_invoked) ) { + re.setPattern(action->actionRegexPattern()); + const QRegularExpressionMatch match = re.match(clipData); + if (match.hasMatch() && (action->automatic() || !automatically_invoked)) { + action->setActionCapturedTexts(match.capturedTexts()); m_myMatches.append( action ); } } return m_myMatches; } void URLGrabber::checkNewData( HistoryItemConstPtr item ) { // qCDebug(KLIPPER_LOG) << "** checking new data: " << clipData; actionMenu( item, true ); // also creates m_myMatches } void URLGrabber::actionMenu( HistoryItemConstPtr item, bool automatically_invoked ) { if (!item) { qWarning("Attempt to invoke URLGrabber without an item"); return; } QString text(item->text()); if (m_stripWhiteSpace) { text = text.trimmed(); } ActionList matchingActionsList = matchingActions( text, automatically_invoked ); if (!matchingActionsList.isEmpty()) { // don't react on blacklisted (e.g. konqi's/netscape's urls) unless the user explicitly asked for it if ( automatically_invoked && isAvoidedWindow() ) { return; } m_myCommandMapper.clear(); m_myPopupKillTimer->stop(); m_myMenu = new QMenu; connect(m_myMenu, &QMenu::triggered, this, &URLGrabber::slotItemSelected); foreach (ClipAction* clipAct, matchingActionsList) { m_myMenu->addSection(QIcon::fromTheme( QStringLiteral("klipper") ), i18n("%1 - Actions For: %2", clipAct->description(), KStringHandler::csqueeze(text, 45))); QList cmdList = clipAct->commands(); int listSize = cmdList.count(); for (int i=0; isetData(id); action->setText(item); if (!command.icon.isEmpty()) action->setIcon(QIcon::fromTheme(command.icon)); m_myCommandMapper.insert(id, qMakePair(clipAct,i)); m_myMenu->addAction(action); } } // only insert this when invoked via clipboard monitoring, not from an // explicit Ctrl-Alt-R if ( automatically_invoked ) { m_myMenu->addSeparator(); QAction *disableAction = new QAction(i18n("Disable This Popup"), this); connect(disableAction, &QAction::triggered, this, &URLGrabber::sigDisablePopup); m_myMenu->addAction(disableAction); } m_myMenu->addSeparator(); QAction *cancelAction = new QAction(QIcon::fromTheme(QStringLiteral("dialog-cancel")), i18n("&Cancel"), this); connect(cancelAction, &QAction::triggered, m_myMenu, &QMenu::hide); m_myMenu->addAction(cancelAction); m_myClipItem = item; if ( m_myPopupKillTimeout > 0 ) m_myPopupKillTimer->start( 1000 * m_myPopupKillTimeout ); emit sigPopup( m_myMenu ); } } void URLGrabber::slotItemSelected(QAction* action) { if (m_myMenu) m_myMenu->hide(); // deleted by the timer or the next action QString id = action->data().toString(); if (id.isEmpty()) { qCDebug(KLIPPER_LOG) << "Klipper: no command associated"; return; } // first is action ptr, second is command index QPair actionCommand = m_myCommandMapper.value(id); if (actionCommand.first) execute(actionCommand.first, actionCommand.second); else qCDebug(KLIPPER_LOG) << "Klipper: cannot find associated action"; } void URLGrabber::execute( const ClipAction* action, int cmdIdx ) const { if (!action) { qCDebug(KLIPPER_LOG) << "Action object is null"; return; } ClipCommand command = action->command(cmdIdx); if ( command.isEnabled ) { QString text(m_myClipItem->text()); if (m_stripWhiteSpace) { text = text.trimmed(); } if( !command.serviceStorageId.isEmpty()) { KService::Ptr service = KService::serviceByStorageId( command.serviceStorageId ); KRun::runApplication( *service, QList< QUrl >() << QUrl( text ), nullptr ); } else { ClipCommandProcess* proc = new ClipCommandProcess(*action, command, text, m_history, m_myClipItem); if (proc->program().isEmpty()) { delete proc; proc = nullptr; } else { proc->start(); } } } } void URLGrabber::loadSettings() { m_stripWhiteSpace = KlipperSettings::stripWhiteSpace(); m_myAvoidWindows = KlipperSettings::noActionsForWM_CLASS(); m_myPopupKillTimeout = KlipperSettings::timeoutForActionPopups(); qDeleteAll(m_myActions); m_myActions.clear(); KConfigGroup cg(KSharedConfig::openConfig(), "General"); int num = cg.readEntry("Number of Actions", 0); QString group; for ( int i = 0; i < num; i++ ) { group = QStringLiteral("Action_%1").arg( i ); m_myActions.append( new ClipAction( KSharedConfig::openConfig(), group ) ); } } void URLGrabber::saveSettings() const { KConfigGroup cg(KSharedConfig::openConfig(), "General"); cg.writeEntry( "Number of Actions", m_myActions.count() ); int i = 0; QString group; foreach (ClipAction* action, m_myActions) { group = QStringLiteral("Action_%1").arg( i ); action->save( KSharedConfig::openConfig(), group ); ++i; } KlipperSettings::setNoActionsForWM_CLASS(m_myAvoidWindows); } // find out whether the active window's WM_CLASS is in our avoid-list bool URLGrabber::isAvoidedWindow() const { const WId active = KWindowSystem::activeWindow(); if (!active) { return false; } KWindowInfo info(active, NET::Properties(), NET::WM2WindowClass); return m_myAvoidWindows.contains(QString::fromLatin1(info.windowClassName())); } void URLGrabber::slotKillPopupMenu() { if ( m_myMenu && m_myMenu->isVisible() ) { if ( m_myMenu->geometry().contains( QCursor::pos() ) && m_myPopupKillTimeout > 0 ) { m_myPopupKillTimer->start( 1000 * m_myPopupKillTimeout ); return; } } if ( m_myMenu ) { m_myMenu->deleteLater(); m_myMenu = nullptr; } } /////////////////////////////////////////////////////////////////////////// //////// ClipCommand::ClipCommand(const QString&_command, const QString& _description, bool _isEnabled, const QString& _icon, Output _output, const QString& _serviceStorageId) : command(_command), description(_description), isEnabled(_isEnabled), output(_output), serviceStorageId( _serviceStorageId) { if (!_icon.isEmpty()) icon = _icon; else { // try to find suitable icon QString appName = command.section( QLatin1Char(' '), 0, 0 ); if ( !appName.isEmpty() ) { if (QIcon::hasThemeIcon(appName)) icon = appName; else icon.clear(); } } } ClipAction::ClipAction( const QString& regExp, const QString& description, bool automatic ) - : m_myRegExp( regExp ), m_myDescription( description ), m_automatic(automatic) + : m_regexPattern( regExp ), m_myDescription( description ), m_automatic(automatic) { } ClipAction::ClipAction( KSharedConfigPtr kc, const QString& group ) - : m_myRegExp( kc->group(group).readEntry("Regexp") ), + : m_regexPattern( kc->group(group).readEntry("Regexp") ), m_myDescription (kc->group(group).readEntry("Description") ), m_automatic(kc->group(group).readEntry("Automatic", QVariant(true)).toBool() ) { KConfigGroup cg(kc, group); int num = cg.readEntry( "Number of commands", 0 ); // read the commands for ( int i = 0; i < num; i++ ) { QString _group = group + QStringLiteral("/Command_%1"); KConfigGroup _cg(kc, _group.arg(i)); addCommand( ClipCommand(_cg.readPathEntry( "Commandline", QString() ), _cg.readEntry( "Description" ), // i18n'ed _cg.readEntry( "Enabled" , false), _cg.readEntry( "Icon"), static_cast(_cg.readEntry( "Output", QVariant(ClipCommand::IGNORE)).toInt()))); } } ClipAction::~ClipAction() { m_myCommands.clear(); } void ClipAction::addCommand( const ClipCommand& cmd ) { if ( cmd.command.isEmpty() && cmd.serviceStorageId.isEmpty() ) return; m_myCommands.append( cmd ); } void ClipAction::replaceCommand( int idx, const ClipCommand& cmd ) { if ( idx < 0 || idx >= m_myCommands.count() ) { qCDebug(KLIPPER_LOG) << "wrong command index given"; return; } m_myCommands.replace(idx, cmd); } // precondition: we're in the correct action's group of the KConfig object void ClipAction::save( KSharedConfigPtr kc, const QString& group ) const { KConfigGroup cg(kc, group); cg.writeEntry( "Description", description() ); - cg.writeEntry( "Regexp", regExp() ); + cg.writeEntry( "Regexp", actionRegexPattern() ); cg.writeEntry( "Number of commands", m_myCommands.count() ); cg.writeEntry( "Automatic", automatic() ); int i=0; // now iterate over all commands of this action foreach (const ClipCommand& cmd, m_myCommands) { QString _group = group + QStringLiteral("/Command_%1"); KConfigGroup cg(kc, _group.arg(i)); cg.writePathEntry( "Commandline", cmd.command ); cg.writeEntry( "Description", cmd.description ); cg.writeEntry( "Enabled", cmd.isEnabled ); cg.writeEntry( "Icon", cmd.icon ); cg.writeEntry( "Output", static_cast(cmd.output) ); ++i; } } diff --git a/klipper/urlgrabber.h b/klipper/urlgrabber.h index f8a358e6f..ad384c4b6 100644 --- a/klipper/urlgrabber.h +++ b/klipper/urlgrabber.h @@ -1,195 +1,194 @@ /* This file is part of the KDE project Copyright (C) 2000 by Carsten Pfeiffer This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef URLGRABBER_H #define URLGRABBER_H #include -#include #include #include #include class History; class HistoryItem; class QTimer; class KConfig; class QMenu; class QAction; class ClipAction; struct ClipCommand; typedef QList ActionList; class URLGrabber : public QObject { Q_OBJECT public: explicit URLGrabber(History* history); ~URLGrabber() override; /** * Checks a given string whether it matches any of the user-defined criteria. * If it does, the configured action will be executed. */ void checkNewData( QSharedPointer item ); void invokeAction( QSharedPointer item ); ActionList actionList() const { return m_myActions; } void setActionList( const ActionList& ); void loadSettings(); void saveSettings() const; int popupTimeout() const { return m_myPopupKillTimeout; } void setPopupTimeout( int timeout ) { m_myPopupKillTimeout = timeout; } QStringList excludedWMClasses() const { return m_myAvoidWindows; } void setExcludedWMClasses( const QStringList& list ) { m_myAvoidWindows = list; } bool stripWhiteSpace() const { return m_stripWhiteSpace; } void setStripWhiteSpace( bool enable ) { m_stripWhiteSpace = enable; } private: const ActionList& matchingActions( const QString&, bool automatically_invoked ); void execute( const ClipAction *action, int commandIdx ) const; bool isAvoidedWindow() const; void actionMenu( QSharedPointer item, bool automatically_invoked ); void matchingMimeActions(const QString& clipData); ActionList m_myActions; ActionList m_myMatches; QStringList m_myAvoidWindows; QSharedPointer m_myClipItem; ClipAction* m_myCurrentAction; // holds mappings of menu action IDs to action commands (action+cmd index in it) QHash > m_myCommandMapper; QMenu* m_myMenu; QTimer* m_myPopupKillTimer; int m_myPopupKillTimeout; bool m_stripWhiteSpace; History* m_history; private Q_SLOTS: void slotItemSelected(QAction* action); void slotKillPopupMenu(); Q_SIGNALS: void sigPopup( QMenu * ); void sigDisablePopup(); }; struct ClipCommand { /** * What to do with output of command */ enum Output { IGNORE, // Discard output REPLACE, // Replace clipboard entry with output ADD // Add output as new clipboard element }; ClipCommand( const QString& _command, const QString& _description, bool enabled=true, const QString& _icon=QString(), Output _output=IGNORE, const QString& serviceStorageId = QString()); QString command; QString description; bool isEnabled; QString icon; Output output; // If this is set, it's an app to handle a mimetype, and will be launched normally using KRun. // StorageId is used instead of KService::Ptr, because the latter disallows operator=. QString serviceStorageId; }; Q_DECLARE_METATYPE(ClipCommand::Output) /** * Represents one configured action. An action consists of one regular * expression, an (optional) description and a list of ClipCommands * (a command to be executed, a description and an enabled/disabled flag). */ class ClipAction { public: explicit ClipAction( const QString& regExp = QString(), const QString& description = QString(), bool automagic = true); ClipAction( KSharedConfigPtr kc, const QString& ); ~ClipAction(); - void setRegExp( const QString& r) { m_myRegExp = QRegExp( r ); } - QString regExp() const { return m_myRegExp.pattern(); } + QString actionRegexPattern() const { return m_regexPattern; } + void setActionRegexPattern(const QString &pattern) { m_regexPattern = pattern; } - bool matches( const QString& string ) const { return ( m_myRegExp.indexIn( string ) != -1 ); } - - QStringList regExpMatches() const { return m_myRegExp.capturedTexts(); } + QStringList actionCapturedTexts() const { return m_regexCapturedTexts; } + void setActionCapturedTexts(const QStringList &captured) { m_regexCapturedTexts = captured; } void setDescription( const QString& d) { m_myDescription = d; } QString description() const { return m_myDescription; } void setAutomatic( bool automatic ) { m_automatic = automatic; } bool automatic() const { return m_automatic; } /** * Removes all ClipCommands associated with this ClipAction. */ void clearCommands() { m_myCommands.clear(); } void addCommand(const ClipCommand& cmd); /** * Replaces command at index @p idx with command @p newCmd */ void replaceCommand( int idx, const ClipCommand& newCmd ); /** * Returns command by its index in command list */ ClipCommand command(int idx) const { return m_myCommands.at(idx); } QList commands() const { return m_myCommands; } /** * Saves this action to a a given KConfig object */ void save( KSharedConfigPtr, const QString& ) const; private: - QRegExp m_myRegExp; + QString m_regexPattern; + QStringList m_regexCapturedTexts; QString m_myDescription; QList m_myCommands; bool m_automatic; }; #endif // URLGRABBER_H