1/* This file is part of the KDE project
2 Copyright (C) 2001 Lubos Lunak <l.lunak@kde.org>
3 Copyright (C) 2010 Martin Gräßlin <kde@martin-graesslin.com>
4
5 This library is free software; you can redistribute it and/or
6 modify it under the terms of the GNU Library General Public
7 License as published by the Free Software Foundation; either
8 version 2 of the License, or (at your option) any later version.
9
10 This library is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 Library General Public License for more details.
14
15 You should have received a copy of the GNU Library General Public License
16 along with this library; see the file COPYING.LIB. If not, write to
17 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
18 Boston, MA 02110-1301, USA.
19*/
20
21#include "startupid.h"
22
23#include <config-X11.h>
24
25#include "klaunchsettings.h"
26
27#include <kiconloader.h>
28#include <kmanagerselection.h>
29#include <QCursor>
30#include <kapplication.h>
31#include <QImage>
32#include <QBitmap>
33//Added by qt3to4:
34#include <QPixmap>
35#include <QPainter>
36#include <kconfig.h>
37#include <X11/Xlib.h>
38#include <QX11Info>
39#include <X11/Xutil.h>
40#include <X11/Xatom.h>
41#include <X11/extensions/shape.h>
42
43#define KDE_STARTUP_ICON QLatin1String( "kmenu" )
44
45#ifdef HAVE_XCURSOR
46#include <X11/Xcursor/Xcursor.h>
47#endif
48
49enum kde_startup_status_enum { StartupPre, StartupIn, StartupDone };
50static kde_startup_status_enum kde_startup_status = StartupPre;
51static Atom kde_splash_progress;
52
53StartupId::StartupId( QWidget* parent, const char* name )
54 : QWidget( parent ),
55 startup_info( KStartupInfo::CleanOnCantDetect ),
56 startup_window( None ),
57 blinking( true ),
58 bouncing( false ),
59 selection_watcher( new KSelectionWatcher( "_KDE_STARTUP_FEEDBACK", -1, this ))
60 {
61 setObjectName( QLatin1String( name ) );
62 hide(); // is QWidget only because of x11Event()
63 if( kde_startup_status == StartupPre )
64 {
65 kde_splash_progress = XInternAtom( QX11Info::display(), "_KDE_SPLASH_PROGRESS", False );
66 XWindowAttributes attrs;
67 XGetWindowAttributes( QX11Info::display(), QX11Info::appRootWindow(), &attrs);
68 XSelectInput( QX11Info::display(), QX11Info::appRootWindow(), attrs.your_event_mask | SubstructureNotifyMask);
69 kapp->installX11EventFilter( this );
70 }
71 update_timer.setSingleShot( true );
72 connect( &update_timer, SIGNAL(timeout()), SLOT(update_startupid()));
73 connect( &startup_info,
74 SIGNAL(gotNewStartup(KStartupInfoId,KStartupInfoData)),
75 SLOT(gotNewStartup(KStartupInfoId,KStartupInfoData)));
76 connect( &startup_info,
77 SIGNAL(gotStartupChange(KStartupInfoId,KStartupInfoData)),
78 SLOT(gotStartupChange(KStartupInfoId,KStartupInfoData)));
79 connect( &startup_info,
80 SIGNAL(gotRemoveStartup(KStartupInfoId,KStartupInfoData)),
81 SLOT(gotRemoveStartup(KStartupInfoId)));
82 connect( selection_watcher, SIGNAL(newOwner(Window)), SLOT(newOwner()));
83 connect( selection_watcher, SIGNAL(lostOwner()), SLOT(lostOwner()));
84 active_selection = ( selection_watcher->owner() != None );
85 }
86
87StartupId::~StartupId()
88 {
89 stop_startupid();
90 }
91
92void StartupId::configure()
93 {
94 startup_info.setTimeout( KLaunchSettings::timeout());
95 blinking = KLaunchSettings::blinking();
96 bouncing = KLaunchSettings::bouncing();
97 }
98
99void StartupId::gotNewStartup( const KStartupInfoId& id_P, const KStartupInfoData& data_P )
100 {
101 if( active_selection )
102 return;
103 QString icon = data_P.findIcon();
104 current_startup = id_P;
105 startups[ id_P ] = icon;
106 start_startupid( icon );
107 }
108
109void StartupId::gotStartupChange( const KStartupInfoId& id_P, const KStartupInfoData& data_P )
110 {
111 if( active_selection )
112 return;
113 if( current_startup == id_P )
114 {
115 QString icon = data_P.findIcon();
116 if( !icon.isEmpty() && icon != startups[ current_startup ] )
117 {
118 startups[ id_P ] = icon;
119 start_startupid( icon );
120 }
121 }
122 }
123
124void StartupId::gotRemoveStartup( const KStartupInfoId& id_P )
125 {
126 if( active_selection )
127 return;
128 startups.remove( id_P );
129 if( startups.count() == 0 )
130 {
131 current_startup = KStartupInfoId(); // null
132 if( kde_startup_status == StartupIn )
133 start_startupid( KDE_STARTUP_ICON );
134 else
135 stop_startupid();
136 return;
137 }
138 current_startup = startups.begin().key();
139 start_startupid( startups[ current_startup ] );
140 }
141
142bool StartupId::x11Event( XEvent* e )
143 {
144 if( e->type == ClientMessage && e->xclient.window == QX11Info::appRootWindow()
145 && e->xclient.message_type == kde_splash_progress )
146 {
147 const char* s = e->xclient.data.b;
148 if( strcmp( s, "desktop" ) == 0 && kde_startup_status == StartupPre )
149 {
150 kde_startup_status = StartupIn;
151 if( startups.count() == 0 )
152 start_startupid( KDE_STARTUP_ICON );
153 // 60(?) sec timeout - shouldn't be hopefully needed anyway, ksmserver should have it too
154 QTimer::singleShot( 60000, this, SLOT(finishKDEStartup()));
155 }
156 else if( strcmp( s, "ready" ) == 0 && kde_startup_status < StartupDone )
157 QTimer::singleShot( 2000, this, SLOT(finishKDEStartup()));
158 }
159 return false;
160 }
161
162void StartupId::finishKDEStartup()
163 {
164 kde_startup_status = StartupDone;
165 kapp->removeX11EventFilter( this );
166 if( startups.count() == 0 )
167 stop_startupid();
168 }
169
170void StartupId::stop_startupid()
171 {
172 if( startup_window != None )
173 XDestroyWindow( QX11Info::display(), startup_window );
174 startup_window = None;
175 if( blinking )
176 for( int i = 0;
177 i < NUM_BLINKING_PIXMAPS;
178 ++i )
179 pixmaps[ i ] = QPixmap(); // null
180 update_timer.stop();
181 }
182
183static QPixmap scalePixmap( const QPixmap& pm, int w, int h )
184{
185 QImage scaled = pm.toImage().scaled(w, h, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
186 if (scaled.format() != QImage::Format_ARGB32_Premultiplied && scaled.format() != QImage::Format_ARGB32)
187 scaled = scaled.convertToFormat(QImage::Format_ARGB32_Premultiplied);
188
189 QImage result(20, 20, QImage::Format_ARGB32_Premultiplied);
190 QPainter p(&result);
191 p.setCompositionMode(QPainter::CompositionMode_Source);
192 p.fillRect(result.rect(), Qt::transparent);
193 p.drawImage((20 - w) / 2, (20 - h) / 2, scaled, 0, 0, w, h);
194 return QPixmap::fromImage(result);
195}
196
197// Transparent images are converted to 32bpp pixmaps, but
198// setting those as window background needs ARGB visual
199// and compositing - convert to 24bpp, at least for now.
200static QPixmap make24bpp( const QPixmap& p )
201 {
202 QPixmap ret( p.size());
203 QPainter pt( &ret );
204 pt.drawPixmap( 0, 0, p );
205 pt.end();
206 ret.setMask( p.mask());
207 return ret;
208 }
209
210void StartupId::start_startupid( const QString& icon_P )
211 {
212
213 const QColor startup_colors[ StartupId::NUM_BLINKING_PIXMAPS ]
214 = { Qt::black, Qt::darkGray, Qt::lightGray, Qt::white, Qt::white };
215
216
217 QPixmap icon_pixmap = KIconLoader::global()->loadIcon( icon_P, KIconLoader::Small, 0,
218 KIconLoader::DefaultState, QStringList(), 0, true ); // return null pixmap if not found
219 if( icon_pixmap.isNull())
220 icon_pixmap = SmallIcon( QLatin1String( "system-run" ) );
221 if( startup_window == None )
222 {
223 XSetWindowAttributes attrs;
224 attrs.override_redirect = True;
225 attrs.save_under = True; // useful saveunder if possible to avoid redrawing
226 attrs.colormap = QX11Info::appColormap();
227 attrs.background_pixel = WhitePixel( QX11Info::display(), QX11Info::appScreen());
228 attrs.border_pixel = BlackPixel( QX11Info::display(), QX11Info::appScreen());
229 startup_window = XCreateWindow( QX11Info::display(), DefaultRootWindow( QX11Info::display()),
230 0, 0, 1, 1, 0, QX11Info::appDepth(), InputOutput, static_cast< Visual* >( QX11Info::appVisual()),
231 CWOverrideRedirect | CWSaveUnder | CWColormap | CWBackPixel | CWBorderPixel, &attrs );
232 XClassHint class_hint;
233 QByteArray cls = qAppName().toLatin1();
234 class_hint.res_name = cls.data();
235 class_hint.res_class = const_cast< char* >( QX11Info::appClass());
236 XSetWMProperties( QX11Info::display(), startup_window, NULL, NULL, NULL, 0, NULL, NULL, &class_hint );
237 XChangeProperty( QX11Info::display(), winId(),
238 XInternAtom( QX11Info::display(), "WM_WINDOW_ROLE", False ), XA_STRING, 8, PropModeReplace,
239 (unsigned char *)"startupfeedback", strlen( "startupfeedback" ));
240 }
241 XResizeWindow( QX11Info::display(), startup_window, icon_pixmap.width(), icon_pixmap.height());
242 if( blinking )
243 { // no mask
244 XShapeCombineMask( QX11Info::display(), startup_window, ShapeBounding, 0, 0, None, ShapeSet );
245 int window_w = icon_pixmap.width();
246 int window_h = icon_pixmap.height();
247 for( int i = 0;
248 i < NUM_BLINKING_PIXMAPS;
249 ++i )
250 {
251 pixmaps[ i ] = QPixmap( window_w, window_h );
252 pixmaps[ i ].fill( startup_colors[ i ] );
253 QPainter p( &pixmaps[ i ] );
254 p.drawPixmap( 0, 0, icon_pixmap );
255 p.end();
256 }
257 color_index = 0;
258 }
259 else if( bouncing )
260 {
261 XResizeWindow( QX11Info::display(), startup_window, 20, 20 );
262 pixmaps[ 0 ] = make24bpp( scalePixmap( icon_pixmap, 16, 16 ));
263 pixmaps[ 1 ] = make24bpp( scalePixmap( icon_pixmap, 14, 18 ));
264 pixmaps[ 2 ] = make24bpp( scalePixmap( icon_pixmap, 12, 20 ));
265 pixmaps[ 3 ] = make24bpp( scalePixmap( icon_pixmap, 18, 14 ));
266 pixmaps[ 4 ] = make24bpp( scalePixmap( icon_pixmap, 20, 12 ));
267 frame = 0;
268 }
269 else
270 {
271 icon_pixmap = make24bpp( icon_pixmap );
272 if( !icon_pixmap.mask().isNull() ) // set mask
273 XShapeCombineMask( QX11Info::display(), startup_window, ShapeBounding, 0, 0,
274 icon_pixmap.mask().handle(), ShapeSet );
275 else // clear mask
276 XShapeCombineMask( QX11Info::display(), startup_window, ShapeBounding, 0, 0, None, ShapeSet );
277 XSetWindowBackgroundPixmap( QX11Info::display(), startup_window, icon_pixmap.handle());
278 XClearWindow( QX11Info::display(), startup_window );
279 }
280 update_startupid();
281 }
282
283namespace
284{
285const int X_DIFF = 15;
286const int Y_DIFF = 15;
287const int color_to_pixmap[] = { 0, 1, 2, 3, 2, 1 };
288const int frame_to_yoffset[] =
289 {
290 -5, -1, 2, 5, 8, 10, 12, 13, 15, 15, 15, 15, 14, 12, 10, 8, 5, 2, -1, -5
291 };
292const int frame_to_pixmap[] =
293 {
294 0, 0, 0, 1, 2, 2, 1, 0, 3, 4, 4, 3, 0, 1, 2, 2, 1, 0, 0, 0
295 };
296}
297
298void StartupId::update_startupid()
299 {
300 int yoffset = 0;
301 if( blinking )
302 {
303 XSetWindowBackgroundPixmap( QX11Info::display(), startup_window,
304 pixmaps[ color_to_pixmap[ color_index ]].handle());
305 XClearWindow( QX11Info::display(), startup_window );
306 if( ++color_index >= ( sizeof( color_to_pixmap ) / sizeof( color_to_pixmap[ 0 ] )))
307 color_index = 0;
308 }
309 else if( bouncing )
310 {
311 yoffset = frame_to_yoffset[ frame ];
312 QPixmap pixmap = pixmaps[ frame_to_pixmap[ frame ] ];
313 XSetWindowBackgroundPixmap( QX11Info::display(), startup_window, pixmap.handle());
314 XClearWindow( QX11Info::display(), startup_window );
315 if ( !pixmap.mask().isNull() ) // set mask
316 XShapeCombineMask( QX11Info::display(), startup_window, ShapeBounding, 0, 0,
317 pixmap.mask().handle(), ShapeSet );
318 else // clear mask
319 XShapeCombineMask( QX11Info::display(), startup_window, ShapeBounding, 0, 0, None, ShapeSet );
320 if ( ++frame >= ( sizeof( frame_to_yoffset ) / sizeof( frame_to_yoffset[ 0 ] ) ) )
321 frame = 0;
322 }
323 Window dummy1, dummy2;
324 int x, y;
325 int dummy3, dummy4;
326 unsigned int dummy5;
327 if( !XQueryPointer( QX11Info::display(), QX11Info::appRootWindow(), &dummy1, &dummy2, &x, &y, &dummy3, &dummy4, &dummy5 ))
328 {
329 XUnmapWindow( QX11Info::display(), startup_window );
330 update_timer.start( 100 );
331 return;
332 }
333 QPoint c_pos( x, y );
334 int cursor_size = 0;
335#ifdef HAVE_XCURSOR
336 cursor_size = XcursorGetDefaultSize( QX11Info::display());
337#endif
338 int X_DIFF;
339 if( cursor_size <= 16 )
340 X_DIFF = 8 + 7;
341 else if( cursor_size <= 32 )
342 X_DIFF = 16 + 7;
343 else if( cursor_size <= 48 )
344 X_DIFF = 24 + 7;
345 else
346 X_DIFF = 32 + 7;
347 int Y_DIFF = X_DIFF;
348 XMoveWindow( QX11Info::display(), startup_window, c_pos.x() + X_DIFF, c_pos.y() + Y_DIFF + yoffset );
349 XMapWindow( QX11Info::display(), startup_window );
350 XRaiseWindow( QX11Info::display(), startup_window );
351 update_timer.start( bouncing ? 30 : 100 );
352 QApplication::flush();
353 }
354
355void StartupId::newOwner()
356 {
357 active_selection = true;
358 }
359
360void StartupId::lostOwner()
361 {
362 active_selection = false;
363 }
364
365#include "startupid.moc"
366