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 | |
47 | URLGrabber::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 | |
79 | URLGrabber::~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 | // |
90 | void URLGrabber::invokeAction( const HistoryItem* item ) |
91 | { |
92 | m_myClipItem = item; |
93 | actionMenu( item, false ); |
94 | } |
95 | |
96 | |
97 | void URLGrabber::setActionList( const ActionList& list ) |
98 | { |
99 | qDeleteAll(m_myActions); |
100 | m_myActions.clear(); |
101 | m_myActions = list; |
102 | } |
103 | |
104 | void 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 | |
169 | const 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 | |
187 | void URLGrabber::checkNewData( const HistoryItem* item ) |
188 | { |
189 | // kDebug() << "** checking new data: " << clipData; |
190 | actionMenu( item, true ); // also creates m_myMatches |
191 | } |
192 | |
193 | |
194 | void URLGrabber::( 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 | |
269 | void 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 | |
291 | void 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 | |
315 | void 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 | |
333 | void 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 |
351 | bool 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 | |
398 | void URLGrabber::() |
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 | |
419 | ClipCommand::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 | |
448 | ClipAction::ClipAction( const QString& regExp, const QString& description, bool automatic ) |
449 | : m_myRegExp( regExp ), m_myDescription( description ), m_automatic(automatic) |
450 | { |
451 | } |
452 | |
453 | ClipAction::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 | |
476 | ClipAction::~ClipAction() |
477 | { |
478 | m_myCommands.clear(); |
479 | } |
480 | |
481 | |
482 | void ClipAction::addCommand( const ClipCommand& cmd ) |
483 | { |
484 | if ( cmd.command.isEmpty() ) |
485 | return; |
486 | |
487 | m_myCommands.append( cmd ); |
488 | } |
489 | |
490 | void 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 |
502 | void 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 | |