summaryrefslogtreecommitdiffstats
path: root/kcontrol/input/xcursor/themepage.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'kcontrol/input/xcursor/themepage.cpp')
-rw-r--r--kcontrol/input/xcursor/themepage.cpp637
1 files changed, 637 insertions, 0 deletions
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 <fredrik@kde.org>
+ *
+ * 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: