diff options
Diffstat (limited to 'kcontrol/input/xcursor')
-rw-r--r-- | kcontrol/input/xcursor/Makefile.am | 7 | ||||
-rw-r--r-- | kcontrol/input/xcursor/previewwidget.cpp | 353 | ||||
-rw-r--r-- | kcontrol/input/xcursor/previewwidget.h | 47 | ||||
-rw-r--r-- | kcontrol/input/xcursor/themepage.cpp | 637 | ||||
-rw-r--r-- | kcontrol/input/xcursor/themepage.h | 76 |
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 <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. + */ + +#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 <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. + */ + + +#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 <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: 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 <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. + */ + +#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: |