1/* This file is part of the KDE project
2 Copyright (C) (C) 2000,2001,2002 by Carsten Pfeiffer <pfeiffer@kde.org>
3
4 This program is free software; you can redistribute it and/or
5 modify it under the terms of the GNU General Public
6 License as published by the Free Software Foundation; either
7 version 2 of the License, or (at your option) any later version.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program; see the file COPYING. If not, write to
16 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17 Boston, MA 02110-1301, USA.
18*/
19#include "urlgrabber.h"
20
21#include <netwm.h>
22
23#include <QtCore/QHash>
24#include <QtCore/QTimer>
25#include <QtCore/QUuid>
26#include <QtCore/QFile>
27#include <QtGui/QX11Info>
28
29#include <KDialog>
30#include <KLocale>
31#include <KMenu>
32#include <KService>
33#include <KDebug>
34#include <KStringHandler>
35#include <KGlobal>
36#include <KMimeTypeTrader>
37#include <KMimeType>
38#include <KCharMacroExpander>
39
40#include "klippersettings.h"
41#include "clipcommandprocess.h"
42
43// TODO: script-interface?
44#include "history.h"
45#include "historystringitem.h"
46
47URLGrabber::URLGrabber(History* history):
48 m_myCurrentAction(0L),
49 m_myMenu(0L),
50 m_myPopupKillTimer(new QTimer( this )),
51 m_myPopupKillTimeout(8),
52 m_stripWhiteSpace(true),
53 m_history(history)
54{
55 m_myPopupKillTimer->setSingleShot( true );
56 connect( m_myPopupKillTimer, SIGNAL(timeout()),
57 SLOT(slotKillPopupMenu()));
58
59 // testing
60 /*
61 ClipAction *action;
62 action = new ClipAction( "^http:\\/\\/", "Web-URL" );
63 action->addCommand("kfmclient exec %s", "Open with Konqi", true);
64 action->addCommand("netscape -no-about-splash -remote \"openURL(%s, new-window)\"", "Open with Netscape", true);
65 m_myActions->append( action );
66
67 action = new ClipAction( "^mailto:", "Mail-URL" );
68 action->addCommand("kmail --composer %s", "Launch kmail", true);
69 m_myActions->append( action );
70
71 action = new ClipAction( "^\\/.+\\.jpg$", "Jpeg-Image" );
72 action->addCommand("kuickshow %s", "Launch KuickShow", true);
73 action->addCommand("kview %s", "Launch KView", true);
74 m_myActions->append( action );
75 */
76}
77
78
79URLGrabber::~URLGrabber()
80{
81 qDeleteAll(m_myActions);
82 m_myActions.clear();
83 delete m_myMenu;
84}
85
86//
87// Called from Klipper::slotRepeatAction, i.e. by pressing Ctrl-Alt-R
88// shortcut. I.e. never from clipboard monitoring
89//
90void URLGrabber::invokeAction( const HistoryItem* item )
91{
92 m_myClipItem = item;
93 actionMenu( item, false );
94}
95
96
97void URLGrabber::setActionList( const ActionList& list )
98{
99 qDeleteAll(m_myActions);
100 m_myActions.clear();
101 m_myActions = list;
102}
103
104void URLGrabber::matchingMimeActions(const QString& clipData)
105{
106 KUrl url(clipData);
107 KConfigGroup cg(KGlobal::config(), "Actions");
108 if(!cg.readEntry("EnableMagicMimeActions",true)) {
109 //kDebug() << "skipping mime magic due to configuration";
110 return;
111 }
112 if(!url.isValid()) {
113 //kDebug() << "skipping mime magic due to invalid url";
114 return;
115 }
116 if(url.isRelative()) { //openinng a relative path will just not work. what path should be used?
117 //kDebug() << "skipping mime magic due to relative url";
118 return;
119 }
120 if(url.isLocalFile()) {
121 if ( clipData == "//") {
122 //kDebug() << "skipping mime magic due to C++ comment //";
123 return;
124 }
125 if(!QFile::exists(url.toLocalFile())) {
126 //kDebug() << "skipping mime magic due to nonexistent localfile";
127 return;
128 }
129 }
130
131 // try to figure out if clipData contains a filename
132 KMimeType::Ptr mimetype = KMimeType::findByUrl( url, 0,
133 false,
134 true /*fast mode*/ );
135
136 // let's see if we found some reasonable mimetype.
137 // If we do we'll populate menu with actions for apps
138 // that can handle that mimetype
139
140 // first: if clipboard contents starts with http, let's assume it's "text/html".
141 // That is even if we've url like "http://www.kde.org/somescript.pl", we'll
142 // still treat that as html page, because determining a mimetype using kio
143 // might take a long time, and i want this function to be quick!
144 if ( ( clipData.startsWith( QLatin1String("http://") ) || clipData.startsWith( QLatin1String("https://") ) )
145 && mimetype->name() != "text/html" )
146 {
147 // use a fake path to create a mimetype that corresponds to "text/html"
148 mimetype = KMimeType::findByPath( "/tmp/klipper.html", 0, true /*fast mode*/ );
149 }
150
151 if ( !mimetype->isDefault() ) {
152 ClipAction* action = new ClipAction( QString(), mimetype->comment() );
153 KService::List lst = KMimeTypeTrader::self()->query( mimetype->name(), "Application" );
154 foreach( const KService::Ptr &service, lst ) {
155 QHash<QChar,QString> map;
156 map.insert( 'i', "--icon " + service->icon() );
157 map.insert( 'c', service->name() );
158
159 QString exec = service->exec();
160 exec = KMacroExpander::expandMacros( exec, map ).trimmed();
161
162 action->addCommand( ClipCommand( exec, service->name(), true, service->icon() ) );
163 }
164 if ( !lst.isEmpty() )
165 m_myMatches.append( action );
166 }
167}
168
169const ActionList& URLGrabber::matchingActions( const QString& clipData, bool automatically_invoked )
170{
171 m_myMatches.clear();
172
173 matchingMimeActions(clipData);
174
175
176 // now look for matches in custom user actions
177 foreach (ClipAction* action, m_myActions) {
178 if ( action->matches( clipData ) && (action->automatic() || !automatically_invoked) ) {
179 m_myMatches.append( action );
180 }
181 }
182
183 return m_myMatches;
184}
185
186
187void URLGrabber::checkNewData( const HistoryItem* item )
188{
189 // kDebug() << "** checking new data: " << clipData;
190 actionMenu( item, true ); // also creates m_myMatches
191}
192
193
194void URLGrabber::actionMenu( const HistoryItem* item, bool automatically_invoked )
195{
196 if (!item) {
197 qWarning("Attempt to invoke URLGrabber without an item");
198 return;
199 }
200 QString text(item->text());
201 if (m_stripWhiteSpace) {
202 text = text.trimmed();
203 }
204 ActionList matchingActionsList = matchingActions( text, automatically_invoked );
205
206 if (!matchingActionsList.isEmpty()) {
207 // don't react on blacklisted (e.g. konqi's/netscape's urls) unless the user explicitly asked for it
208 if ( automatically_invoked && isAvoidedWindow() ) {
209 return;
210 }
211
212 m_myCommandMapper.clear();
213
214 m_myPopupKillTimer->stop();
215
216 m_myMenu = new KMenu;
217
218 connect(m_myMenu, SIGNAL(triggered(QAction*)), SLOT(slotItemSelected(QAction*)));
219
220 foreach (ClipAction* clipAct, matchingActionsList) {
221 m_myMenu->addTitle(KIcon( "klipper" ),
222 i18n("%1 - Actions For: %2", clipAct->description(), KStringHandler::csqueeze(text, 45)));
223 QList<ClipCommand> cmdList = clipAct->commands();
224 int listSize = cmdList.count();
225 for (int i=0; i<listSize;++i) {
226 ClipCommand command = cmdList.at(i);
227
228 QString item = command.description;
229 if ( item.isEmpty() )
230 item = command.command;
231
232 QString id = QUuid::createUuid().toString();
233 QAction * action = new QAction(this);
234 action->setData(id);
235 action->setText(item);
236
237 if (!command.icon.isEmpty())
238 action->setIcon(KIcon(command.icon));
239
240 m_myCommandMapper.insert(id, qMakePair(clipAct,i));
241 m_myMenu->addAction(action);
242 }
243 }
244
245 // only insert this when invoked via clipboard monitoring, not from an
246 // explicit Ctrl-Alt-R
247 if ( automatically_invoked )
248 {
249 m_myMenu->addSeparator();
250 QAction *disableAction = new QAction(i18n("Disable This Popup"), this);
251 connect(disableAction, SIGNAL(triggered()), SIGNAL(sigDisablePopup()));
252 m_myMenu->addAction(disableAction);
253 }
254 m_myMenu->addSeparator();
255
256 QAction *cancelAction = new QAction(KIcon("dialog-cancel"), i18n("&Cancel"), this);
257 connect(cancelAction, SIGNAL(triggered()), m_myMenu, SLOT(hide()));
258 m_myMenu->addAction(cancelAction);
259 m_myClipItem = item;
260
261 if ( m_myPopupKillTimeout > 0 )
262 m_myPopupKillTimer->start( 1000 * m_myPopupKillTimeout );
263
264 emit sigPopup( m_myMenu );
265 }
266}
267
268
269void URLGrabber::slotItemSelected(QAction* action)
270{
271 if (m_myMenu)
272 m_myMenu->hide(); // deleted by the timer or the next action
273
274 QString id = action->data().toString();
275
276 if (id.isEmpty()) {
277 kDebug() << "Klipper: no command associated";
278 return;
279 }
280
281 // first is action ptr, second is command index
282 QPair<ClipAction*, int> actionCommand = m_myCommandMapper.value(id);
283
284 if (actionCommand.first)
285 execute(actionCommand.first, actionCommand.second);
286 else
287 kDebug() << "Klipper: cannot find associated action";
288}
289
290
291void URLGrabber::execute( const ClipAction* action, int cmdIdx ) const
292{
293 if (!action) {
294 kDebug() << "Action object is null";
295 return;
296 }
297
298 ClipCommand command = action->command(cmdIdx);
299
300 if ( command.isEnabled ) {
301 QString text(m_myClipItem->text());
302 if (m_stripWhiteSpace) {
303 text = text.trimmed();
304 }
305 ClipCommandProcess* proc = new ClipCommandProcess(*action, command, text, m_history, m_myClipItem);
306 if (proc->program().isEmpty()) {
307 delete proc;
308 proc = 0L;
309 } else {
310 proc->start();
311 }
312 }
313}
314
315void URLGrabber::loadSettings()
316{
317 m_stripWhiteSpace = KlipperSettings::stripWhiteSpace();
318 m_myAvoidWindows = KlipperSettings::noActionsForWM_CLASS();
319 m_myPopupKillTimeout = KlipperSettings::timeoutForActionPopups();
320
321 qDeleteAll(m_myActions);
322 m_myActions.clear();
323
324 KConfigGroup cg(KGlobal::config(), "General");
325 int num = cg.readEntry("Number of Actions", 0);
326 QString group;
327 for ( int i = 0; i < num; i++ ) {
328 group = QString("Action_%1").arg( i );
329 m_myActions.append( new ClipAction( KGlobal::config(), group ) );
330 }
331}
332
333void URLGrabber::saveSettings() const
334{
335 KConfigGroup cg(KGlobal::config(), "General");
336 cg.writeEntry( "Number of Actions", m_myActions.count() );
337
338 int i = 0;
339 QString group;
340 foreach (ClipAction* action, m_myActions) {
341 group = QString("Action_%1").arg( i );
342 action->save( KGlobal::config(), group );
343 ++i;
344 }
345
346 KlipperSettings::setNoActionsForWM_CLASS(m_myAvoidWindows);
347}
348
349// find out whether the active window's WM_CLASS is in our avoid-list
350// digged a little bit in netwm.cpp
351bool URLGrabber::isAvoidedWindow() const
352{
353#ifdef Q_WS_X11
354 Display *d = QX11Info::display();
355 static Atom wm_class = XInternAtom( d, "WM_CLASS", true );
356 static Atom active_window = XInternAtom( d, "_NET_ACTIVE_WINDOW", true );
357 Atom type_ret;
358 int format_ret;
359 unsigned long nitems_ret, unused;
360 unsigned char *data_ret;
361 long BUFSIZE = 2048;
362 bool ret = false;
363 Window active = 0L;
364 QString wmClass;
365
366 // get the active window
367 if (XGetWindowProperty(d, DefaultRootWindow( d ), active_window, 0l, 1l,
368 False, XA_WINDOW, &type_ret, &format_ret,
369 &nitems_ret, &unused, &data_ret)
370 == Success) {
371 if (type_ret == XA_WINDOW && format_ret == 32 && nitems_ret == 1) {
372 active = *((Window *) data_ret);
373 }
374 XFree(data_ret);
375 }
376 if ( !active )
377 return false;
378
379 // get the class of the active window
380 if ( XGetWindowProperty(d, active, wm_class, 0L, BUFSIZE, False, XA_STRING,
381 &type_ret, &format_ret, &nitems_ret,
382 &unused, &data_ret ) == Success) {
383 if ( type_ret == XA_STRING && format_ret == 8 && nitems_ret > 0 ) {
384 wmClass = QString::fromUtf8( (const char *) data_ret );
385 ret = (m_myAvoidWindows.indexOf( wmClass ) != -1);
386 }
387
388 XFree( data_ret );
389 }
390
391 return ret;
392#else
393 return false;
394#endif
395}
396
397
398void URLGrabber::slotKillPopupMenu()
399{
400 if ( m_myMenu && m_myMenu->isVisible() )
401 {
402 if ( m_myMenu->geometry().contains( QCursor::pos() ) &&
403 m_myPopupKillTimeout > 0 )
404 {
405 m_myPopupKillTimer->start( 1000 * m_myPopupKillTimeout );
406 return;
407 }
408 }
409
410 if ( m_myMenu ) {
411 m_myMenu->deleteLater();
412 m_myMenu = 0;
413 }
414}
415
416///////////////////////////////////////////////////////////////////////////
417////////
418
419ClipCommand::ClipCommand(const QString&_command, const QString& _description,
420 bool _isEnabled, const QString& _icon, Output _output)
421 : command(_command),
422 description(_description),
423 isEnabled(_isEnabled),
424 output(_output)
425{
426
427 if (!_icon.isEmpty())
428 icon = _icon;
429 else
430 {
431 // try to find suitable icon
432 QString appName = command.section( ' ', 0, 0 );
433 if ( !appName.isEmpty() )
434 {
435 QPixmap iconPix = KIconLoader::global()->loadIcon(
436 appName, KIconLoader::Small, 0,
437 KIconLoader::DefaultState,
438 QStringList(), 0, true /* canReturnNull */ );
439 if ( !iconPix.isNull() )
440 icon = appName;
441 else
442 icon.clear();
443 }
444 }
445}
446
447
448ClipAction::ClipAction( const QString& regExp, const QString& description, bool automatic )
449 : m_myRegExp( regExp ), m_myDescription( description ), m_automatic(automatic)
450{
451}
452
453ClipAction::ClipAction( KSharedConfigPtr kc, const QString& group )
454 : m_myRegExp( kc->group(group).readEntry("Regexp") ),
455 m_myDescription (kc->group(group).readEntry("Description") ),
456 m_automatic(kc->group(group).readEntry("Automatic", QVariant(true)).toBool() )
457{
458 KConfigGroup cg(kc, group);
459
460 int num = cg.readEntry( "Number of commands", 0 );
461
462 // read the commands
463 for ( int i = 0; i < num; i++ ) {
464 QString _group = group + "/Command_%1";
465 KConfigGroup _cg(kc, _group.arg(i));
466
467 addCommand( ClipCommand(_cg.readPathEntry( "Commandline", QString() ),
468 _cg.readEntry( "Description" ), // i18n'ed
469 _cg.readEntry( "Enabled" , false),
470 _cg.readEntry( "Icon"),
471 static_cast<ClipCommand::Output>(_cg.readEntry( "Output", QVariant(ClipCommand::IGNORE)).toInt())));
472 }
473}
474
475
476ClipAction::~ClipAction()
477{
478 m_myCommands.clear();
479}
480
481
482void ClipAction::addCommand( const ClipCommand& cmd )
483{
484 if ( cmd.command.isEmpty() )
485 return;
486
487 m_myCommands.append( cmd );
488}
489
490void ClipAction::replaceCommand( int idx, const ClipCommand& cmd )
491{
492 if ( idx < 0 || idx >= m_myCommands.count() ) {
493 kDebug() << "wrong command index given";
494 return;
495 }
496
497 m_myCommands.replace(idx, cmd);
498}
499
500
501// precondition: we're in the correct action's group of the KConfig object
502void ClipAction::save( KSharedConfigPtr kc, const QString& group ) const
503{
504 KConfigGroup cg(kc, group);
505 cg.writeEntry( "Description", description() );
506 cg.writeEntry( "Regexp", regExp() );
507 cg.writeEntry( "Number of commands", m_myCommands.count() );
508 cg.writeEntry( "Automatic", automatic() );
509
510 int i=0;
511 // now iterate over all commands of this action
512 foreach (const ClipCommand& cmd, m_myCommands) {
513 QString _group = group + "/Command_%1";
514 KConfigGroup cg(kc, _group.arg(i));
515
516 cg.writePathEntry( "Commandline", cmd.command );
517 cg.writeEntry( "Description", cmd.description );
518 cg.writeEntry( "Enabled", cmd.isEnabled );
519 cg.writeEntry( "Icon", cmd.icon );
520 cg.writeEntry( "Output", static_cast<int>(cmd.output) );
521
522 ++i;
523 }
524}
525
526#include "urlgrabber.moc"
527