summaryrefslogtreecommitdiffstats
path: root/kcontrol/input/xcursor
diff options
context:
space:
mode:
authortoma <toma@283d02a7-25f6-0310-bc7c-ecb5cbfe19da>2009-11-25 17:56:58 +0000
committertoma <toma@283d02a7-25f6-0310-bc7c-ecb5cbfe19da>2009-11-25 17:56:58 +0000
commit4aed2c8219774f5d797760606b8489a92ddc5163 (patch)
tree3f8c130f7d269626bf6a9447407ef6c35954426a /kcontrol/input/xcursor
downloadtdebase-4aed2c8219774f5d797760606b8489a92ddc5163.tar.gz
tdebase-4aed2c8219774f5d797760606b8489a92ddc5163.zip
Copy the KDE 3.5 branch to branches/trinity for new KDE 3.5 features.
BUG:215923 git-svn-id: svn://anonsvn.kde.org/home/kde/branches/trinity/kdebase@1054174 283d02a7-25f6-0310-bc7c-ecb5cbfe19da
Diffstat (limited to 'kcontrol/input/xcursor')
-rw-r--r--kcontrol/input/xcursor/Makefile.am7
-rw-r--r--kcontrol/input/xcursor/previewwidget.cpp353
-rw-r--r--kcontrol/input/xcursor/previewwidget.h47
-rw-r--r--kcontrol/input/xcursor/themepage.cpp637
-rw-r--r--kcontrol/input/xcursor/themepage.h76
5 files changed, 1120 insertions, 0 deletions
diff --git a/kcontrol/input/xcursor/Makefile.am b/kcontrol/input/xcursor/Makefile.am
new file mode 100644
index 000000000..9ef9c9bbf
--- /dev/null
+++ b/kcontrol/input/xcursor/Makefile.am
@@ -0,0 +1,7 @@
+AM_CPPFLAGS = $(all_includes)
+
+noinst_LTLIBRARIES = libthemepage.la
+libthemepage_la_SOURCES = themepage.cpp previewwidget.cpp
+METASOURCES = AUTO
+noinst_HEADERS = themepage.h previewwidget.h
+
diff --git a/kcontrol/input/xcursor/previewwidget.cpp b/kcontrol/input/xcursor/previewwidget.cpp
new file mode 100644
index 000000000..36108ef36
--- /dev/null
+++ b/kcontrol/input/xcursor/previewwidget.cpp
@@ -0,0 +1,353 @@
+/*
+ * Copyright (C) 2003 Fredrik H�glund <[email protected]>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License version 2 as published by the Free Software Foundation.
+ *
+ * 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 <kglobal.h>
+
+#include <qwidget.h>
+#include <qpainter.h>
+#include <qpixmap.h>
+#include <qstring.h>
+#include <qcursor.h>
+
+#include <kglobal.h>
+
+#include <X11/Xlib.h>
+#include <X11/Xutil.h>
+#include <X11/extensions/Xrender.h>
+#include <X11/Xcursor/Xcursor.h>
+
+#include "previewwidget.h"
+
+
+extern bool qt_has_xft;
+extern bool qt_use_xrender;
+
+
+namespace {
+
+ // Preview cursors
+ const char *cursor_names[] =
+ {
+ "left_ptr",
+ "left_ptr_watch",
+ "watch",
+ "hand2",
+ "question_arrow",
+ "sb_h_double_arrow",
+ "sb_v_double_arrow",
+ "bottom_left_corner",
+ "bottom_right_corner",
+ "fleur",
+ "pirate",
+ "cross",
+ "X_cursor",
+ "right_ptr",
+ "right_side",
+ "right_tee",
+ "sb_right_arrow",
+ "sb_right_tee",
+ "base_arrow_down",
+ "base_arrow_up",
+ "bottom_side",
+ "bottom_tee",
+ "center_ptr",
+ "circle",
+ "dot",
+ "dot_box_mask",
+ "dot_box_mask",
+ "double_arrow",
+ "draped_box",
+ "left_side",
+ "left_tee",
+ "ll_angle",
+ "top_side",
+ "top_tee",
+ };
+
+ const int numCursors = 6; // The number of cursors in the above list to be previewed
+ const int previewSize = 24; // The cursor size to be used in the preview widget
+ const int cursorSpacing = 20;
+}
+
+
+class PreviewCursor
+{
+ public:
+ PreviewCursor();
+ ~PreviewCursor();
+
+ void load( const QString &, const QString & );
+ const Picture picture() const { return m_pict; }
+ const Cursor handle() const { return m_handle; }
+ const int width() const { return m_width; }
+ const int height() const { return m_height; }
+
+ private:
+ Picture createPicture( const XcursorImage* ) const;
+ void cropCursorImage( XcursorImage*& ) const;
+ Picture m_pict;
+ Cursor m_handle;
+ int m_width;
+ int m_height;
+}; // class PreviewCursor
+
+
+PreviewCursor::PreviewCursor() :
+ m_pict( 0 ), m_handle( 0 ), m_width( 0 ), m_height( 0 )
+{
+}
+
+
+void PreviewCursor::load( const QString &name, const QString &theme )
+{
+ Display *dpy = QPaintDevice::x11AppDisplay();
+
+ if ( m_pict ) XRenderFreePicture( dpy, m_pict );
+ if ( m_handle ) XFreeCursor( dpy, m_handle );
+ m_pict = 0;
+ m_handle = 0;
+ m_width = m_height = 0;
+
+ // Load the preview cursor image
+ XcursorImage *image =
+ XcursorLibraryLoadImage( name.latin1(), theme.latin1(), previewSize );
+
+ // If the theme doesn't have this cursor, load the default cursor for now
+ if ( !image )
+ image = XcursorLibraryLoadImage( "left_ptr", theme.latin1(), previewSize );
+
+ // TODO The old classic X cursors
+ if ( !image )
+ return;
+
+ // Auto-crop the image (some cursor themes use a fixed image size
+ // for all cursors, and doing this results in correctly centered images)
+ cropCursorImage( image );
+
+ m_pict = createPicture( image );
+ m_width = image->width;
+ m_height = image->height;
+
+ // Scale the image if its height is greater than 2x the requested size
+ if ( m_height > previewSize * 2.0 ) {
+ double factor = double( previewSize * 2.0 / m_height );
+ XTransform xform = {
+ {{ XDoubleToFixed(1.0), XDoubleToFixed(0), XDoubleToFixed(0) },
+ { XDoubleToFixed(0), XDoubleToFixed(1.0), XDoubleToFixed(0) },
+ { XDoubleToFixed(0), XDoubleToFixed(0), XDoubleToFixed(factor) }}
+ };
+ XRenderSetPictureTransform( dpy, m_pict, &xform );
+ m_width = int( m_width * factor );
+ m_height = int( m_height * factor );
+ }
+
+ // We don't need this image anymore
+ XcursorImageDestroy( image );
+
+ // Load the actual cursor we will use
+ int size = XcursorGetDefaultSize( dpy );
+ XcursorImages *images = XcursorLibraryLoadImages( name.latin1(), theme.latin1(), size );
+
+ if ( images ) {
+ m_handle = XcursorImagesLoadCursor( dpy, images );
+ XcursorImagesDestroy( images );
+ } else {
+ images = XcursorLibraryLoadImages( "left_ptr", theme.latin1(), size );
+ m_handle = XcursorImagesLoadCursor( dpy, images );
+ XcursorImagesDestroy( images );
+ }
+}
+
+
+PreviewCursor::~PreviewCursor()
+{
+ if ( m_handle ) XFreeCursor( QPaintDevice::x11AppDisplay(), m_handle );
+ if ( m_pict ) XRenderFreePicture( QPaintDevice::x11AppDisplay(), m_pict );
+}
+
+
+Picture PreviewCursor::createPicture( const XcursorImage* image ) const
+{
+ Display *dpy = QPaintDevice::x11AppDisplay();
+
+ XImage ximage;
+ ximage.width = image->width;
+ ximage.height = image->height;
+ ximage.xoffset = 0;
+ ximage.format = ZPixmap;
+ ximage.data = reinterpret_cast<char*>( image->pixels );
+ ximage.byte_order = ImageByteOrder( dpy );
+ ximage.bitmap_unit = 32;
+ ximage.bitmap_bit_order = ximage.byte_order;
+ ximage.bitmap_pad = 32;
+ ximage.depth = 32;
+ ximage.bits_per_pixel = 32;
+ ximage.bytes_per_line = image->width * 4;
+ ximage.red_mask = 0x00ff0000;
+ ximage.green_mask = 0x0000ff00;
+ ximage.blue_mask = 0x000000ff;
+ ximage.obdata = 0;
+
+ XInitImage( &ximage );
+
+ Pixmap pix = XCreatePixmap( dpy, DefaultRootWindow(dpy), ximage.width, ximage.height, 32 );
+ GC gc = XCreateGC( dpy, pix, 0, NULL );
+ XPutImage( dpy, pix, gc, &ximage, 0, 0, 0, 0, ximage.width, ximage.height );
+ XFreeGC( dpy, gc );
+
+ XRenderPictFormat *fmt = XRenderFindStandardFormat( dpy, PictStandardARGB32 );
+ Picture pict = XRenderCreatePicture( dpy, pix, fmt, 0, NULL );
+ XFreePixmap( dpy, pix );
+
+ return pict;
+}
+
+
+void PreviewCursor::cropCursorImage( XcursorImage *&image ) const
+{
+ // Calculate the auto-crop rectangle
+ QRect r( QPoint( image->width, image->height ), QPoint() );
+ XcursorPixel *pixels = image->pixels;
+ for ( int y = 0; y < int(image->height); y++ ) {
+ for ( int x = 0; x < int(image->width); x++ ) {
+ if ( *(pixels++) >> 24 ) {
+ if ( x < r.left() ) r.setLeft( x );
+ if ( x > r.right() ) r.setRight( x );
+ if ( y < r.top() ) r.setTop( y );
+ if ( y > r.bottom() ) r.setBottom( y );
+ }
+ }
+ }
+
+ // Normalize the rectangle
+ r = r.normalize();
+
+ // Don't crop the image if the size isn't going to change
+ if ( r.width() == int( image->width ) && r.height() == int( image->height ) )
+ return;
+
+ // Create the new image
+ XcursorImage *cropped = XcursorImageCreate( r.width(), r.height() );
+ XcursorPixel *src = image->pixels + r.top() * image->width + r.left();
+ XcursorPixel *dst = cropped->pixels;
+ for ( int y = 0; y < r.height(); y++, src += (image->width - r.width()) ) {
+ for ( int x = 0; x < r.width(); x++ ) {
+ *(dst++) = *(src++);
+ }
+ }
+
+ // Destroy the original
+ XcursorImageDestroy( image );
+ image = cropped;
+}
+
+
+
+// ------------------------------------------------------------------------------------------------
+
+
+
+PreviewWidget::PreviewWidget( QWidget *parent, const char *name )
+ : QWidget( parent, name )
+{
+ cursors = new PreviewCursor* [ numCursors ];
+ for ( int i = 0; i < numCursors; i++ )
+ cursors[i] = new PreviewCursor;
+
+ current = -1;
+ setMouseTracking( true );
+ setFixedHeight( previewSize + 20 );
+}
+
+
+PreviewWidget::~PreviewWidget()
+{
+ for ( int i = 0; i < numCursors; i++ )
+ delete cursors[i];
+
+ delete [] cursors;
+}
+
+
+void PreviewWidget::setTheme( const QString &theme )
+{
+ setUpdatesEnabled( false );
+
+ int minHeight = previewSize + 20; // Minimum height of the preview widget
+ int maxHeight = height(); // Tallest cursor height
+ int maxWidth = previewSize; // Widest cursor width
+
+ for ( int i = 0; i < numCursors; i++ ) {
+ cursors[i]->load( cursor_names[i], theme.latin1() );
+ if ( cursors[i]->width() > maxWidth )
+ maxWidth = cursors[i]->width();
+ if ( cursors[i]->height() > maxHeight )
+ maxHeight = cursors[i]->height();
+ }
+
+ current = -1;
+ setFixedSize( ( maxWidth + cursorSpacing ) * numCursors, kMax( maxHeight, minHeight ) );
+ setUpdatesEnabled( true );
+ repaint( false );
+}
+
+
+void PreviewWidget::paintEvent( QPaintEvent * )
+{
+ QPixmap buffer( size() );
+ QPainter p( &buffer );
+ p.fillRect( rect(), colorGroup().brush( QColorGroup::Background ) );
+ Picture dest;
+
+ if ( !qt_has_xft || !qt_use_xrender ) {
+ XRenderPictFormat *fmt = XRenderFindVisualFormat( x11Display(), (Visual*)buffer.x11Visual() );
+ dest = XRenderCreatePicture( x11Display(), buffer.handle(), fmt, 0, NULL );
+ } else
+ dest = buffer.x11RenderHandle();
+
+ int rwidth = width() / numCursors;
+
+ for ( int i = 0; i < numCursors; i++ ) {
+ if ( cursors[i]->picture() ) {
+ XRenderComposite( x11Display(), PictOpOver,
+ cursors[i]->picture(), 0, dest, 0, 0, 0, 0,
+ rwidth * i + (rwidth - cursors[i]->width()) / 2,
+ (height() - cursors[i]->height()) / 2,
+ cursors[i]->width(), cursors[i]->height() );
+ }
+ }
+
+ bitBlt( this, 0, 0, &buffer );
+
+ if ( !qt_has_xft || !qt_use_xrender )
+ XRenderFreePicture( x11Display(), dest );
+}
+
+
+void PreviewWidget::mouseMoveEvent( QMouseEvent *e )
+{
+ int pos = e->x() / ( width() / numCursors );
+
+ if ( pos != current && pos < numCursors ) {
+ XDefineCursor( x11Display(), winId(), cursors[pos]->handle() );
+ current = pos;
+ }
+}
+
+
+// vim: set noet ts=4 sw=4:
diff --git a/kcontrol/input/xcursor/previewwidget.h b/kcontrol/input/xcursor/previewwidget.h
new file mode 100644
index 000000000..1c1d12c0f
--- /dev/null
+++ b/kcontrol/input/xcursor/previewwidget.h
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2003 Fredrik H�glund <[email protected]>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License version 2 as published by the Free Software Foundation.
+ *
+ * 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.
+ */
+
+
+#ifndef __CURSORPREVIEW_H
+#define __CURSORPREVIEW_H
+
+
+class PreviewCursor;
+
+
+class PreviewWidget : public QWidget
+{
+ public:
+ PreviewWidget( QWidget *parent = NULL, const char *name = NULL );
+ ~PreviewWidget();
+
+ void setTheme( const QString & );
+
+ void paintEvent( QPaintEvent * );
+ void mouseMoveEvent( QMouseEvent * );
+
+ private:
+ PreviewCursor **cursors;
+ int current;
+}; // class CursorPreview
+
+
+
+#endif
+
+// vim: set noet ts=4 sw=4:
diff --git a/kcontrol/input/xcursor/themepage.cpp b/kcontrol/input/xcursor/themepage.cpp
new file mode 100644
index 000000000..96e3e6871
--- /dev/null
+++ b/kcontrol/input/xcursor/themepage.cpp
@@ -0,0 +1,637 @@
+/*
+ * Copyright (C) 2003 Fredrik H�lund <[email protected]>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License version 2 as published by the Free Software Foundation.
+ *
+ * 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.
+ */
+
+#ifdef HAVE_CONFIG_H
+# include <config.h>
+#endif
+
+#include <klocale.h>
+#include <kaboutdata.h>
+#include <kstandarddirs.h>
+#include <klistview.h>
+#include <ksimpleconfig.h>
+#include <kglobalsettings.h>
+#include <kdialog.h>
+#include <kmessagebox.h>
+#include <kurlrequesterdlg.h>
+#include <kio/job.h>
+#include <kio/netaccess.h>
+#include <ktar.h>
+
+#include <qlayout.h>
+#include <qdir.h>
+#include <qpixmap.h>
+#include <qimage.h>
+#include <qlabel.h>
+#include <qhbox.h>
+#include <qpainter.h>
+#include <qfileinfo.h>
+#include <qpushbutton.h>
+
+#include <cstdlib> // for getenv()
+
+#include "themepage.h"
+#include "themepage.moc"
+
+#include "previewwidget.h"
+
+#include <X11/Xlib.h>
+#include <X11/Xcursor/Xcursor.h>
+
+// Check for older version
+#if !defined(XCURSOR_LIB_MAJOR) && defined(XCURSOR_MAJOR)
+# define XCURSOR_LIB_MAJOR XCURSOR_MAJOR
+# define XCURSOR_LIB_MINOR XCURSOR_MINOR
+#endif
+
+namespace {
+ // Listview icon size
+ const int iconSize = 24;
+
+ // Listview columns
+ enum Columns { NameColumn = 0, DescColumn, /* hidden */ DirColumn };
+}
+
+struct ThemeInfo {
+ QString path; // Path to the cursor theme
+ bool writable; // Theme directory is writable
+};
+
+
+ThemePage::ThemePage( QWidget* parent, const char* name )
+ : QWidget( parent, name ), selectedTheme( NULL ), currentTheme( NULL )
+{
+ QBoxLayout *layout = new QVBoxLayout( this );
+ layout->setAutoAdd( true );
+ layout->setMargin( KDialog::marginHint() );
+ layout->setSpacing( KDialog::spacingHint() );
+
+ new QLabel( i18n("Select the cursor theme you want to use (hover preview to test cursor):"), this );
+
+ // Create the preview widget
+ preview = new PreviewWidget( new QHBox( this ) );
+
+ // Create the theme list view
+ listview = new KListView( this );
+ listview->setFullWidth( true );
+ listview->setAllColumnsShowFocus( true );
+ listview->addColumn( i18n("Name") );
+ listview->addColumn( i18n("Description") );
+
+ connect( listview, SIGNAL(selectionChanged(QListViewItem*)),
+ SLOT(selectionChanged(QListViewItem*)) );
+
+ themeDirs = getThemeBaseDirs();
+ insertThemes();
+
+ QHBox *hbox = new QHBox( this );
+ hbox->setSpacing( KDialog::spacingHint() );
+ installButton = new QPushButton( i18n("Install New Theme..."), hbox );
+ removeButton = new QPushButton( i18n("Remove Theme"), hbox );
+
+ connect( installButton, SIGNAL( clicked() ), SLOT( installClicked() ) );
+ connect( removeButton, SIGNAL( clicked() ), SLOT( removeClicked() ) );
+
+ // Disable the install button if ~/.icons isn't writable
+ QString path = QDir::homeDirPath() + "/.icons";
+ QFileInfo icons = QFileInfo( path );
+
+ if ( ( icons.exists() && !icons.isWritable() ) ||
+ ( !icons.exists() && !QFileInfo( QDir::homeDirPath() ).isWritable() ) )
+ installButton->setEnabled( false );
+
+ if ( !themeDirs.contains( path ) )
+ installButton->setEnabled( false );
+
+ selectionChanged( listview->currentItem() );
+}
+
+
+ThemePage::~ThemePage()
+{
+}
+
+
+void ThemePage::save()
+{
+ if ( currentTheme == selectedTheme )
+ return;
+
+ KConfig c( "kcminputrc" );
+ c.setGroup( "Mouse" );
+ c.writeEntry( "cursorTheme", selectedTheme != "system" ? selectedTheme : QString::null );
+
+ KMessageBox::information( this, i18n("You have to restart KDE for these "
+ "changes to take effect."), i18n("Cursor Settings Changed"),
+ "CursorSettingsChanged" );
+
+ currentTheme = selectedTheme;
+}
+
+void ThemePage::load()
+{
+ load( false );
+}
+
+void ThemePage::load( bool useDefaults )
+{
+ // Get the name of the theme libXcursor currently uses
+ const char *theme = XcursorGetTheme( x11Display() );
+ currentTheme = theme;
+
+ // Get the name of the theme KDE is configured to use
+ KConfig c( "kcminputrc" );
+ c.setReadDefaults( useDefaults );
+ c.setGroup( "Mouse" );
+ currentTheme = c.readEntry( "cursorTheme", currentTheme );
+ if( currentTheme.isEmpty())
+ currentTheme = "system";
+
+ // Find the theme in the listview and select it
+ QListViewItem *item = listview->findItem( currentTheme, DirColumn );
+ if( !item )
+ item = listview->findItem( "system", DirColumn );
+ selectedTheme = item->text( DirColumn );
+ listview->setSelected( item, true );
+ listview->ensureItemVisible( item );
+
+ // Update the preview widget as well
+ if ( preview )
+ preview->setTheme( selectedTheme );
+
+ // Disable the listview if we're in kiosk mode
+ if ( c.entryIsImmutable( "cursorTheme" ) )
+ listview->setEnabled( false );
+}
+
+
+void ThemePage::defaults()
+{
+ load( true );
+}
+
+
+void ThemePage::selectionChanged( QListViewItem *item )
+{
+ if ( !item )
+ {
+ removeButton->setEnabled( false );
+ return;
+ }
+
+ selectedTheme = item->text( DirColumn );
+
+ // Update the preview widget
+ if ( preview )
+ preview->setTheme( selectedTheme );
+
+ removeButton->setEnabled( themeInfo[ selectedTheme ] && themeInfo[ selectedTheme ]->writable );
+
+ emit changed( currentTheme != selectedTheme );
+}
+
+
+void ThemePage::installClicked()
+{
+ // Get the URL for the theme we're going to install
+ KURL url = KURLRequesterDlg::getURL( QString::null, this, i18n( "Drag or Type Theme URL" ) );
+ if ( url.isEmpty() )
+ return;
+
+ QString tmpFile;
+ if ( !KIO::NetAccess::download( url, tmpFile, this ) ) {
+ QString text;
+
+ if ( url.isLocalFile() )
+ text = i18n( "Unable to find the cursor theme archive %1." );
+ else
+ text = i18n( "Unable to download the cursor theme archive; "
+ "please check that the address %1 is correct." );
+
+ KMessageBox::sorry( this, text.arg( url.prettyURL() ) );
+ return;
+ }
+
+ if ( !installThemes( tmpFile ) )
+ KMessageBox::error( this, i18n( "The file %1 does not appear to be a valid "
+ "cursor theme archive.").arg( url.fileName() ) );
+
+ KIO::NetAccess::removeTempFile( tmpFile );
+}
+
+
+void ThemePage::removeClicked()
+{
+ QString question = i18n( "<qt>Are you sure you want to remove the "
+ "<strong>%1</strong> cursor theme?<br>"
+ "This will delete all the files installed by this theme.</qt>")
+ .arg( listview->currentItem()->text( NameColumn ) );
+
+ // Get confirmation from the user
+ int answer = KMessageBox::warningContinueCancel( this, question, i18n( "Confirmation" ), KStdGuiItem::del() );
+ if ( answer != KMessageBox::Continue )
+ return;
+
+ // Delete the theme from the harddrive
+ KURL u;
+ u.setPath( themeInfo[ selectedTheme ]->path );
+ KIO::del( u );
+
+ // Remove the theme from the listview and from the themeinfo dict
+ delete listview->findItem( selectedTheme, DirColumn );
+ themeInfo.remove( selectedTheme );
+ listview->setSelected( listview->currentItem(), true );
+
+ // TODO:
+ // Since it's possible to substitute cursors in a system theme by adding a local
+ // theme with the same name, we shouldn't remove the theme from the list if it's
+ // still available elsewhere. This could be solved by calling insertThemes() here,
+ // but since KIO::del() is an asynchronos operation, the theme we're deleting will
+ // be readded to the list again before KIO has removed it.
+}
+
+
+bool ThemePage::installThemes( const QString &file )
+{
+ KTar archive( file );
+
+ if ( !archive.open( IO_ReadOnly ) )
+ return false;
+
+ const KArchiveDirectory *archiveDir = archive.directory();
+ QStringList themeDirs;
+
+ const QStringList entries = archiveDir->entries();
+ for ( QStringList::ConstIterator it = entries.begin(); it != entries.end(); ++it )
+ {
+ const KArchiveEntry *entry = archiveDir->entry( *it );
+ if ( entry->isDirectory() && entry->name().lower() != "default" ) {
+ const KArchiveDirectory *dir = static_cast< const KArchiveDirectory* >( entry );
+ if ( dir->entry( "index.theme" ) && dir->entry( "cursors" ) )
+ themeDirs << dir->name();
+ }
+ }
+
+ if ( themeDirs.count() < 1 )
+ return false;
+
+ const QString destDir = QDir::homeDirPath() + "/.icons/";
+ KStandardDirs::makeDir( destDir ); // Make sure the directory exists
+
+ for ( QStringList::ConstIterator it = themeDirs.begin(); it != themeDirs.end(); ++it )
+ {
+ // Check if a theme with that name already exists
+ if ( QDir( destDir ).exists( *it ) ) {
+ const QString question = i18n( "A theme named %1 already exists in your icon "
+ "theme folder. Do you want replace it with this one?" ).arg( *it );
+ int answer = KMessageBox::warningContinueCancel( this, question, i18n( "Overwrite Theme?"), i18n("Replace") );
+ if ( answer != KMessageBox::Continue )
+ continue;
+
+ // ### If the theme that's being replaced is the current theme, it
+ // will cause cursor inconsistencies in newly started apps.
+ }
+
+ // ### Should we check if a theme with the same name exists in a global theme dir?
+ // If that's the case it will effectively replace it, even though the global theme
+ // won't be deleted. Checking for this situation is easy, since the global theme
+ // will be in the listview. Maybe this should never be allowed since it might
+ // result in strange side effects (from the average users point of view). OTOH
+ // a user might want to do this 'upgrade' a global theme.
+
+ const QString dest = destDir + *it;
+ const KArchiveDirectory *dir = static_cast< const KArchiveDirectory* >( archiveDir->entry( *it ) );
+ dir->copyTo( dest );
+ insertTheme( dest );
+ }
+
+ listview->sort();
+
+ archive.close();
+ return true;
+}
+
+
+void ThemePage::insertTheme( const QString &path )
+{
+ QString dirName = QDir( path ).dirName();
+
+ // Defaults in case there's no name or comment field.
+ QString name = dirName;
+ QString desc = i18n( "No description available" );
+ QString sample = "left_ptr";
+
+ KSimpleConfig c( path + "/index.theme", true ); // Open read-only
+ c.setGroup( "Icon Theme" );
+
+ // Don't insert the theme if it's hidden.
+ if ( c.readBoolEntry( "Hidden", false ) )
+ return;
+
+ // ### If the theme is hidden, the user will probably find it strange that it
+ // doesn't appear in the list view. There also won't be a way for the user
+ // to delete the theme using the KCM. Perhaps a warning about this should
+ // be issued, and the user given a chance to undo the installation.
+
+ // Read the name, description and sample cursor
+ name = c.readEntry( "Name", name );
+ desc = c.readEntry( "Comment", desc );
+ sample = c.readEntry( "Example", sample );
+
+ // Create a ThemeInfo object if one doesn't already exist, and fill in the members
+ ThemeInfo *info = themeInfo[ dirName ];
+ if ( !info ) {
+ info = new ThemeInfo;
+ themeInfo.insert( dirName, info );
+ }
+
+ info->path = path;
+ info->writable = true;
+
+ // If an item with the same name already exists, remove it
+ delete listview->findItem( dirName, DirColumn );
+
+ // Create the listview item and insert it into the list.
+ KListViewItem *item = new KListViewItem( listview, name, desc, /*hidden*/ dirName );
+ item->setPixmap( NameColumn, createIcon( dirName, sample ) );
+ listview->insertItem( item );
+}
+
+
+const QStringList ThemePage::getThemeBaseDirs() const
+{
+#if XCURSOR_LIB_MAJOR == 1 && XCURSOR_LIB_MINOR < 1
+ // These are the default paths Xcursor will scan for cursor themes
+ QString path( "~/.icons:/usr/share/icons:/usr/share/pixmaps:/usr/X11R6/lib/X11/icons" );
+
+ // If XCURSOR_PATH is set, use that instead of the default path
+ char *xcursorPath = std::getenv( "XCURSOR_PATH" );
+ if ( xcursorPath )
+ path = xcursorPath;
+#else
+ // Get the search patch from Xcursor
+ QString path = XcursorLibraryPath();
+#endif
+ // Expand all occurences of ~ to the home dir
+ path.replace( "~/", QDir::homeDirPath() + '/' );
+ return QStringList::split( ':', path );
+}
+
+
+bool ThemePage::isCursorTheme( const QString &theme, const int depth ) const
+{
+ // Prevent infinate recursion
+ if ( depth > 10 )
+ return false;
+
+ // Search each icon theme directory for 'theme'
+ for ( QStringList::ConstIterator it = themeDirs.begin(); it != themeDirs.end(); ++it )
+ {
+ QDir dir( *it );
+ if ( !dir.exists() )
+ continue;
+
+ const QStringList subdirs( dir.entryList( QDir::Dirs ) );
+ if ( subdirs.contains( theme ) )
+ {
+ const QString path = *it + '/' + theme;
+ const QString indexfile = path + "/index.theme";
+ const bool haveIndexFile = dir.exists( indexfile );
+ const bool haveCursors = dir.exists( path + "/cursors" );
+ QStringList inherit;
+
+ // Return true if we have a cursors subdirectory
+ if ( haveCursors )
+ return true;
+
+ // Parse the index.theme file if one exists
+ if ( haveIndexFile )
+ {
+ KSimpleConfig c( indexfile, true ); // Open read-only
+ c.setGroup( "Icon Theme" );
+ inherit = c.readListEntry( "Inherits" );
+ }
+
+ // Recurse through the list of inherited themes
+ for ( QStringList::ConstIterator it2 = inherit.begin(); it2 != inherit.end(); ++it2 )
+ {
+ if ( *it2 == theme ) // Avoid possible DoS
+ continue;
+
+ if ( isCursorTheme( *it2, depth + 1 ) )
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+
+void ThemePage::insertThemes()
+{
+ // Scan each base dir for cursor themes and add them to the listview.
+ // An icon theme is considered to be a cursor theme if it contains
+ // a cursors subdirectory or if it inherits a cursor theme.
+ for ( QStringList::ConstIterator it = themeDirs.begin(); it != themeDirs.end(); ++it )
+ {
+ QDir dir( *it );
+ if ( !dir.exists() )
+ continue;
+
+ QStringList subdirs( dir.entryList( QDir::Dirs ) );
+ subdirs.remove( "." );
+ subdirs.remove( ".." );
+
+ for ( QStringList::ConstIterator it = subdirs.begin(); it != subdirs.end(); ++it )
+ {
+ // Only add the theme if we don't already have a theme with that name
+ // in the list. Xcursor will use the first theme it finds in that
+ // case, and since we use the same search order that should also be
+ // the theme we end up adding to the list.
+ if ( listview->findItem( *it, DirColumn ) )
+ continue;
+
+ const QString path = dir.path() + '/' + *it;
+ const QString indexfile = path + "/index.theme";
+ const bool haveIndexFile = dir.exists( *it + "/index.theme" );
+ const bool haveCursors = dir.exists( *it + "/cursors" );
+
+ // If the directory doesn't have a cursors subdir and lack an
+ // index.theme file it's definately not a cursor theme
+ if ( !haveIndexFile && !haveCursors )
+ continue;
+
+ // Defaults in case there's no index.theme file or it lacks
+ // a name and a comment field.
+ QString name = *it;
+ QString desc = i18n( "No description available" );
+ QString sample = "left_ptr";
+
+ // Parse the index.theme file if the theme has one.
+ if ( haveIndexFile )
+ {
+ KSimpleConfig c( indexfile, true );
+ c.setGroup( "Icon Theme" );
+
+ // Skip this theme if it's hidden.
+ if ( c.readBoolEntry( "Hidden", false ) )
+ continue;
+
+ // If there's no cursors subdirectory we'll do a recursive scan
+ // to check if the theme inherits a theme with one.
+ if ( !haveCursors )
+ {
+ bool result = false;
+ QStringList inherit = c.readListEntry( "Inherits" );
+ for ( QStringList::ConstIterator it2 = inherit.begin(); it2 != inherit.end(); ++it2 )
+ if ( result = isCursorTheme( *it2 ) )
+ break;
+
+ // If this theme doesn't inherit a cursor theme, proceed
+ // to the next theme in the list.
+ if ( !result )
+ continue;
+ }
+
+ // Read the name, description and sample cursor
+ name = c.readEntry( "Name", name );
+ desc = c.readEntry( "Comment", desc );
+ sample = c.readEntry( "Example", sample );
+ }
+
+ // Create a ThemeInfo object, and fill in the members
+ ThemeInfo *info = new ThemeInfo;
+ info->path = path;
+ info->writable = QFileInfo( path ).isWritable();
+ themeInfo.insert( *it, info );
+
+ // Create the listview item and insert it into the list.
+ KListViewItem *item = new KListViewItem( listview, name, desc, /*hidden*/ *it );
+ item->setPixmap( NameColumn, createIcon( *it, sample ) );
+ listview->insertItem( item );
+ }
+ }
+
+ // Note: If a default theme dir wasn't found in the above search, Xcursor will
+ // default to using the cursor font.
+
+ // Sort the theme list
+ listview->sort();
+
+ KListViewItem *item = new KListViewItem( listview, ' ' + i18n( "No theme" ), i18n( "The old classic X cursors") , /*hidden*/ "none" );
+ listview->insertItem( item );
+ item = new KListViewItem( listview, ' ' + i18n( "System theme" ), i18n( "Do not change cursor theme") , /*hidden*/ "system" );
+ listview->insertItem( item );
+ // no ThemeInfo object for this one
+}
+
+
+QPixmap ThemePage::createIcon( const QString &theme, const QString &sample ) const
+{
+ XcursorImage *xcur;
+ QPixmap pix;
+
+ xcur = XcursorLibraryLoadImage( sample.latin1(), theme.latin1(), iconSize );
+ if ( !xcur ) xcur = XcursorLibraryLoadImage( "left_ptr", theme.latin1(), iconSize );
+
+ if ( xcur ) {
+ // Calculate an auto-crop rectangle for the cursor image
+ // (helps with cursors converted from windows .cur or .ani files)
+ QRect r( QPoint( xcur->width, xcur->height ), QPoint() );
+ XcursorPixel *src = xcur->pixels;
+
+ for ( int y = 0; y < int( xcur->height ); y++ ) {
+ for ( int x = 0; x < int( xcur->width ); x++ ) {
+ if ( *(src++) >> 24 ) {
+ if ( x < r.left() ) r.setLeft( x );
+ if ( x > r.right() ) r.setRight( x );
+ if ( y < r.top() ) r.setTop( y );
+ if ( y > r.bottom() ) r.setBottom( y );
+ }
+ }
+ }
+
+ // Normalize the rectangle
+ r = r.normalize();
+
+ // Calculate the image size
+ int size = kMax( iconSize, kMax( r.width(), r.height() ) );
+
+ // Create the intermediate QImage
+ QImage image( size, size, 32 );
+ image.setAlphaBuffer( true );
+
+ // Clear the image
+ Q_UINT32 *dst = reinterpret_cast<Q_UINT32*>( image.bits() );
+ for ( int i = 0; i < image.width() * image.height(); i++ )
+ dst[i] = 0;
+
+ // Compute the source and destination offsets
+ QPoint dstOffset( (image.width() - r.width()) / 2, (image.height() - r.height()) / 2 );
+ QPoint srcOffset( r.topLeft() );
+
+ dst = reinterpret_cast<Q_UINT32*>( image.scanLine(dstOffset.y()) ) + dstOffset.x();
+ src = reinterpret_cast<Q_UINT32*>( xcur->pixels ) + srcOffset.y() * xcur->width + srcOffset.x();
+
+ // Copy the XcursorImage into the QImage, converting it from premultiplied
+ // to non-premultiplied alpha and cropping it if needed.
+ for ( int y = 0; y < r.height(); y++ )
+ {
+ for ( int x = 0; x < r.width(); x++, dst++, src++ ) {
+ const Q_UINT32 pixel = *src;
+
+ const Q_UINT8 a = qAlpha( pixel );
+ const Q_UINT8 r = qRed( pixel );
+ const Q_UINT8 g = qGreen( pixel );
+ const Q_UINT8 b = qBlue( pixel );
+
+ if ( !a || a == 255 ) {
+ *dst = pixel;
+ } else {
+ float alpha = a / 255.0;
+ *dst = qRgba( int(r / alpha), int(g / alpha), int(b / alpha), a );
+ }
+ }
+ dst += ( image.width() - r.width() );
+ src += ( xcur->width - r.width() );
+ }
+
+ // Scale down the image if we need to
+ if ( image.width() > iconSize || image.height() > iconSize )
+ image = image.smoothScale( iconSize, iconSize, QImage::ScaleMin );
+
+ pix.convertFromImage( image );
+ XcursorImageDestroy( xcur );
+ } else {
+
+ QImage image( iconSize, iconSize, 32 );
+ image.setAlphaBuffer( true );
+
+ Q_UINT32 *data = reinterpret_cast< Q_UINT32* >( image.bits() );
+ for ( int i = 0; i < image.width() * image.height(); i++ )
+ data[ i ] = 0;
+
+ pix.convertFromImage( image );
+ }
+
+ return pix;
+}
+
+
+// vim: set noet ts=4 sw=4:
diff --git a/kcontrol/input/xcursor/themepage.h b/kcontrol/input/xcursor/themepage.h
new file mode 100644
index 000000000..7d4dec675
--- /dev/null
+++ b/kcontrol/input/xcursor/themepage.h
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2003 Fredrik H�glund <[email protected]>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License version 2 as published by the Free Software Foundation.
+ *
+ * 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.
+ */
+
+#ifndef __THEMEPAGE_H
+#define __THEMEPAGE_H
+
+#include <qdict.h>
+
+
+class KListView;
+class QString;
+class PreviewWidget;
+class QStringList;
+class QListViewItem;
+class QPushButton;
+
+struct ThemeInfo;
+
+
+class ThemePage : public QWidget
+{
+ Q_OBJECT
+
+ public:
+ ThemePage( QWidget* parent = 0, const char* name = 0 );
+ ~ThemePage();
+
+ // Called by the KCM
+ void save();
+ void load();
+ void load( bool useDefaults );
+ void defaults();
+
+ signals:
+ void changed( bool );
+
+ private slots:
+ void selectionChanged( QListViewItem * );
+ void installClicked();
+ void removeClicked();
+
+ private:
+ bool installThemes( const QString &file );
+ void insertTheme( const QString & );
+ const QStringList getThemeBaseDirs() const;
+ bool isCursorTheme( const QString &theme, const int depth = 0 ) const;
+ void insertThemes();
+ QPixmap createIcon( const QString &, const QString & ) const;
+
+ KListView *listview;
+ PreviewWidget *preview;
+ QPushButton *installButton, *removeButton;
+ QString selectedTheme;
+ QString currentTheme;
+ QStringList themeDirs;
+ QDict<ThemeInfo> themeInfo;
+};
+
+#endif // __THEMEPAGE_H
+
+// vim: set noet ts=4 sw=4: