diff --git a/klipper/CMakeLists.txt b/klipper/CMakeLists.txt index cf4b5c972..3cf37cd49 100644 --- a/klipper/CMakeLists.txt +++ b/klipper/CMakeLists.txt @@ -1,103 +1,104 @@ set(KLIPPER_VERSION_STRING ${PROJECT_VERSION}) add_definitions(-DTRANSLATION_DOMAIN=\"klipper\") set(libklipper_common_SRCS klipper_debug.cpp klipper.cpp urlgrabber.cpp configdialog.cpp history.cpp historyitem.cpp historymodel.cpp historystringitem.cpp klipperpopup.cpp popupproxy.cpp historyimageitem.cpp historyurlitem.cpp actionstreewidget.cpp editactiondialog.cpp clipcommandprocess.cpp ) find_package(KF5Prison ${KF5_MIN_VERSION}) set_package_properties(KF5Prison PROPERTIES DESCRIPTION "Prison library" URL "http://projects.kde.org/prison" TYPE OPTIONAL PURPOSE "Needed to create mobile barcodes from clipboard data" ) set(HAVE_PRISON ${KF5Prison_FOUND}) configure_file(config-klipper.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-klipper.h ) kde4_add_app_icon(libklipper_common_SRCS "${KDE4_ICON_INSTALL_DIR}/oxygen/*/apps/klipper.png") ki18n_wrap_ui(libklipper_common_SRCS generalconfig.ui actionsconfig.ui editactiondialog.ui) kconfig_add_kcfg_files(libklipper_common_SRCS klippersettings.kcfgc) set(klipper_KDEINIT_SRCS ${libklipper_common_SRCS} main.cpp tray.cpp) kf5_add_kdeinit_executable(klipper ${klipper_KDEINIT_SRCS}) target_link_libraries(kdeinit_klipper Qt5::Concurrent KF5::Completion # klineedit - port away? KF5::ConfigGui KF5::CoreAddons KF5::DBusAddons KF5::GlobalAccel KF5::IconThemes + KF5::KIOWidgets KF5::Notifications KF5::Service KF5::TextWidgets KF5::WindowSystem KF5::WidgetsAddons KF5::XmlGui ${ZLIB_LIBRARY} ) if (X11_FOUND) target_link_libraries(kdeinit_klipper XCB::XCB Qt5::X11Extras) endif() if (HAVE_PRISON) target_link_libraries(kdeinit_klipper KF5::Prison) endif () install(TARGETS kdeinit_klipper ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) install(TARGETS klipper ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) install(PROGRAMS org.kde.klipper.desktop DESTINATION ${KDE_INSTALL_APPDIR}) install(PROGRAMS klipper.desktop DESTINATION ${KDE_INSTALL_AUTOSTARTDIR}) # Plasma Data Engine set(plasma_engine_clipboard_SRCS ${libklipper_common_SRCS} clipboardengine.cpp clipboardservice.cpp clipboardjob.cpp) add_library(plasma_engine_clipboard MODULE ${plasma_engine_clipboard_SRCS}) kcoreaddons_desktop_to_json(plasma_engine_clipboard plasma-dataengine-clipboard.desktop) target_link_libraries(plasma_engine_clipboard Qt5::Concurrent Qt5::DBus Qt5::Widgets # QAction KF5::ConfigGui KF5::Completion # klineedit - port away? KF5::CoreAddons # KUrlMimeData KF5::GlobalAccel KF5::IconThemes KF5::KIOWidgets # PreviewJob KF5::Plasma KF5::Notifications KF5::Service KF5::TextWidgets # KTextEdit KF5::WidgetsAddons # KMessageBox KF5::WindowSystem KF5::XmlGui # KActionCollection ${ZLIB_LIBRARY} ) if (X11_FOUND) target_link_libraries(plasma_engine_clipboard XCB::XCB Qt5::X11Extras) endif() if (HAVE_PRISON) target_link_libraries(plasma_engine_clipboard KF5::Prison) endif () install(TARGETS plasma_engine_clipboard DESTINATION ${KDE_INSTALL_PLUGINDIR}/plasma/dataengine) install(FILES plasma-dataengine-clipboard.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}) install(FILES org.kde.plasma.clipboard.operations DESTINATION ${PLASMA_DATA_INSTALL_DIR}/services) add_subdirectory(autotests) diff --git a/klipper/urlgrabber.cpp b/klipper/urlgrabber.cpp index 5cdf76b66..6a7a30fb9 100644 --- a/klipper/urlgrabber.cpp +++ b/klipper/urlgrabber.cpp @@ -1,486 +1,487 @@ /* 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(0L), m_myMenu(0L), 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); KConfigGroup cg(KSharedConfig::openConfig(), "Actions"); if(!cg.readEntry("EnableMagicMimeActions",true)) { //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 ) { - QHash map; - map.insert( 'i', "--icon " + service->icon() ); - map.insert( 'c', service->name() ); - - QString exec = service->exec(); - exec = KMacroExpander::expandMacros( exec, map ).trimmed(); - - action->addCommand( ClipCommand( exec, service->name(), true, service->icon() ) ); + 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 foreach (ClipAction* action, m_myActions) { if ( action->matches( clipData ) && (action->automatic() || !automatically_invoked) ) { 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(); } - ClipCommandProcess* proc = new ClipCommandProcess(*action, command, text, m_history, m_myClipItem); - if (proc->program().isEmpty()) { - delete proc; - proc = 0L; + if( !command.serviceStorageId.isEmpty()) { + KService::Ptr service = KService::serviceByStorageId( command.serviceStorageId ); + KRun::runApplication( *service, QList< QUrl >() << QUrl( text ), nullptr ); } else { - proc->start(); + ClipCommandProcess* proc = new ClipCommandProcess(*action, command, text, m_history, m_myClipItem); + if (proc->program().isEmpty()) { + delete proc; + proc = 0L; + } 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(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 = 0; } } /////////////////////////////////////////////////////////////////////////// //////// ClipCommand::ClipCommand(const QString&_command, const QString& _description, - bool _isEnabled, const QString& _icon, Output _output) + bool _isEnabled, const QString& _icon, Output _output, + const QString& _serviceStorageId) : command(_command), description(_description), isEnabled(_isEnabled), - output(_output) + output(_output), + serviceStorageId( _serviceStorageId) { if (!_icon.isEmpty()) icon = _icon; else { // try to find suitable icon QString appName = command.section( ' ', 0, 0 ); if ( !appName.isEmpty() ) { QPixmap iconPix = KIconLoader::global()->loadIcon( appName, KIconLoader::Small, 0, KIconLoader::DefaultState, QStringList(), 0, true /* canReturnNull */ ); if ( !iconPix.isNull() ) icon = appName; else icon.clear(); } } } ClipAction::ClipAction( const QString& regExp, const QString& description, bool automatic ) : m_myRegExp( regExp ), m_myDescription( description ), m_automatic(automatic) { } ClipAction::ClipAction( KSharedConfigPtr kc, const QString& group ) : m_myRegExp( 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 + "/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() ) + 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( "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 + "/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 da51017b5..05cbfa958 100644 --- a/klipper/urlgrabber.h +++ b/klipper/urlgrabber.h @@ -1,191 +1,195 @@ /* 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: 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); + 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(); } bool matches( const QString& string ) const { return ( m_myRegExp.indexIn( string ) != -1 ); } QStringList regExpMatches() const { return m_myRegExp.capturedTexts(); } 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_myDescription; QList m_myCommands; bool m_automatic; }; #endif // URLGRABBER_H