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
67namespace {
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 */
94class KlipperSessionManager : public KSessionManager
95{
96public:
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 }
112private:
113 Klipper* klipper;
114};
115
116// config == KGlobal::config for process, otherwise applet
117Klipper::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* popup = 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
240Klipper::~Klipper()
241{
242 delete m_sessionManager;
243 delete m_myURLGrabber;
244}
245
246// DBUS
247QString Klipper::getClipboardContents()
248{
249 return getClipboardHistoryItem(0);
250}
251
252void Klipper::showKlipperPopupMenu() {
253 slotPopupMenu();
254}
255void Klipper::showKlipperManuallyInvokeActionMenu() {
256 slotRepeatAction();
257}
258
259
260// DBUS - don't call from Klipper itself
261void 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
273void Klipper::clearClipboardContents()
274{
275 updateTimestamp();
276 slotClearClipboard();
277}
278
279// DBUS - don't call from Klipper itself
280void Klipper::clearClipboardHistory()
281{
282 updateTimestamp();
283 slotClearClipboard();
284 history()->slotClear();
285 saveSession();
286}
287
288// DBUS - don't call from Klipper itself
289void Klipper::saveClipboardHistory()
290{
291 if ( m_bKeepContents ) { // save the clipboard eventually
292 saveHistory();
293 }
294}
295
296void Klipper::slotStartShowTimer()
297{
298 m_showTimer.start();
299}
300
301void 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
343void 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
352void Klipper::showPopupMenu( QMenu* menu )
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
367bool 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
427void 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.
461void Klipper::saveSession()
462{
463 if ( m_bKeepContents ) { // save the clipboard eventually
464 saveHistory();
465 }
466 saveSettings();
467}
468
469void 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
478void 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
490void 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
517void Klipper::slotPopupMenu() {
518 KlipperPopup* popup = history()->popup();
519 popup->ensureClean();
520 popup->slotSetTopActive();
521 showPopupMenu( popup );
522}
523
524
525void Klipper::slotRepeatAction()
526{
527 const HistoryStringItem* top = dynamic_cast<const HistoryStringItem*>( history()->first() );
528 if ( top ) {
529 m_myURLGrabber->invokeAction( top );
530 }
531}
532
533void 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
548void 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
562void Klipper::slotClearClipboard()
563{
564 Ignore lock( m_locklevel );
565
566 m_clip->clear(QClipboard::Selection);
567 m_clip->clear(QClipboard::Clipboard);
568}
569
570HistoryItem* 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
582void 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.
599const int MAX_CLIPBOARD_CHANGES = 10; // max changes per second
600
601bool 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
635void 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
644void 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
757void 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
778void 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
790QStringList Klipper::getClipboardHistoryMenu()
791{
792 QStringList menu;
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
804QString 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//
824bool 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
851static Time next_x_time;
852static 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
888void 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
911static const char * const description =
912 I18N_NOOP("KDE cut & paste history utility");
913
914void 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
943void Klipper::destroyAboutData()
944{
945 delete m_about_data;
946 m_about_data = NULL;
947}
948
949KAboutData* Klipper::m_about_data;
950
951KAboutData* Klipper::aboutData()
952{
953 return m_about_data;
954}
955
956void 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
988void 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
1020void 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
1037void 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
1046void 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
1055QString 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