1 | /* This file is part of the KDE project |
2 | |
3 | Copyright (C) by Andrew Stanley-Jones <asj@cban.com> |
4 | Copyright (C) 2000 by Carsten Pfeiffer <pfeiffer@kde.org> |
5 | Copyright (C) 2004 Esben Mose Hansen <kde@mosehansen.dk> |
6 | Copyright (C) 2008 by Dmitry Suzdalev <dimsuz@gmail.com> |
7 | |
8 | This program is free software; you can redistribute it and/or |
9 | modify it under the terms of the GNU General Public |
10 | License as published by the Free Software Foundation; either |
11 | version 2 of the License, or (at your option) any later version. |
12 | |
13 | This program is distributed in the hope that it will be useful, |
14 | but WITHOUT ANY WARRANTY; without even the implied warranty of |
15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
16 | General Public License for more details. |
17 | |
18 | You should have received a copy of the GNU General Public License |
19 | along with this program; see the file COPYING. If not, write to |
20 | the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, |
21 | Boston, MA 02110-1301, USA. |
22 | */ |
23 | |
24 | #include "klipper.h" |
25 | |
26 | #include <zlib.h> |
27 | |
28 | #include <QtGui/QMenu> |
29 | #include <QtDBus/QDBusConnection> |
30 | |
31 | #include <KAboutData> |
32 | #include <KLocale> |
33 | #include <KMessageBox> |
34 | #include <KSaveFile> |
35 | #include <KSessionManager> |
36 | #include <KStandardDirs> |
37 | #include <KDebug> |
38 | #include <KGlobalSettings> |
39 | #include <KActionCollection> |
40 | #include <KToggleAction> |
41 | #include <KTextEdit> |
42 | #include <KApplication> |
43 | #include <KIcon> |
44 | |
45 | #include "configdialog.h" |
46 | #include "klippersettings.h" |
47 | #include "urlgrabber.h" |
48 | #include "version.h" |
49 | #include "history.h" |
50 | #include "historyitem.h" |
51 | #include "historystringitem.h" |
52 | #include "klipperpopup.h" |
53 | |
54 | #ifdef HAVE_PRISON |
55 | #include <prison/BarcodeWidget> |
56 | #include <prison/DataMatrixBarcode> |
57 | #include <prison/QRCodeBarcode> |
58 | #endif |
59 | |
60 | #ifdef Q_WS_X11 |
61 | #include <X11/Xlib.h> |
62 | #include <X11/Xatom.h> |
63 | #endif |
64 | |
65 | //#define NOISY_KLIPPER |
66 | |
67 | namespace { |
68 | /** |
69 | * Use this when manipulating the clipboard |
70 | * from within clipboard-related signals. |
71 | * |
72 | * This avoids issues such as mouse-selections that immediately |
73 | * disappear. |
74 | * pattern: Resource Acqusition is Initialisation (RAII) |
75 | * |
76 | * (This is not threadsafe, so don't try to use such in threaded |
77 | * applications). |
78 | */ |
79 | struct Ignore { |
80 | Ignore(int& locklevel) : locklevelref(locklevel) { |
81 | locklevelref++; |
82 | } |
83 | ~Ignore() { |
84 | locklevelref--; |
85 | } |
86 | private: |
87 | int& locklevelref; |
88 | }; |
89 | } |
90 | |
91 | /** |
92 | * Helper class to save history upon session exit. |
93 | */ |
94 | class KlipperSessionManager : public KSessionManager |
95 | { |
96 | public: |
97 | KlipperSessionManager( Klipper* k ) |
98 | : klipper( k ) |
99 | {} |
100 | |
101 | virtual ~KlipperSessionManager() {} |
102 | |
103 | /** |
104 | * Save state upon session exit. |
105 | * |
106 | * Saving history on session save |
107 | */ |
108 | virtual bool commitData( QSessionManager& ) { |
109 | klipper->saveSession(); |
110 | return true; |
111 | } |
112 | private: |
113 | Klipper* klipper; |
114 | }; |
115 | |
116 | // config == KGlobal::config for process, otherwise applet |
117 | Klipper::Klipper(QObject* parent, const KSharedConfigPtr& config) |
118 | : QObject( parent ) |
119 | , m_overflowCounter( 0 ) |
120 | , m_locklevel( 0 ) |
121 | , m_config( config ) |
122 | , m_pendingContentsCheck( false ) |
123 | , m_sessionManager( new KlipperSessionManager( this )) |
124 | { |
125 | setenv("KSNI_NO_DBUSMENU" , "1" , 1); |
126 | QDBusConnection::sessionBus().registerObject("/klipper" , this, QDBusConnection::ExportScriptableSlots); |
127 | |
128 | updateTimestamp(); // read initial X user time |
129 | m_clip = kapp->clipboard(); |
130 | |
131 | connect( m_clip, SIGNAL(changed(QClipboard::Mode)), |
132 | this, SLOT(newClipData(QClipboard::Mode)) ); |
133 | |
134 | connect( &m_overflowClearTimer, SIGNAL(timeout()), SLOT(slotClearOverflow())); |
135 | |
136 | m_pendingCheckTimer.setSingleShot( true ); |
137 | connect( &m_pendingCheckTimer, SIGNAL(timeout()), SLOT(slotCheckPending())); |
138 | |
139 | |
140 | m_history = new History( this ); |
141 | |
142 | // we need that collection, otherwise KToggleAction is not happy :} |
143 | m_collection = new KActionCollection( this ); |
144 | |
145 | m_toggleURLGrabAction = new KToggleAction( this ); |
146 | m_collection->addAction( "clipboard_action" , m_toggleURLGrabAction ); |
147 | m_toggleURLGrabAction->setText(i18n("Enable Clipboard Actions" )); |
148 | m_toggleURLGrabAction->setGlobalShortcut(KShortcut(Qt::ALT+Qt::CTRL+Qt::Key_X)); |
149 | connect( m_toggleURLGrabAction, SIGNAL(toggled(bool)), |
150 | this, SLOT(setURLGrabberEnabled(bool))); |
151 | |
152 | /* |
153 | * Create URL grabber |
154 | */ |
155 | m_myURLGrabber = new URLGrabber(m_history); |
156 | connect( m_myURLGrabber, SIGNAL(sigPopup(QMenu*)), |
157 | SLOT(showPopupMenu(QMenu*)) ); |
158 | connect( m_myURLGrabber, SIGNAL(sigDisablePopup()), |
159 | SLOT(disableURLGrabber()) ); |
160 | |
161 | /* |
162 | * Load configuration settings |
163 | */ |
164 | loadSettings(); |
165 | |
166 | // load previous history if configured |
167 | if (m_bKeepContents) { |
168 | loadHistory(); |
169 | } |
170 | |
171 | m_clearHistoryAction = m_collection->addAction( "clear-history" ); |
172 | m_clearHistoryAction->setIcon( KIcon("edit-clear-history" ) ); |
173 | m_clearHistoryAction->setText( i18n("C&lear Clipboard History" ) ); |
174 | m_clearHistoryAction->setGlobalShortcut(KShortcut()); |
175 | connect(m_clearHistoryAction, SIGNAL(triggered()), SLOT(slotAskClearHistory())); |
176 | |
177 | m_configureAction = m_collection->addAction( "configure" ); |
178 | m_configureAction->setIcon( KIcon("configure" ) ); |
179 | m_configureAction->setText( i18n("&Configure Klipper..." ) ); |
180 | connect(m_configureAction, SIGNAL(triggered(bool)), SLOT(slotConfigure())); |
181 | |
182 | m_quitAction = m_collection->addAction( "quit" ); |
183 | m_quitAction->setIcon( KIcon("application-exit" ) ); |
184 | m_quitAction->setText( i18nc("@item:inmenu Quit Klipper" , "&Quit" ) ); |
185 | connect(m_quitAction, SIGNAL(triggered(bool)), SLOT(slotQuit())); |
186 | |
187 | m_repeatAction = m_collection->addAction("repeat_action" ); |
188 | m_repeatAction->setText(i18n("Manually Invoke Action on Current Clipboard" )); |
189 | m_repeatAction->setGlobalShortcut(KShortcut(Qt::ALT+Qt::CTRL+Qt::Key_R)); |
190 | connect(m_repeatAction, SIGNAL(triggered()), SLOT(slotRepeatAction())); |
191 | |
192 | // add an edit-possibility |
193 | m_editAction = m_collection->addAction("edit_clipboard" ); |
194 | m_editAction->setIcon(KIcon("document-properties" )); |
195 | m_editAction->setText(i18n("&Edit Contents..." )); |
196 | m_editAction->setGlobalShortcut(KShortcut()); |
197 | connect(m_editAction, SIGNAL(triggered()), SLOT(slotEditData())); |
198 | |
199 | #ifdef HAVE_PRISON |
200 | // add barcode for mobile phones |
201 | m_showBarcodeAction = m_collection->addAction("show-barcode" ); |
202 | m_showBarcodeAction->setText(i18n("&Show Barcode..." )); |
203 | m_showBarcodeAction->setGlobalShortcut(KShortcut()); |
204 | connect(m_showBarcodeAction, SIGNAL(triggered()), SLOT(slotShowBarcode())); |
205 | #endif |
206 | |
207 | // Cycle through history |
208 | m_cycleNextAction = m_collection->addAction("cycleNextAction" ); |
209 | m_cycleNextAction->setText(i18n("Next History Item" )); |
210 | m_cycleNextAction->setGlobalShortcut(KShortcut()); |
211 | connect(m_cycleNextAction, SIGNAL(triggered(bool)), SLOT(slotCycleNext())); |
212 | m_cyclePrevAction = m_collection->addAction("cyclePrevAction" ); |
213 | m_cyclePrevAction->setText(i18n("Previous History Item" )); |
214 | m_cyclePrevAction->setGlobalShortcut(KShortcut()); |
215 | connect(m_cyclePrevAction, SIGNAL(triggered(bool)), SLOT(slotCyclePrev())); |
216 | |
217 | // Action to show Klipper popup on mouse position |
218 | m_showOnMousePos = m_collection->addAction("show-on-mouse-pos" ); |
219 | m_showOnMousePos->setText(i18n("Open Klipper at Mouse Position" )); |
220 | m_showOnMousePos->setGlobalShortcut(KShortcut()); |
221 | connect(m_showOnMousePos, SIGNAL(triggered(bool)), this, SLOT(slotPopupMenu())); |
222 | |
223 | KlipperPopup* = history()->popup(); |
224 | connect ( history(), SIGNAL(topChanged()), SLOT(slotHistoryTopChanged()) ); |
225 | connect( popup, SIGNAL(aboutToShow()), SLOT(slotStartShowTimer()) ); |
226 | |
227 | popup->plugAction( m_toggleURLGrabAction ); |
228 | popup->plugAction( m_clearHistoryAction ); |
229 | popup->plugAction( m_configureAction ); |
230 | popup->plugAction( m_repeatAction ); |
231 | popup->plugAction( m_editAction ); |
232 | #ifdef HAVE_PRISON |
233 | popup->plugAction( m_showBarcodeAction ); |
234 | #endif |
235 | if ( !isApplet() ) { |
236 | popup->plugAction( m_quitAction ); |
237 | } |
238 | } |
239 | |
240 | Klipper::~Klipper() |
241 | { |
242 | delete m_sessionManager; |
243 | delete m_myURLGrabber; |
244 | } |
245 | |
246 | // DBUS |
247 | QString Klipper::getClipboardContents() |
248 | { |
249 | return getClipboardHistoryItem(0); |
250 | } |
251 | |
252 | void Klipper::() { |
253 | slotPopupMenu(); |
254 | } |
255 | void Klipper::() { |
256 | slotRepeatAction(); |
257 | } |
258 | |
259 | |
260 | // DBUS - don't call from Klipper itself |
261 | void Klipper::setClipboardContents(QString s) |
262 | { |
263 | if (s.isEmpty()) |
264 | return; |
265 | Ignore lock( m_locklevel ); |
266 | updateTimestamp(); |
267 | HistoryStringItem* item = new HistoryStringItem( s ); |
268 | setClipboard( *item, Clipboard | Selection); |
269 | history()->insert( item ); |
270 | } |
271 | |
272 | // DBUS - don't call from Klipper itself |
273 | void Klipper::clearClipboardContents() |
274 | { |
275 | updateTimestamp(); |
276 | slotClearClipboard(); |
277 | } |
278 | |
279 | // DBUS - don't call from Klipper itself |
280 | void Klipper::clearClipboardHistory() |
281 | { |
282 | updateTimestamp(); |
283 | slotClearClipboard(); |
284 | history()->slotClear(); |
285 | saveSession(); |
286 | } |
287 | |
288 | // DBUS - don't call from Klipper itself |
289 | void Klipper::saveClipboardHistory() |
290 | { |
291 | if ( m_bKeepContents ) { // save the clipboard eventually |
292 | saveHistory(); |
293 | } |
294 | } |
295 | |
296 | void Klipper::slotStartShowTimer() |
297 | { |
298 | m_showTimer.start(); |
299 | } |
300 | |
301 | void Klipper::loadSettings() |
302 | { |
303 | // Security bug 142882: If user has save clipboard turned off, old data should be deleted from disk |
304 | static bool firstrun = true; |
305 | if (!firstrun && m_bKeepContents && !KlipperSettings::keepClipboardContents()) { |
306 | saveHistory(true); |
307 | } |
308 | firstrun=false; |
309 | |
310 | m_bKeepContents = KlipperSettings::keepClipboardContents(); |
311 | m_bReplayActionInHistory = KlipperSettings::replayActionInHistory(); |
312 | m_bNoNullClipboard = KlipperSettings::preventEmptyClipboard(); |
313 | // 0 is the id of "Ignore selection" radiobutton |
314 | m_bIgnoreSelection = KlipperSettings::ignoreSelection(); |
315 | m_bIgnoreImages = KlipperSettings::ignoreImages(); |
316 | m_bSynchronize = KlipperSettings::syncClipboards(); |
317 | // NOTE: not used atm - kregexpeditor is not ported to kde4 |
318 | m_bUseGUIRegExpEditor = KlipperSettings::useGUIRegExpEditor(); |
319 | m_bSelectionTextOnly = KlipperSettings::selectionTextOnly(); |
320 | |
321 | m_bURLGrabber = KlipperSettings::uRLGrabberEnabled(); |
322 | // this will cause it to loadSettings too |
323 | setURLGrabberEnabled(m_bURLGrabber); |
324 | history()->setMaxSize( KlipperSettings::maxClipItems() ); |
325 | // Convert 4.3 settings |
326 | if (KlipperSettings::synchronize() != 3) { |
327 | // 2 was the id of "Ignore selection" radiobutton |
328 | m_bIgnoreSelection = KlipperSettings::synchronize() == 2; |
329 | // 0 was the id of "Synchronize contents" radiobutton |
330 | m_bSynchronize = KlipperSettings::synchronize() == 0; |
331 | KConfigSkeletonItem* item = KlipperSettings::self()->findItem("SyncClipboards" ); |
332 | item->setProperty(m_bSynchronize); |
333 | item = KlipperSettings::self()->findItem("IgnoreSelection" ); |
334 | item->setProperty(m_bIgnoreSelection); |
335 | item = KlipperSettings::self()->findItem("Synchronize" ); // Mark property as converted. |
336 | item->setProperty(3); |
337 | KlipperSettings::self()->writeConfig(); |
338 | KlipperSettings::self()->readConfig(); |
339 | |
340 | } |
341 | } |
342 | |
343 | void Klipper::saveSettings() const |
344 | { |
345 | m_myURLGrabber->saveSettings(); |
346 | KlipperSettings::self()->setVersion(klipper_version); |
347 | KlipperSettings::self()->writeConfig(); |
348 | |
349 | // other settings should be saved automatically by KConfigDialog |
350 | } |
351 | |
352 | void Klipper::( QMenu* ) |
353 | { |
354 | Q_ASSERT( menu != 0L ); |
355 | |
356 | QSize size = menu->sizeHint(); // geometry is not valid until it's shown |
357 | QPoint pos = QCursor::pos(); |
358 | // ### We can't know where the systray icon is (since it can be hidden or shown |
359 | // in several places), so the cursor position is the only option. |
360 | |
361 | if ( size.height() < pos.y() ) |
362 | pos.ry() -= size.height(); |
363 | |
364 | menu->popup(pos); |
365 | } |
366 | |
367 | bool Klipper::loadHistory() { |
368 | static const char* const failed_load_warning = |
369 | "Failed to load history resource. Clipboard history cannot be read." ; |
370 | // don't use "appdata", klipper is also a kicker applet |
371 | QString history_file_name = KStandardDirs::locateLocal( "data" , "klipper/history2.lst" ); |
372 | QFile history_file( history_file_name ); |
373 | if ( !history_file.exists() ) { |
374 | kWarning() << failed_load_warning << ": " << "History file does not exist" ; |
375 | return false; |
376 | } |
377 | if ( !history_file.open( QIODevice::ReadOnly ) ) { |
378 | kWarning() << failed_load_warning << ": " << history_file.errorString() ; |
379 | return false; |
380 | } |
381 | QDataStream file_stream( &history_file ); |
382 | if( file_stream.atEnd()) { |
383 | kWarning() << failed_load_warning << ": " << "Error in reading data" ; |
384 | return false; |
385 | } |
386 | QByteArray data; |
387 | quint32 crc; |
388 | file_stream >> crc >> data; |
389 | if( crc32( 0, reinterpret_cast<unsigned char *>( data.data() ), data.size() ) != crc ) { |
390 | kWarning() << failed_load_warning << ": " << "CRC checksum does not match" ; |
391 | return false; |
392 | } |
393 | QDataStream history_stream( &data, QIODevice::ReadOnly ); |
394 | |
395 | char* version; |
396 | history_stream >> version; |
397 | delete[] version; |
398 | |
399 | // The list needs to be reversed, as it is saved |
400 | // youngest-first to keep the most important clipboard |
401 | // items at the top, but the history is created oldest |
402 | // first. |
403 | QList<HistoryItem*> reverseList; |
404 | for ( HistoryItem* item = HistoryItem::create( history_stream ); |
405 | item; |
406 | item = HistoryItem::create( history_stream ) ) |
407 | { |
408 | reverseList.prepend( item ); |
409 | } |
410 | |
411 | history()->slotClear(); |
412 | |
413 | for ( QList<HistoryItem*>::const_iterator it = reverseList.constBegin(); |
414 | it != reverseList.constEnd(); |
415 | ++it ) |
416 | { |
417 | history()->forceInsert( *it ); |
418 | } |
419 | |
420 | if ( !history()->empty() ) { |
421 | setClipboard( *history()->first(), Clipboard | Selection ); |
422 | } |
423 | |
424 | return true; |
425 | } |
426 | |
427 | void Klipper::saveHistory(bool empty) { |
428 | static const char* const failed_save_warning = |
429 | "Failed to save history. Clipboard history cannot be saved." ; |
430 | // don't use "appdata", klipper is also a kicker applet |
431 | QString history_file_name( KStandardDirs::locateLocal( "data" , "klipper/history2.lst" ) ); |
432 | if ( history_file_name.isNull() || history_file_name.isEmpty() ) { |
433 | kWarning() << failed_save_warning ; |
434 | return; |
435 | } |
436 | KSaveFile history_file( history_file_name ); |
437 | if ( !history_file.open() ) { |
438 | kWarning() << failed_save_warning ; |
439 | return; |
440 | } |
441 | QByteArray data; |
442 | QDataStream history_stream( &data, QIODevice::WriteOnly ); |
443 | history_stream << klipper_version; // const char* |
444 | |
445 | if (!empty) { |
446 | const HistoryItem *item = history()->first(); |
447 | if (item) { |
448 | do { |
449 | history_stream << item; |
450 | item = history()->find(item->next_uuid()); |
451 | } while (item != history()->first()); |
452 | } |
453 | } |
454 | |
455 | quint32 crc = crc32( 0, reinterpret_cast<unsigned char *>( data.data() ), data.size() ); |
456 | QDataStream ds ( &history_file ); |
457 | ds << crc << data; |
458 | } |
459 | |
460 | // save session on shutdown. Don't simply use the c'tor, as that may not be called. |
461 | void Klipper::saveSession() |
462 | { |
463 | if ( m_bKeepContents ) { // save the clipboard eventually |
464 | saveHistory(); |
465 | } |
466 | saveSettings(); |
467 | } |
468 | |
469 | void Klipper::disableURLGrabber() |
470 | { |
471 | KMessageBox::information( 0L, |
472 | i18n( "You can enable URL actions later by left-clicking on the " |
473 | "Klipper icon and selecting 'Enable Clipboard Actions'" ) ); |
474 | |
475 | setURLGrabberEnabled( false ); |
476 | } |
477 | |
478 | void Klipper::slotConfigure() |
479 | { |
480 | if (KConfigDialog::showDialog("preferences" )) { |
481 | return; |
482 | } |
483 | |
484 | ConfigDialog *dlg = new ConfigDialog( 0, KlipperSettings::self(), this, m_collection, isApplet() ); |
485 | connect(dlg, SIGNAL(settingsChanged(QString)), SLOT(loadSettings())); |
486 | |
487 | dlg->show(); |
488 | } |
489 | |
490 | void Klipper::slotQuit() |
491 | { |
492 | // If the menu was just opened, likely the user |
493 | // selected quit by accident while attempting to |
494 | // click the Klipper icon. |
495 | if ( m_showTimer.elapsed() < 300 ) { |
496 | return; |
497 | } |
498 | |
499 | saveSession(); |
500 | int autoStart = KMessageBox::questionYesNoCancel(0, i18n("Should Klipper start automatically when you login?" ), |
501 | i18n("Automatically Start Klipper?" ), KGuiItem(i18n("Start" )), |
502 | KGuiItem(i18n("Do Not Start" )), KStandardGuiItem::cancel(), "StartAutomatically" ); |
503 | |
504 | KConfigGroup config( KGlobal::config(), "General" ); |
505 | if ( autoStart == KMessageBox::Yes ) { |
506 | config.writeEntry("AutoStart" , true); |
507 | } else if ( autoStart == KMessageBox::No) { |
508 | config.writeEntry("AutoStart" , false); |
509 | } else // cancel chosen don't quit |
510 | return; |
511 | config.sync(); |
512 | |
513 | kapp->quit(); |
514 | |
515 | } |
516 | |
517 | void Klipper::() { |
518 | KlipperPopup* = history()->popup(); |
519 | popup->ensureClean(); |
520 | popup->slotSetTopActive(); |
521 | showPopupMenu( popup ); |
522 | } |
523 | |
524 | |
525 | void Klipper::slotRepeatAction() |
526 | { |
527 | const HistoryStringItem* top = dynamic_cast<const HistoryStringItem*>( history()->first() ); |
528 | if ( top ) { |
529 | m_myURLGrabber->invokeAction( top ); |
530 | } |
531 | } |
532 | |
533 | void Klipper::setURLGrabberEnabled( bool enable ) |
534 | { |
535 | if (enable != m_bURLGrabber) { |
536 | m_bURLGrabber = enable; |
537 | m_lastURLGrabberTextSelection.clear(); |
538 | m_lastURLGrabberTextClipboard.clear(); |
539 | KlipperSettings::setURLGrabberEnabled(enable); |
540 | } |
541 | |
542 | m_toggleURLGrabAction->setChecked( enable ); |
543 | |
544 | // make it update its settings |
545 | m_myURLGrabber->loadSettings(); |
546 | } |
547 | |
548 | void Klipper::slotHistoryTopChanged() { |
549 | if ( m_locklevel ) { |
550 | return; |
551 | } |
552 | |
553 | const HistoryItem* topitem = history()->first(); |
554 | if ( topitem ) { |
555 | setClipboard( *topitem, Clipboard | Selection ); |
556 | } |
557 | if ( m_bReplayActionInHistory && m_bURLGrabber ) { |
558 | slotRepeatAction(); |
559 | } |
560 | } |
561 | |
562 | void Klipper::slotClearClipboard() |
563 | { |
564 | Ignore lock( m_locklevel ); |
565 | |
566 | m_clip->clear(QClipboard::Selection); |
567 | m_clip->clear(QClipboard::Clipboard); |
568 | } |
569 | |
570 | HistoryItem* Klipper::applyClipChanges( const QMimeData* clipData ) |
571 | { |
572 | if ( m_locklevel ) { |
573 | return 0L; |
574 | } |
575 | Ignore lock( m_locklevel ); |
576 | HistoryItem* item = HistoryItem::create( clipData ); |
577 | history()->insert( item ); |
578 | return item; |
579 | |
580 | } |
581 | |
582 | void Klipper::newClipData( QClipboard::Mode mode ) |
583 | { |
584 | if ( m_locklevel ) { |
585 | return; |
586 | } |
587 | |
588 | if( mode == QClipboard::Selection && blockFetchingNewData()) |
589 | return; |
590 | |
591 | checkClipData( mode == QClipboard::Selection ? true : false ); |
592 | |
593 | } |
594 | |
595 | // Protection against too many clipboard data changes. Lyx responds to clipboard data |
596 | // requests with setting new clipboard data, so if Lyx takes over clipboard, |
597 | // Klipper notices, requests this data, this triggers "new" clipboard contents |
598 | // from Lyx, so Klipper notices again, requests this data, ... you get the idea. |
599 | const int MAX_CLIPBOARD_CHANGES = 10; // max changes per second |
600 | |
601 | bool Klipper::blockFetchingNewData() |
602 | { |
603 | #ifdef Q_WS_X11 |
604 | // Hacks for #85198 and #80302. |
605 | // #85198 - block fetching new clipboard contents if Shift is pressed and mouse is not, |
606 | // this may mean the user is doing selection using the keyboard, in which case |
607 | // it's possible the app sets new clipboard contents after every change - Klipper's |
608 | // history would list them all. |
609 | // #80302 - OOo (v1.1.3 at least) has a bug that if Klipper requests its clipboard contents |
610 | // while the user is doing a selection using the mouse, OOo stops updating the clipboard |
611 | // contents, so in practice it's like the user has selected only the part which was |
612 | // selected when Klipper asked first. |
613 | // Use XQueryPointer rather than QApplication::mouseButtons()/keyboardModifiers(), because |
614 | // Klipper needs the very current state. |
615 | Window root, child; |
616 | int root_x, root_y, win_x, win_y; |
617 | uint state; |
618 | XQueryPointer( QX11Info::display(), QX11Info::appRootWindow(), &root, &child, |
619 | &root_x, &root_y, &win_x, &win_y, &state ); |
620 | if( ( state & ( ShiftMask | Button1Mask )) == ShiftMask // #85198 |
621 | || ( state & Button1Mask ) == Button1Mask ) { // #80302 |
622 | m_pendingContentsCheck = true; |
623 | m_pendingCheckTimer.start( 100 ); |
624 | return true; |
625 | } |
626 | m_pendingContentsCheck = false; |
627 | if ( m_overflowCounter == 0 ) |
628 | m_overflowClearTimer.start( 1000 ); |
629 | if( ++m_overflowCounter > MAX_CLIPBOARD_CHANGES ) |
630 | return true; |
631 | #endif |
632 | return false; |
633 | } |
634 | |
635 | void Klipper::slotCheckPending() |
636 | { |
637 | if( !m_pendingContentsCheck ) |
638 | return; |
639 | m_pendingContentsCheck = false; // blockFetchingNewData() will be called again |
640 | updateTimestamp(); |
641 | newClipData( QClipboard::Selection ); // always selection |
642 | } |
643 | |
644 | void Klipper::checkClipData( bool selectionMode ) |
645 | { |
646 | if ( ignoreClipboardChanges() ) // internal to klipper, ignoring QSpinBox selections |
647 | { |
648 | // keep our old clipboard, thanks |
649 | // This won't quite work, but it's close enough for now. |
650 | // The trouble is that the top selection =! top clipboard |
651 | // but we don't track that yet. We will.... |
652 | const HistoryItem* top = history()->first(); |
653 | if ( top ) { |
654 | setClipboard( *top, selectionMode ? Selection : Clipboard); |
655 | } |
656 | return; |
657 | } |
658 | |
659 | // debug code |
660 | #ifdef NOISY_KLIPPER |
661 | kDebug() << "Checking clip data" ; |
662 | |
663 | kDebug() << "====== c h e c k C l i p D a t a ============================" |
664 | << kBacktrace() |
665 | << "====== c h e c k C l i p D a t a ============================" |
666 | << endl;; |
667 | |
668 | |
669 | if ( sender() ) { |
670 | kDebug() << "sender=" << sender()->objectName(); |
671 | } else { |
672 | kDebug() << "no sender" ; |
673 | } |
674 | |
675 | kDebug() << "\nselectionMode=" << selectionMode |
676 | << "\nowning (sel,cli)=(" << m_clip->ownsSelection() << "," << m_clip->ownsClipboard() << ")" |
677 | << "\ntext=" << m_clip->text( selectionMode ? QClipboard::Selection : QClipboard::Clipboard) << endl; |
678 | #endif |
679 | |
680 | const QMimeData* data = m_clip->mimeData( selectionMode ? QClipboard::Selection : QClipboard::Clipboard ); |
681 | if ( !data ) { |
682 | kWarning() << "No data in clipboard. This not not supposed to happen." ; |
683 | return; |
684 | } |
685 | |
686 | bool changed = true; // ### FIXME (only relevant under polling, might be better to simply remove polling and rely on XFixes) |
687 | bool clipEmpty = data->formats().isEmpty(); |
688 | if (clipEmpty) { |
689 | // Might be a timeout. Try again |
690 | clipEmpty = data->formats().isEmpty(); |
691 | #ifdef NOISY_KLIPPER |
692 | kDebug() << "was empty. Retried, now " << (clipEmpty?" still empty" :" no longer empty" ); |
693 | #endif |
694 | } |
695 | |
696 | if ( changed && clipEmpty && m_bNoNullClipboard ) { |
697 | const HistoryItem* top = history()->first(); |
698 | if ( top ) { |
699 | // keep old clipboard after someone set it to null |
700 | #ifdef NOISY_KLIPPER |
701 | kDebug() << "Resetting clipboard (Prevent empty clipboard)" ; |
702 | #endif |
703 | setClipboard( *top, selectionMode ? Selection : Clipboard ); |
704 | } |
705 | return; |
706 | } |
707 | |
708 | // this must be below the "bNoNullClipboard" handling code! |
709 | // XXX: I want a better handling of selection/clipboard in general. |
710 | // XXX: Order sensitive code. Must die. |
711 | if ( selectionMode && m_bIgnoreSelection ) |
712 | return; |
713 | |
714 | if( selectionMode && m_bSelectionTextOnly && !data->hasText()) |
715 | return; |
716 | |
717 | if( KUrl::List::canDecode( data ) ) |
718 | ; // ok |
719 | else if( data->hasText() ) |
720 | ; // ok |
721 | else if( data->hasImage() ) |
722 | { |
723 | if( m_bIgnoreImages ) |
724 | return; |
725 | } |
726 | else // unknown, ignore |
727 | return; |
728 | |
729 | HistoryItem* item = applyClipChanges( data ); |
730 | if (changed) { |
731 | #ifdef NOISY_KLIPPER |
732 | kDebug() << "Synchronize?" << m_bSynchronize; |
733 | #endif |
734 | if ( m_bSynchronize && item ) { |
735 | setClipboard( *item, selectionMode ? Clipboard : Selection ); |
736 | } |
737 | } |
738 | QString& lastURLGrabberText = selectionMode |
739 | ? m_lastURLGrabberTextSelection : m_lastURLGrabberTextClipboard; |
740 | if( m_bURLGrabber && item && data->hasText()) |
741 | { |
742 | m_myURLGrabber->checkNewData( item ); |
743 | |
744 | // Make sure URLGrabber doesn't repeat all the time if klipper reads the same |
745 | // text all the time (e.g. because XFixes is not available and the application |
746 | // has broken TIMESTAMP target). Using most recent history item may not always |
747 | // work. |
748 | if ( item->text() != lastURLGrabberText ) |
749 | { |
750 | lastURLGrabberText = item->text(); |
751 | } |
752 | } else { |
753 | lastURLGrabberText.clear(); |
754 | } |
755 | } |
756 | |
757 | void Klipper::setClipboard( const HistoryItem& item, int mode ) |
758 | { |
759 | Ignore lock( m_locklevel ); |
760 | |
761 | Q_ASSERT( ( mode & 1 ) == 0 ); // Warn if trying to pass a boolean as a mode. |
762 | |
763 | if ( mode & Selection ) { |
764 | #ifdef NOISY_KLIPPER |
765 | kDebug() << "Setting selection to <" << item.text() << ">" ; |
766 | #endif |
767 | m_clip->setMimeData( item.mimeData(), QClipboard::Selection ); |
768 | } |
769 | if ( mode & Clipboard ) { |
770 | #ifdef NOISY_KLIPPER |
771 | kDebug() << "Setting clipboard to <" << item.text() << ">" ; |
772 | #endif |
773 | m_clip->setMimeData( item.mimeData(), QClipboard::Clipboard ); |
774 | } |
775 | |
776 | } |
777 | |
778 | void Klipper::slotClearOverflow() |
779 | { |
780 | m_overflowClearTimer.stop(); |
781 | |
782 | if( m_overflowCounter > MAX_CLIPBOARD_CHANGES ) { |
783 | kDebug() << "App owning the clipboard/selection is lame" ; |
784 | // update to the latest data - this unfortunately may trigger the problem again |
785 | newClipData( QClipboard::Selection ); // Always the selection. |
786 | } |
787 | m_overflowCounter = 0; |
788 | } |
789 | |
790 | QStringList Klipper::() |
791 | { |
792 | QStringList ; |
793 | const HistoryItem* item = history()->first(); |
794 | if (item) { |
795 | do { |
796 | menu << item->text(); |
797 | item = history()->find(item->next_uuid()); |
798 | } while (item != history()->first()); |
799 | } |
800 | |
801 | return menu; |
802 | } |
803 | |
804 | QString Klipper::getClipboardHistoryItem(int i) |
805 | { |
806 | const HistoryItem* item = history()->first(); |
807 | if (item) { |
808 | do { |
809 | if (i-- == 0) { |
810 | return item->text(); |
811 | } |
812 | item = history()->find(item->next_uuid()); |
813 | } while (item != history()->first()); |
814 | } |
815 | return QString(); |
816 | |
817 | } |
818 | |
819 | // |
820 | // changing a spinbox in klipper's config-dialog causes the lineedit-contents |
821 | // of the spinbox to be selected and hence the clipboard changes. But we don't |
822 | // want all those items in klipper's history. See #41917 |
823 | // |
824 | bool Klipper::ignoreClipboardChanges() const |
825 | { |
826 | QWidget *focusWidget = qApp->focusWidget(); |
827 | if ( focusWidget ) |
828 | { |
829 | if ( focusWidget->inherits( "QSpinBox" ) || |
830 | (focusWidget->parentWidget() && |
831 | focusWidget->inherits("QLineEdit" ) && |
832 | focusWidget->parentWidget()->inherits("QSpinWidget" )) ) |
833 | { |
834 | return true; |
835 | } |
836 | } |
837 | |
838 | return false; |
839 | } |
840 | |
841 | #ifdef Q_WS_X11 |
842 | // QClipboard uses qt_x_time as the timestamp for selection operations. |
843 | // It is updated mainly from user actions, but Klipper polls the clipboard |
844 | // without any user action triggering it, so qt_x_time may be old, |
845 | // which could possibly lead to QClipboard reporting empty clipboard. |
846 | // Therefore, qt_x_time needs to be updated to current X server timestamp. |
847 | |
848 | // Call KApplication::updateUserTime() only from functions that are |
849 | // called from outside (DBUS), or from QTimer timeout ! |
850 | |
851 | static Time next_x_time; |
852 | static Bool update_x_time_predicate( Display*, XEvent* event, XPointer ) |
853 | { |
854 | if( next_x_time != CurrentTime ) |
855 | return False; |
856 | // from qapplication_x11.cpp |
857 | switch ( event->type ) { |
858 | case ButtonPress: |
859 | // fallthrough intended |
860 | case ButtonRelease: |
861 | next_x_time = event->xbutton.time; |
862 | break; |
863 | case MotionNotify: |
864 | next_x_time = event->xmotion.time; |
865 | break; |
866 | case KeyPress: |
867 | // fallthrough intended |
868 | case KeyRelease: |
869 | next_x_time = event->xkey.time; |
870 | break; |
871 | case PropertyNotify: |
872 | next_x_time = event->xproperty.time; |
873 | break; |
874 | case EnterNotify: |
875 | case LeaveNotify: |
876 | next_x_time = event->xcrossing.time; |
877 | break; |
878 | case SelectionClear: |
879 | next_x_time = event->xselectionclear.time; |
880 | break; |
881 | default: |
882 | break; |
883 | } |
884 | return False; |
885 | } |
886 | #endif |
887 | |
888 | void Klipper::updateTimestamp() |
889 | { |
890 | #ifdef Q_WS_X11 |
891 | static QWidget* w = 0; |
892 | if ( !w ) |
893 | w = new QWidget; |
894 | unsigned char data[ 1 ]; |
895 | XChangeProperty( QX11Info::display(), w->winId(), XA_ATOM, XA_ATOM, 8, PropModeAppend, data, 1 ); |
896 | next_x_time = CurrentTime; |
897 | XEvent dummy; |
898 | XCheckIfEvent( QX11Info::display(), &dummy, update_x_time_predicate, NULL ); |
899 | if( next_x_time == CurrentTime ) |
900 | { |
901 | XSync( QX11Info::display(), False ); |
902 | XCheckIfEvent( QX11Info::display(), &dummy, update_x_time_predicate, NULL ); |
903 | } |
904 | Q_ASSERT( next_x_time != CurrentTime ); |
905 | QX11Info::setAppTime( next_x_time ); |
906 | XEvent ev; // remove the PropertyNotify event from the events queue |
907 | XWindowEvent( QX11Info::display(), w->winId(), PropertyChangeMask, &ev ); |
908 | #endif |
909 | } |
910 | |
911 | static const char * const description = |
912 | I18N_NOOP("KDE cut & paste history utility" ); |
913 | |
914 | void Klipper::createAboutData() |
915 | { |
916 | m_about_data = new KAboutData("klipper" , 0, ki18n("Klipper" ), |
917 | klipper_version, ki18n(description), KAboutData::License_GPL, |
918 | ki18n("(c) 1998, Andrew Stanley-Jones\n" |
919 | "1998-2002, Carsten Pfeiffer\n" |
920 | "2001, Patrick Dubroy" )); |
921 | |
922 | m_about_data->addAuthor(ki18n("Carsten Pfeiffer" ), |
923 | ki18n("Author" ), |
924 | "pfeiffer@kde.org" ); |
925 | |
926 | m_about_data->addAuthor(ki18n("Andrew Stanley-Jones" ), |
927 | ki18n( "Original Author" ), |
928 | "asj@cban.com" ); |
929 | |
930 | m_about_data->addAuthor(ki18n("Patrick Dubroy" ), |
931 | ki18n("Contributor" ), |
932 | "patrickdu@corel.com" ); |
933 | |
934 | m_about_data->addAuthor( ki18n("Luboš Luňák" ), |
935 | ki18n("Bugfixes and optimizations" ), |
936 | "l.lunak@kde.org" ); |
937 | |
938 | m_about_data->addAuthor( ki18n("Esben Mose Hansen" ), |
939 | ki18n("Maintainer" ), |
940 | "kde@mosehansen.dk" ); |
941 | } |
942 | |
943 | void Klipper::destroyAboutData() |
944 | { |
945 | delete m_about_data; |
946 | m_about_data = NULL; |
947 | } |
948 | |
949 | KAboutData* Klipper::m_about_data; |
950 | |
951 | KAboutData* Klipper::aboutData() |
952 | { |
953 | return m_about_data; |
954 | } |
955 | |
956 | void Klipper::slotEditData() |
957 | { |
958 | const HistoryStringItem* item = dynamic_cast<const HistoryStringItem*>(m_history->first()); |
959 | |
960 | KDialog dlg; |
961 | dlg.setModal( true ); |
962 | dlg.setCaption( i18n("Edit Contents" ) ); |
963 | dlg.setButtons( KDialog::Ok | KDialog::Cancel ); |
964 | |
965 | KTextEdit *edit = new KTextEdit( &dlg ); |
966 | if (item) { |
967 | edit->setText( item->text() ); |
968 | } |
969 | edit->setFocus(); |
970 | edit->setMinimumSize( 300, 40 ); |
971 | dlg.setMainWidget( edit ); |
972 | dlg.adjustSize(); |
973 | |
974 | if ( dlg.exec() == KDialog::Accepted ) { |
975 | QString text = edit->toPlainText(); |
976 | if (item) { |
977 | m_history->remove( item ); |
978 | } |
979 | m_history->insert( new HistoryStringItem(text) ); |
980 | if (m_myURLGrabber) { |
981 | m_myURLGrabber->checkNewData( m_history->first() ); |
982 | } |
983 | } |
984 | |
985 | } |
986 | |
987 | #ifdef HAVE_PRISON |
988 | void Klipper::slotShowBarcode() |
989 | { |
990 | using namespace prison; |
991 | const HistoryStringItem* item = dynamic_cast<const HistoryStringItem*>(m_history->first()); |
992 | |
993 | KDialog dlg; |
994 | dlg.setModal( true ); |
995 | dlg.setCaption( i18n("Mobile Barcode" ) ); |
996 | dlg.setButtons( KDialog::Ok ); |
997 | |
998 | QWidget* mw = new QWidget(&dlg); |
999 | QHBoxLayout* layout = new QHBoxLayout(mw); |
1000 | |
1001 | BarcodeWidget* qrcode = new BarcodeWidget(new QRCodeBarcode()); |
1002 | BarcodeWidget* datamatrix = new BarcodeWidget(new DataMatrixBarcode()); |
1003 | |
1004 | if (item) { |
1005 | qrcode->setData( item->text() ); |
1006 | datamatrix->setData( item->text() ); |
1007 | } |
1008 | |
1009 | layout->addWidget(qrcode); |
1010 | layout->addWidget(datamatrix); |
1011 | |
1012 | mw->setFocus(); |
1013 | dlg.setMainWidget( mw ); |
1014 | dlg.adjustSize(); |
1015 | |
1016 | dlg.exec(); |
1017 | } |
1018 | #endif //HAVE_PRISON |
1019 | |
1020 | void Klipper::slotAskClearHistory() |
1021 | { |
1022 | int clearHist = KMessageBox::questionYesNo(0, |
1023 | i18n("Really delete entire clipboard history?" ), |
1024 | i18n("Delete clipboard history?" ), |
1025 | KStandardGuiItem::yes(), |
1026 | KStandardGuiItem::no(), |
1027 | QString::fromUtf8("really_clear_history" ), |
1028 | KMessageBox::Dangerous); |
1029 | if (clearHist == KMessageBox::Yes) { |
1030 | history()->slotClear(); |
1031 | slotClearClipboard(); |
1032 | saveHistory(); |
1033 | } |
1034 | |
1035 | } |
1036 | |
1037 | void Klipper::slotCycleNext() |
1038 | { |
1039 | //do cycle and show popup only if we have something in clipboard |
1040 | if (m_history->first()) { |
1041 | m_history->cycleNext(); |
1042 | emit passivePopup(i18n("Clipboard history" ), cycleText()); |
1043 | } |
1044 | } |
1045 | |
1046 | void Klipper::slotCyclePrev() |
1047 | { |
1048 | //do cycle and show popup only if we have something in clipboard |
1049 | if (m_history->first()) { |
1050 | m_history->cyclePrev(); |
1051 | emit passivePopup(i18n("Clipboard history" ), cycleText()); |
1052 | } |
1053 | } |
1054 | |
1055 | QString Klipper::cycleText() const |
1056 | { |
1057 | const int WIDTH_IN_PIXEL = 400; |
1058 | |
1059 | const HistoryItem* itemprev = m_history->prevInCycle(); |
1060 | const HistoryItem* item = m_history->first(); |
1061 | const HistoryItem* itemnext = m_history->nextInCycle(); |
1062 | |
1063 | QFontMetrics font_metrics(m_history->popup()->fontMetrics()); |
1064 | QString result("<table>" ); |
1065 | |
1066 | if (itemprev) { |
1067 | result += "<tr><td>" ; |
1068 | result += i18n("up" ); |
1069 | result += "</td><td>" ; |
1070 | result += font_metrics.elidedText(Qt::escape(itemprev->text().simplified()), Qt::ElideMiddle, WIDTH_IN_PIXEL); |
1071 | result += "</td></tr>" ; |
1072 | } |
1073 | |
1074 | result += "<tr><td>" ; |
1075 | result += i18n("current" ); |
1076 | result += "</td><td><b>" ; |
1077 | result += font_metrics.elidedText(Qt::escape(item->text().simplified()), Qt::ElideMiddle, WIDTH_IN_PIXEL); |
1078 | result += "</b></td></tr>" ; |
1079 | |
1080 | if (itemnext) { |
1081 | result += "<tr><td>" ; |
1082 | result += i18n("down" ); |
1083 | result += "</td><td>" ; |
1084 | result += font_metrics.elidedText(Qt::escape(itemnext->text().simplified()), Qt::ElideMiddle, WIDTH_IN_PIXEL); |
1085 | result += "</td></tr>" ; |
1086 | } |
1087 | |
1088 | result += "</table>" ; |
1089 | return result; |
1090 | } |
1091 | |
1092 | #include "klipper.moc" |
1093 | |