mirror of https://github.com/Qortal/Brooklyn
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
433 lines
13 KiB
433 lines
13 KiB
/* |
|
SPDX-FileCopyrightText: 2000, 2001, 2002 Carsten Pfeiffer <[email protected]> |
|
|
|
SPDX-License-Identifier: GPL-2.0-or-later |
|
*/ |
|
#include "urlgrabber.h" |
|
|
|
#include <netwm.h> |
|
|
|
#include "klipper_debug.h" |
|
#include <QFile> |
|
#include <QIcon> |
|
#include <QMenu> |
|
#include <QMimeDatabase> |
|
#include <QRegularExpression> |
|
#include <QTimer> |
|
#include <QUuid> |
|
|
|
#include <KApplicationTrader> |
|
#include <KIO/ApplicationLauncherJob> |
|
#include <KLocalizedString> |
|
#include <KNotificationJobUiDelegate> |
|
#include <KService> |
|
#include <KStringHandler> |
|
#include <KWindowSystem> |
|
|
|
#include "clipcommandprocess.h" |
|
#include "klippersettings.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); |
|
} |
|
|
|
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()) { |
|
return; |
|
} |
|
if (!url.isValid()) { |
|
return; |
|
} |
|
if (url.isRelative()) { // openinng a relative path will just not work. what path should be used? |
|
return; |
|
} |
|
if (url.isLocalFile()) { |
|
if (clipData == QLatin1String("//")) { |
|
return; |
|
} |
|
if (!QFile::exists(url.toLocalFile())) { |
|
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 = KApplicationTrader::queryByMimeType(mimetype.name()); |
|
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) { |
|
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) |
|
{ |
|
actionMenu(item, true); // also creates m_myMatches |
|
} |
|
|
|
void URLGrabber::actionMenu(HistoryItemConstPtr item, bool automatically_invoked) |
|
{ |
|
if (!item) { |
|
qCWarning(KLIPPER_LOG, "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")), clipAct->description()); |
|
QList<ClipCommand> cmdList = clipAct->commands(); |
|
int listSize = cmdList.count(); |
|
for (int i = 0; i < listSize; ++i) { |
|
ClipCommand command = cmdList.at(i); |
|
|
|
QString item = command.description; |
|
if (item.isEmpty()) |
|
item = command.command; |
|
|
|
QString id = QUuid::createUuid().toString(); |
|
QAction *action = new QAction(this); |
|
action->setData(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); |
|
|
|
Q_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<ClipAction *, int> 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); |
|
auto *job = new KIO::ApplicationLauncherJob(service); |
|
job->setUrls({QUrl(text)}); |
|
job->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled)); |
|
job->start(); |
|
} 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 ®Exp, const QString &description, bool automatic) |
|
: m_regexPattern(regExp) |
|
, m_myDescription(description) |
|
, m_automatic(automatic) |
|
{ |
|
} |
|
|
|
ClipAction::ClipAction(KSharedConfigPtr kc, const QString &group) |
|
: 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<ClipCommand::Output>(_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", 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<int>(cmd.output)); |
|
|
|
++i; |
|
} |
|
}
|
|
|