diff options
Diffstat (limited to 'klipper/toplevel.cpp')
-rw-r--r-- | klipper/toplevel.cpp | 1174 |
1 files changed, 1174 insertions, 0 deletions
diff --git a/klipper/toplevel.cpp b/klipper/toplevel.cpp new file mode 100644 index 000000000..cc5de3f63 --- /dev/null +++ b/klipper/toplevel.cpp @@ -0,0 +1,1174 @@ +// -*- Mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 8; -*- +/* This file is part of the KDE project + + Copyright (C) by Andrew Stanley-Jones + Copyright (C) 2000 by Carsten Pfeiffer <pfeiffer@kde.org> + Copyright (C) 2004 Esben Mose Hansen <kde@mosehansen.dk> + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; see the file COPYING. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include <qclipboard.h> +#include <qcursor.h> +#include <qdatetime.h> +#include <qfile.h> +#include <qintdict.h> +#include <qpainter.h> +#include <qtooltip.h> +#include <qmime.h> +#include <qdragobject.h> + +#include <kaboutdata.h> +#include <kaction.h> +#include <kapplication.h> +#include <kconfig.h> +#include <kglobal.h> +#include <kipc.h> +#include <kkeydialog.h> +#include <klocale.h> +#include <kmessagebox.h> +#include <ksavefile.h> +#include <kstandarddirs.h> +#include <ksimpleconfig.h> +#include <kstringhandler.h> +#include <ksystemtray.h> +#include <kurldrag.h> +#include <kwin.h> +#include <kdebug.h> +#include <kglobalsettings.h> +#include <dcopclient.h> +#include <kiconloader.h> +#include <khelpmenu.h> +#include <kstdguiitem.h> + +#include "configdialog.h" +#include "toplevel.h" +#include "urlgrabber.h" +#include "version.h" +#include "clipboardpoll.h" +#include "history.h" +#include "historyitem.h" +#include "historystringitem.h" +#include "klipperpopup.h" + +#include <zlib.h> + +#include <X11/Xlib.h> +#include <X11/Xatom.h> + +#include <kclipboard.h> + +namespace { + /** + * Use this when manipulating the clipboard + * from within clipboard-related signals. + * + * This avoids issues such as mouse-selections that immediately + * disappear. + * pattern: Resource Acqusition is Initialisation (RAII) + * + * (This is not threadsafe, so don't try to use such in threaded + * applications). + */ + struct Ignore { + Ignore(int& locklevel) : locklevelref(locklevel) { + locklevelref++; + } + ~Ignore() { + locklevelref--; + } + private: + int& locklevelref; + }; +} + +/** + * Helper class to save history upon session exit. + */ +class KlipperSessionManaged : public KSessionManaged +{ +public: + KlipperSessionManaged( KlipperWidget* k ) + : klipper( k ) + {} + + /** + * Save state upon session exit. + * + * Saving history on session save + */ + virtual bool commitData( QSessionManager& ) { + klipper->saveSession(); + return true; + } +private: + KlipperWidget* klipper; +}; + +extern bool qt_qclipboard_bailout_hack; +#if KDE_IS_VERSION( 3, 9, 0 ) +#error Check status of #80072 with Qt4. +#endif + +static void ensureGlobalSyncOff(KConfig* config); + +// config == kapp->config for process, otherwise applet +KlipperWidget::KlipperWidget( QWidget *parent, KConfig* config ) + : QWidget( parent ) + , DCOPObject( "klipper" ) + , m_overflowCounter( 0 ) + , locklevel( 0 ) + , m_config( config ) + , m_pendingContentsCheck( false ) + , session_managed( new KlipperSessionManaged( this )) +{ + qt_qclipboard_bailout_hack = true; + + // We don't use the clipboardsynchronizer anymore, and it confuses Klipper + ensureGlobalSyncOff(m_config); + + updateTimestamp(); // read initial X user time + setBackgroundMode( X11ParentRelative ); + clip = kapp->clipboard(); + + connect( &m_overflowClearTimer, SIGNAL( timeout()), SLOT( slotClearOverflow())); + m_overflowClearTimer.start( 1000 ); + connect( &m_pendingCheckTimer, SIGNAL( timeout()), SLOT( slotCheckPending())); + + m_history = new History( this, "main_history" ); + + // we need that collection, otherwise KToggleAction is not happy :} + QString defaultGroup( "default" ); + KActionCollection *collection = new KActionCollection( this, "my collection" ); + toggleURLGrabAction = new KToggleAction( collection, "toggleUrlGrabAction" ); + toggleURLGrabAction->setEnabled( true ); + toggleURLGrabAction->setGroup( defaultGroup ); + clearHistoryAction = new KAction( i18n("C&lear Clipboard History"), + "history_clear", + 0, + history(), + SLOT( slotClear() ), + collection, + "clearHistoryAction" ); + connect( clearHistoryAction, SIGNAL( activated() ), SLOT( slotClearClipboard() ) ); + clearHistoryAction->setGroup( defaultGroup ); + configureAction = new KAction( i18n("&Configure Klipper..."), + "configure", + 0, + this, + SLOT( slotConfigure() ), + collection, + "configureAction" ); + configureAction->setGroup( defaultGroup ); + quitAction = new KAction( i18n("&Quit"), + "exit", + 0, + this, + SLOT( slotQuit() ), + collection, + "quitAction" ); + quitAction->setGroup( "exit" ); + myURLGrabber = 0L; + KConfig *kc = m_config; + readConfiguration( kc ); + setURLGrabberEnabled( bURLGrabber ); + + hideTimer = new QTime(); + showTimer = new QTime(); + + readProperties(m_config); + connect(kapp, SIGNAL(settingsChanged(int)), SLOT(slotSettingsChanged(int))); + + poll = new ClipboardPoll( this ); + connect( poll, SIGNAL( clipboardChanged( bool ) ), + this, SLOT( newClipData( bool ) ) ); + + m_pixmap = KSystemTray::loadIcon( "klipper" ); + adjustSize(); + + globalKeys = new KGlobalAccel(this); + KGlobalAccel* keys = globalKeys; +#include "klipperbindings.cpp" + // the keys need to be read from kdeglobals, not kickerrc --ellis, 22/9/02 + globalKeys->readSettings(); + globalKeys->updateConnections(); + toggleURLGrabAction->setShortcut(globalKeys->shortcut("Enable/Disable Clipboard Actions")); + + connect( toggleURLGrabAction, SIGNAL( toggled( bool )), + this, SLOT( setURLGrabberEnabled( bool ))); + + KlipperPopup* popup = history()->popup(); + connect ( history(), SIGNAL( topChanged() ), SLOT( slotHistoryTopChanged() ) ); + connect( popup, SIGNAL( aboutToHide() ), SLOT( slotStartHideTimer() ) ); + connect( popup, SIGNAL( aboutToShow() ), SLOT( slotStartShowTimer() ) ); + + popup->plugAction( toggleURLGrabAction ); + popup->plugAction( clearHistoryAction ); + popup->plugAction( configureAction ); + if ( !isApplet() ) { + popup->plugAction( quitAction ); + } + + QToolTip::add( this, i18n("Klipper - clipboard tool") ); +} + +KlipperWidget::~KlipperWidget() +{ + delete session_managed; + delete showTimer; + delete hideTimer; + delete myURLGrabber; + if( m_config != kapp->config()) + delete m_config; + qt_qclipboard_bailout_hack = false; +} + +void KlipperWidget::adjustSize() +{ + resize( m_pixmap.size() ); +} + +// DCOP +QString KlipperWidget::getClipboardContents() +{ + return getClipboardHistoryItem(0); +} + +// DCOP - don't call from Klipper itself +void KlipperWidget::setClipboardContents(QString s) +{ + Ignore lock( locklevel ); + updateTimestamp(); + HistoryStringItem* item = new HistoryStringItem( s ); + setClipboard( *item, Clipboard | Selection); + history()->insert( item ); +} + +// DCOP - don't call from Klipper itself +void KlipperWidget::clearClipboardContents() +{ + updateTimestamp(); + slotClearClipboard(); +} + +// DCOP - don't call from Klipper itself +void KlipperWidget::clearClipboardHistory() +{ + updateTimestamp(); + slotClearClipboard(); + history()->slotClear(); + saveSession(); +} + + +void KlipperWidget::mousePressEvent(QMouseEvent *e) +{ + if ( e->button() != LeftButton && e->button() != RightButton ) + return; + + // if we only hid the menu less than a third of a second ago, + // it's probably because the user clicked on the klipper icon + // to hide it, and therefore won't want it shown again. + if ( hideTimer->elapsed() > 300 ) { + slotPopupMenu(); + } +} + +void KlipperWidget::paintEvent(QPaintEvent *) +{ + QPainter p(this); + int x = (width() - m_pixmap.width()) / 2; + int y = (height() - m_pixmap.height()) / 2; + if ( x < 0 ) x = 0; + if ( y < 0 ) y = 0; + p.drawPixmap(x , y, m_pixmap); + p.end(); +} + +void KlipperWidget::slotStartHideTimer() +{ + hideTimer->start(); +} + +void KlipperWidget::slotStartShowTimer() +{ + showTimer->start(); +} + +void KlipperWidget::showPopupMenu( QPopupMenu *menu ) +{ + Q_ASSERT( menu != 0L ); + + QSize size = menu->sizeHint(); // geometry is not valid until it's shown + if (bPopupAtMouse) { + QPoint g = QCursor::pos(); + if ( size.height() < g.y() ) + menu->popup(QPoint( g.x(), g.y() - size.height())); + else + menu->popup(QPoint(g.x(), g.y())); + } else { + KWin::WindowInfo i = KWin::windowInfo( winId(), NET::WMGeometry ); + QRect g = i.geometry(); + QRect screen = KGlobalSettings::desktopGeometry(g.center()); + + if ( g.x()-screen.x() > screen.width()/2 && + g.y()-screen.y() + size.height() > screen.height() ) + menu->popup(QPoint( g.x(), g.y() - size.height())); + else + menu->popup(QPoint( g.x() + width(), g.y() + height())); + + // menu->exec(mapToGlobal(QPoint( width()/2, height()/2 ))); + } +} + +bool KlipperWidget::loadHistory() { + static const char* const failed_load_warning = + "Failed to load history resource. Clipboard history cannot be read."; + // don't use "appdata", klipper is also a kicker applet + QString history_file_name = ::locateLocal( "data", "klipper/history2.lst" ); + QFile history_file( history_file_name ); + bool oldfile = false; + if ( !history_file.exists() ) { // backwards compatibility + oldfile = true; + history_file_name = ::locateLocal( "data", "klipper/history.lst" ); + history_file.setName( history_file_name ); + if ( !history_file.exists() ) { + history_file_name = ::locateLocal( "data", "kicker/history.lst" ); + history_file.setName( history_file_name ); + if ( !history_file.exists() ) { + return false; + } + } + } + if ( !history_file.open( IO_ReadOnly ) ) { + kdWarning() << failed_load_warning << ": " << history_file.errorString() << endl; + return false; + } + QDataStream file_stream( &history_file ); + if( file_stream.atEnd()) { + kdWarning() << failed_load_warning << endl; + return false; + } + QDataStream* history_stream = &file_stream; + QByteArray data; + if( !oldfile ) { + Q_UINT32 crc; + file_stream >> crc >> data; + if( crc32( 0, reinterpret_cast<unsigned char *>( data.data() ), data.size() ) != crc ) { + kdWarning() << failed_load_warning << ": " << history_file.errorString() << endl; + return false; + } + + history_stream = new QDataStream( data, IO_ReadOnly ); + } + char* version; + *history_stream >> version; + delete[] version; + + // The list needs to be reversed, as it is saved + // youngest-first to keep the most important clipboard + // items at the top, but the history is created oldest + // first. + QPtrList<HistoryItem> reverseList; + for ( HistoryItem* item = HistoryItem::create( *history_stream ); + item; + item = HistoryItem::create( *history_stream ) ) + { + reverseList.prepend( item ); + } + + for ( HistoryItem* item = reverseList.first(); + item; + item = reverseList.next() ) + { + history()->forceInsert( item ); + } + + if ( !history()->empty() ) { + m_lastSelection = -1; + m_lastClipboard = -1; + setClipboard( *history()->first(), Clipboard | Selection ); + } + + if( history_stream != &file_stream ) + delete history_stream; + + return true; +} + +void KlipperWidget::saveHistory() { + static const char* const failed_save_warning = + "Failed to save history. Clipboard history cannot be saved."; + // don't use "appdata", klipper is also a kicker applet + QString history_file_name( ::locateLocal( "data", "klipper/history2.lst" ) ); + if ( history_file_name.isNull() || history_file_name.isEmpty() ) { + kdWarning() << failed_save_warning << endl; + return; + } + KSaveFile history_file( history_file_name ); + if ( history_file.status() != 0 ) { + kdWarning() << failed_save_warning << endl; + return; + } + QByteArray data; + QDataStream history_stream( data, IO_WriteOnly ); + history_stream << klipper_version; // const char* + for ( const HistoryItem* item = history()->first(); item; item = history()->next() ) { + history_stream << item; + } + Q_UINT32 crc = crc32( 0, reinterpret_cast<unsigned char *>( data.data() ), data.size() ); + *history_file.dataStream() << crc << data; +} + +void KlipperWidget::readProperties(KConfig *kc) +{ + QStringList dataList; + + history()->slotClear(); + + if (bKeepContents) { // load old clipboard if configured + if ( !loadHistory() ) { + // Try to load from the old config file. + // Remove this at some point. + KConfigGroupSaver groupSaver(kc, "General"); + dataList = kc->readListEntry("ClipboardData"); + + for (QStringList::ConstIterator it = dataList.end(); + it != dataList.begin(); + ) + { + history()->forceInsert( new HistoryStringItem( *( --it ) ) ); + } + + if ( !dataList.isEmpty() ) + { + m_lastSelection = -1; + m_lastClipboard = -1; + setClipboard( *history()->first(), Clipboard | Selection ); + } + } + } + +} + + +void KlipperWidget::readConfiguration( KConfig *kc ) +{ + kc->setGroup("General"); + bPopupAtMouse = kc->readBoolEntry("PopupAtMousePosition", false); + bKeepContents = kc->readBoolEntry("KeepClipboardContents", true); + bURLGrabber = kc->readBoolEntry("URLGrabberEnabled", false); + bReplayActionInHistory = kc->readBoolEntry("ReplayActionInHistory", false); + bNoNullClipboard = kc->readBoolEntry("NoEmptyClipboard", true); + bUseGUIRegExpEditor = kc->readBoolEntry("UseGUIRegExpEditor", true ); + history()->max_size( kc->readNumEntry("MaxClipItems", 7) ); + bIgnoreSelection = kc->readBoolEntry("IgnoreSelection", false); + bSynchronize = kc->readBoolEntry("Synchronize", false); + bSelectionTextOnly = kc->readBoolEntry("SelectionTextOnly",true); + bIgnoreImages = kc->readBoolEntry("IgnoreImages",true); +} + +void KlipperWidget::writeConfiguration( KConfig *kc ) +{ + kc->setGroup("General"); + kc->writeEntry("PopupAtMousePosition", bPopupAtMouse); + kc->writeEntry("KeepClipboardContents", bKeepContents); + kc->writeEntry("ReplayActionInHistory", bReplayActionInHistory); + kc->writeEntry("NoEmptyClipboard", bNoNullClipboard); + kc->writeEntry("UseGUIRegExpEditor", bUseGUIRegExpEditor); + kc->writeEntry("MaxClipItems", history()->max_size() ); + kc->writeEntry("IgnoreSelection", bIgnoreSelection); + kc->writeEntry("Synchronize", bSynchronize ); + kc->writeEntry("SelectionTextOnly", bSelectionTextOnly); + kc->writeEntry("TrackImages", bIgnoreImages); + kc->writeEntry("Version", klipper_version ); + + if ( myURLGrabber ) + myURLGrabber->writeConfiguration( kc ); + + kc->sync(); +} + +// save session on shutdown. Don't simply use the c'tor, as that may not be called. +void KlipperWidget::saveSession() +{ + if ( bKeepContents ) { // save the clipboard eventually + saveHistory(); + } +} + +void KlipperWidget::slotSettingsChanged( int category ) +{ + if ( category == (int) KApplication::SETTINGS_SHORTCUTS ) { + globalKeys->readSettings(); + globalKeys->updateConnections(); + toggleURLGrabAction->setShortcut(globalKeys->shortcut("Enable/Disable Clipboard Actions")); + } +} + +void KlipperWidget::disableURLGrabber() +{ + KMessageBox::information( 0L, + i18n( "You can enable URL actions later by right-clicking on the " + "Klipper icon and selecting 'Enable Actions'" ) ); + + setURLGrabberEnabled( false ); +} + +void KlipperWidget::slotConfigure() +{ + bool haveURLGrabber = bURLGrabber; + if ( !myURLGrabber ) { // temporary, for the config-dialog + setURLGrabberEnabled( true ); + readConfiguration( m_config ); + } + + ConfigDialog *dlg = new ConfigDialog( myURLGrabber->actionList(), + globalKeys, isApplet() ); + dlg->setKeepContents( bKeepContents ); + dlg->setPopupAtMousePos( bPopupAtMouse ); + dlg->setStripWhiteSpace( myURLGrabber->stripWhiteSpace() ); + dlg->setReplayActionInHistory( bReplayActionInHistory ); + dlg->setNoNullClipboard( bNoNullClipboard ); + dlg->setUseGUIRegExpEditor( bUseGUIRegExpEditor ); + dlg->setPopupTimeout( myURLGrabber->popupTimeout() ); + dlg->setMaxItems( history()->max_size() ); + dlg->setIgnoreSelection( bIgnoreSelection ); + dlg->setSynchronize( bSynchronize ); + dlg->setNoActionsFor( myURLGrabber->avoidWindows() ); + + if ( dlg->exec() == QDialog::Accepted ) { + bKeepContents = dlg->keepContents(); + bPopupAtMouse = dlg->popupAtMousePos(); + bReplayActionInHistory = dlg->replayActionInHistory(); + bNoNullClipboard = dlg->noNullClipboard(); + bIgnoreSelection = dlg->ignoreSelection(); + bSynchronize = dlg->synchronize(); + bUseGUIRegExpEditor = dlg->useGUIRegExpEditor(); + dlg->commitShortcuts(); + // the keys need to be written to kdeglobals, not kickerrc --ellis, 22/9/02 + globalKeys->writeSettings(0, true); + globalKeys->updateConnections(); + toggleURLGrabAction->setShortcut(globalKeys->shortcut("Enable/Disable Clipboard Actions")); + + myURLGrabber->setActionList( dlg->actionList() ); + myURLGrabber->setPopupTimeout( dlg->popupTimeout() ); + myURLGrabber->setStripWhiteSpace( dlg->stripWhiteSpace() ); + myURLGrabber->setAvoidWindows( dlg->noActionsFor() ); + + history()->max_size( dlg->maxItems() ); + + writeConfiguration( m_config ); + + } + setURLGrabberEnabled( haveURLGrabber ); + + delete dlg; +} + +void KlipperWidget::slotQuit() +{ + // If the menu was just opened, likely the user + // selected quit by accident while attempting to + // click the Klipper icon. + if ( showTimer->elapsed() < 300 ) { + return; + } + + saveSession(); + int autoStart = KMessageBox::questionYesNoCancel( 0L, i18n("Should Klipper start automatically\nwhen you login?"), i18n("Automatically Start Klipper?"), i18n("Start"), i18n("Do Not Start") ); + + KConfig *config = KGlobal::config(); + config->setGroup("General"); + if ( autoStart == KMessageBox::Yes ) { + config->writeEntry("AutoStart", true); + } else if ( autoStart == KMessageBox::No) { + config->writeEntry("AutoStart", false); + } else // cancel chosen don't quit + return; + config->sync(); + + kapp->quit(); + +} + +void KlipperWidget::slotPopupMenu() { + KlipperPopup* popup = history()->popup(); + popup->ensureClean(); + showPopupMenu( popup ); +} + + +void KlipperWidget::slotRepeatAction() +{ + if ( !myURLGrabber ) { + myURLGrabber = new URLGrabber( m_config ); + connect( myURLGrabber, SIGNAL( sigPopup( QPopupMenu * )), + SLOT( showPopupMenu( QPopupMenu * )) ); + connect( myURLGrabber, SIGNAL( sigDisablePopup() ), + this, SLOT( disableURLGrabber() ) ); + } + + const HistoryStringItem* top = dynamic_cast<const HistoryStringItem*>( history()->first() ); + if ( top ) { + myURLGrabber->invokeAction( top->text() ); + } +} + +void KlipperWidget::setURLGrabberEnabled( bool enable ) +{ + if (enable != bURLGrabber) { + bURLGrabber = enable; + KConfig *kc = m_config; + kc->setGroup("General"); + kc->writeEntry("URLGrabberEnabled", bURLGrabber); + m_lastURLGrabberTextSelection = QString(); + m_lastURLGrabberTextClipboard = QString(); + } + + toggleURLGrabAction->setChecked( enable ); + + if ( !bURLGrabber ) { + delete myURLGrabber; + myURLGrabber = 0L; + toggleURLGrabAction->setText(i18n("Enable &Actions")); + } + + else { + toggleURLGrabAction->setText(i18n("&Actions Enabled")); + if ( !myURLGrabber ) { + myURLGrabber = new URLGrabber( m_config ); + connect( myURLGrabber, SIGNAL( sigPopup( QPopupMenu * )), + SLOT( showPopupMenu( QPopupMenu * )) ); + connect( myURLGrabber, SIGNAL( sigDisablePopup() ), + this, SLOT( disableURLGrabber() ) ); + } + } +} + +void KlipperWidget::toggleURLGrabber() +{ + setURLGrabberEnabled( !bURLGrabber ); +} + +void KlipperWidget::slotHistoryTopChanged() { + if ( locklevel ) { + return; + } + + const HistoryItem* topitem = history()->first(); + if ( topitem ) { + setClipboard( *topitem, Clipboard | Selection ); + } + if ( bReplayActionInHistory && bURLGrabber ) { + slotRepeatAction(); + } + +} + +void KlipperWidget::slotClearClipboard() +{ + Ignore lock( locklevel ); + + clip->clear(QClipboard::Selection); + clip->clear(QClipboard::Clipboard); + + +} + + +//XXX: Should die, and the DCOP signal handled sensible. +QString KlipperWidget::clipboardContents( bool * /*isSelection*/ ) +{ + kdWarning() << "Obsolete function called. Please fix" << endl; + +#if 0 + bool selection = true; + QMimeSource* data = clip->data(QClipboard::Selection); + + if ( data->serialNumber() == m_lastSelection ) + { + QString clipContents = clip->text(QClipboard::Clipboard); + if ( clipContents != m_lastClipboard ) + { + contents = clipContents; + selection = false; + } + else + selection = true; + } + + if ( isSelection ) + *isSelection = selection; + +#endif + + return 0; +} + +void KlipperWidget::applyClipChanges( const QMimeSource& clipData ) +{ + if ( locklevel ) + return; + Ignore lock( locklevel ); + history()->insert( HistoryItem::create( clipData ) ); + +} + +void KlipperWidget::newClipData( bool selectionMode ) +{ + if ( locklevel ) { + return; + } + + if( blockFetchingNewData()) + return; + + checkClipData( selectionMode ); + +} + +void KlipperWidget::clipboardSignalArrived( bool selectionMode ) +{ + if ( locklevel ) { + return; + } + if( blockFetchingNewData()) + return; + + updateTimestamp(); + checkClipData( selectionMode ); +} + +// Protection against too many clipboard data changes. Lyx responds to clipboard data +// requests with setting new clipboard data, so if Lyx takes over clipboard, +// Klipper notices, requests this data, this triggers "new" clipboard contents +// from Lyx, so Klipper notices again, requests this data, ... you get the idea. +const int MAX_CLIPBOARD_CHANGES = 10; // max changes per second + +bool KlipperWidget::blockFetchingNewData() +{ +// Hacks for #85198 and #80302. +// #85198 - block fetching new clipboard contents if Shift is pressed and mouse is not, +// this may mean the user is doing selection using the keyboard, in which case +// it's possible the app sets new clipboard contents after every change - Klipper's +// history would list them all. +// #80302 - OOo (v1.1.3 at least) has a bug that if Klipper requests its clipboard contents +// while the user is doing a selection using the mouse, OOo stops updating the clipboard +// contents, so in practice it's like the user has selected only the part which was +// selected when Klipper asked first. + ButtonState buttonstate = kapp->keyboardMouseState(); + if( ( buttonstate & ( ShiftButton | LeftButton )) == ShiftButton // #85198 + || ( buttonstate & LeftButton ) == LeftButton ) { // #80302 + m_pendingContentsCheck = true; + m_pendingCheckTimer.start( 100, true ); + return true; + } + m_pendingContentsCheck = false; + if( ++m_overflowCounter > MAX_CLIPBOARD_CHANGES ) + return true; + return false; +} + +void KlipperWidget::slotCheckPending() +{ + if( !m_pendingContentsCheck ) + return; + m_pendingContentsCheck = false; // blockFetchingNewData() will be called again + updateTimestamp(); + newClipData( true ); // always selection +} + +void KlipperWidget::checkClipData( bool selectionMode ) +{ + if ( ignoreClipboardChanges() ) // internal to klipper, ignoring QSpinBox selections + { + // keep our old clipboard, thanks + // This won't quite work, but it's close enough for now. + // The trouble is that the top selection =! top clipboard + // but we don't track that yet. We will.... + const HistoryItem* top = history()->first(); + if ( top ) { + setClipboard( *top, selectionMode ? Selection : Clipboard); + } + return; + } + +// debug code + +// debug code +#ifdef NOISY_KLIPPER + kdDebug() << "Checking clip data" << endl; +#endif +#if 0 + kdDebug() << "====== c h e c k C l i p D a t a ============================" + << kdBacktrace() + << "====== c h e c k C l i p D a t a ============================" + << endl;; +#endif +#if 0 + if ( sender() ) { + kdDebug() << "sender=" << sender()->name() << endl; + } else { + kdDebug() << "no sender" << endl; + } +#endif +#if 0 + kdDebug() << "\nselectionMode=" << selectionMode + << "\nserialNo=" << clip->data()->serialNumber() << " (sel,cli)=(" << m_lastSelection << "," << m_lastClipboard << ")" + << "\nowning (sel,cli)=(" << clip->ownsSelection() << "," << clip->ownsClipboard() << ")" + << "\ntext=" << clip->text( selectionMode ? QClipboard::Selection : QClipboard::Clipboard) << endl; + +#endif +#if 0 + const char *format; + int i = 0; + while ( (format = clip->data()->format( i++ )) ) + { + qDebug( " format: %s", format); + } +#endif + QMimeSource* data = clip->data( selectionMode ? QClipboard::Selection : QClipboard::Clipboard ); + if ( !data ) { + kdWarning("No data in clipboard. This not not supposed to happen." ); + return; + } + + int lastSerialNo = selectionMode ? m_lastSelection : m_lastClipboard; + bool changed = data->serialNumber() != lastSerialNo; + bool clipEmpty = ( data->format() == 0L ); + + if ( changed && clipEmpty && bNoNullClipboard ) { + const HistoryItem* top = history()->first(); + if ( top ) { + // keep old clipboard after someone set it to null +#ifdef NOISY_KLIPPER + kdDebug() << "Resetting clipboard (Prevent empty clipboard)" << endl; +#endif + setClipboard( *top, selectionMode ? Selection : Clipboard ); + } + return; + } + + // this must be below the "bNoNullClipboard" handling code! + // XXX: I want a better handling of selection/clipboard in general. + // XXX: Order sensitive code. Must die. + if ( selectionMode && bIgnoreSelection ) + return; + + if( selectionMode && bSelectionTextOnly && !QTextDrag::canDecode( data )) + return; + + if( KURLDrag::canDecode( data )) + ; // ok + else if( QTextDrag::canDecode( data )) + ; // ok + else if( QImageDrag::canDecode( data )) + { +// Limit mimetypes that are tracked by Klipper (this is basically a workaround +// for #109032). Can't add UI in 3.5 because of string freeze, and I'm not sure +// if this actually needs to be more configurable than only text vs all klipper knows. + if( bIgnoreImages ) + return; + else { + // ok + } + } + else // unknown, ignore + return; + + // store old contents: + if ( selectionMode ) + m_lastSelection = data->serialNumber(); + else + m_lastClipboard = data->serialNumber(); + + QString& lastURLGrabberText = selectionMode + ? m_lastURLGrabberTextSelection : m_lastURLGrabberTextClipboard; + if( QTextDrag::canDecode( data )) + { + if ( bURLGrabber && myURLGrabber ) + { + QString text; + QTextDrag::decode( data, text ); + // Make sure URLGrabber doesn't repeat all the time if klipper reads the same + // text all the time (e.g. because XFixes is not available and the application + // has broken TIMESTAMP target). Using most recent history item may not always + // work. + if ( text != lastURLGrabberText ) + { + lastURLGrabberText = text; + if ( myURLGrabber->checkNewData( text ) ) + { + return; // don't add into the history + } + } + } + else + lastURLGrabberText = QString(); + } + else + lastURLGrabberText = QString(); + + if (changed) { + applyClipChanges( *data ); +#ifdef NOISY_KLIPPER + kdDebug() << "Synchronize?" << ( bSynchronize ? "yes" : "no" ) << endl; +#endif + if ( bSynchronize ) { + const HistoryItem* topItem = history()->first(); + if ( topItem ) { + setClipboard( *topItem, selectionMode ? Clipboard : Selection ); + } + } + } + +} + +void KlipperWidget::setClipboard( const HistoryItem& item, int mode ) +{ + Ignore lock( locklevel ); + + Q_ASSERT( ( mode & 1 ) == 0 ); // Warn if trying to pass a boolean as a mode. + + if ( mode & Selection ) { +#ifdef NOSIY_KLIPPER + kdDebug() << "Setting selection to <" << item.text() << ">" << endl; +#endif + clip->setData( item.mimeSource(), QClipboard::Selection ); + m_lastSelection = clip->data()->serialNumber(); + } + if ( mode & Clipboard ) { +#ifdef NOSIY_KLIPPER + kdDebug() << "Setting clipboard to <" << item.text() << ">" << endl; +#endif + clip->setData( item.mimeSource(), QClipboard::Clipboard ); + m_lastClipboard = clip->data()->serialNumber(); + } + +} + +void KlipperWidget::slotClearOverflow() +{ + if( m_overflowCounter > MAX_CLIPBOARD_CHANGES ) { + kdDebug() << "App owning the clipboard/selection is lame" << endl; + // update to the latest data - this unfortunately may trigger the problem again + newClipData( true ); // Always the selection. + } + m_overflowCounter = 0; +} + +QStringList KlipperWidget::getClipboardHistoryMenu() +{ + QStringList menu; + for ( const HistoryItem* item = history()->first(); item; item = history()->next() ) { + menu << item->text(); + } + return menu; +} + +QString KlipperWidget::getClipboardHistoryItem(int i) +{ + for ( const HistoryItem* item = history()->first(); item; item = history()->next() , i-- ) { + if ( i == 0 ) { + return item->text(); + } + } + return QString::null; + +} + +// +// changing a spinbox in klipper's config-dialog causes the lineedit-contents +// of the spinbox to be selected and hence the clipboard changes. But we don't +// want all those items in klipper's history. See #41917 +// +bool KlipperWidget::ignoreClipboardChanges() const +{ + QWidget *focusWidget = qApp->focusWidget(); + if ( focusWidget ) + { + if ( focusWidget->inherits( "QSpinBox" ) || + (focusWidget->parentWidget() && + focusWidget->inherits("QLineEdit") && + focusWidget->parentWidget()->inherits("QSpinWidget")) ) + { + return true; + } + } + + return false; +} + +// QClipboard uses qt_x_time as the timestamp for selection operations. +// It is updated mainly from user actions, but Klipper polls the clipboard +// without any user action triggering it, so qt_x_time may be old, +// which could possibly lead to QClipboard reporting empty clipboard. +// Therefore, qt_x_time needs to be updated to current X server timestamp. + +// Call KApplication::updateUserTime() only from functions that are +// called from outside (DCOP), or from QTimer timeout ! + +extern Time qt_x_time; +extern Time qt_x_user_time; + +static Time next_x_time; +static Bool update_x_time_predicate( Display*, XEvent* event, XPointer ) +{ + if( next_x_time != CurrentTime ) + return False; + // from qapplication_x11.cpp + switch ( event->type ) { + case ButtonPress: + // fallthrough intended + case ButtonRelease: + next_x_time = event->xbutton.time; + break; + case MotionNotify: + next_x_time = event->xmotion.time; + break; + case KeyPress: + // fallthrough intended + case KeyRelease: + next_x_time = event->xkey.time; + break; + case PropertyNotify: + next_x_time = event->xproperty.time; + break; + case EnterNotify: + case LeaveNotify: + next_x_time = event->xcrossing.time; + break; + case SelectionClear: + next_x_time = event->xselectionclear.time; + break; + default: + break; + } + return False; +} + +void KlipperWidget::updateTimestamp() +{ // Qt3.3.0 and 3.3.1 use qt_x_user_time for clipboard operations + Time& time = ( strcmp( qVersion(), "3.3.1" ) == 0 + || strcmp( qVersion(), "3.3.0" ) == 0 ) + ? qt_x_user_time : qt_x_time; + static QWidget* w = 0; + if ( !w ) + w = new QWidget; + unsigned char data[ 1 ]; + XChangeProperty( qt_xdisplay(), w->winId(), XA_ATOM, XA_ATOM, 8, PropModeAppend, data, 1 ); + next_x_time = CurrentTime; + XEvent dummy; + XCheckIfEvent( qt_xdisplay(), &dummy, update_x_time_predicate, NULL ); + if( next_x_time == CurrentTime ) + { + XSync( qt_xdisplay(), False ); + XCheckIfEvent( qt_xdisplay(), &dummy, update_x_time_predicate, NULL ); + } + Q_ASSERT( next_x_time != CurrentTime ); + time = next_x_time; + XEvent ev; // remove the PropertyNotify event from the events queue + XWindowEvent( qt_xdisplay(), w->winId(), PropertyChangeMask, &ev ); +} + +static const char * const description = + I18N_NOOP("KDE cut & paste history utility"); + +void KlipperWidget::createAboutData() +{ + about_data = new KAboutData("klipper", I18N_NOOP("Klipper"), + klipper_version, description, KAboutData::License_GPL, + "(c) 1998, Andrew Stanley-Jones\n" + "1998-2002, Carsten Pfeiffer\n" + "2001, Patrick Dubroy"); + + about_data->addAuthor("Carsten Pfeiffer", + I18N_NOOP("Author"), + "pfeiffer@kde.org"); + + about_data->addAuthor("Andrew Stanley-Jones", + I18N_NOOP( "Original Author" ), + "asj@cban.com"); + + about_data->addAuthor("Patrick Dubroy", + I18N_NOOP("Contributor"), + "patrickdu@corel.com"); + + about_data->addAuthor( "Luboš Luňák", + I18N_NOOP("Bugfixes and optimizations"), + "l.lunak@kde.org"); + + about_data->addAuthor( "Esben Mose Hansen", + I18N_NOOP("Maintainer"), + "kde@mosehansen.dk"); +} + +void KlipperWidget::destroyAboutData() +{ + delete about_data; + about_data = NULL; +} + +KAboutData* KlipperWidget::about_data; + +KAboutData* KlipperWidget::aboutData() +{ + return about_data; +} + +Klipper::Klipper( QWidget* parent ) + : KlipperWidget( parent, kapp->config()) +{ +} + +// this sucks ... KUniqueApplication registers itself as 'klipper' +// for the unique-app detection calls (and it shouldn't use that name IMHO) +// but in Klipper it's not KUniqueApplication class who handles +// the DCOP calls, but an instance of class Klipper, registered as 'klipper' +// this below avoids a warning when KUniqueApplication wouldn't otherwise +// find newInstance() (which doesn't do anything in Klipper anyway) +int Klipper::newInstance() +{ + kapp->dcopClient()->setPriorityCall(false); // Allow other dcop calls + return 0; +} + +// this is used for quiting klipper process, if klipper is being started as an applet +// (AKA ugly hack) +void Klipper::quitProcess() +{ + kapp->dcopClient()->detach(); + kapp->quit(); +} + +static void ensureGlobalSyncOff(KConfig* config) { + config->setGroup("General"); + if ( config->readBoolEntry( "SynchronizeClipboardAndSelection" ) ) { + kdDebug() << "Shutting off global synchronization" << endl; + config->writeEntry("SynchronizeClipboardAndSelection", + false, + true, + true ); + config->sync(); + KClipboardSynchronizer::setSynchronizing( false ); + KClipboardSynchronizer::setReverseSynchronizing( false ); + KIPC::sendMessageAll( KIPC::ClipboardConfigChanged, 0 ); + } + +} + +#include "toplevel.moc" |