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 | |
49 | enum kde_startup_status_enum { StartupPre, StartupIn, StartupDone }; |
50 | static kde_startup_status_enum kde_startup_status = StartupPre; |
51 | static Atom kde_splash_progress; |
52 | |
53 | StartupId::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 | |
87 | StartupId::~StartupId() |
88 | { |
89 | stop_startupid(); |
90 | } |
91 | |
92 | void StartupId::configure() |
93 | { |
94 | startup_info.setTimeout( KLaunchSettings::timeout()); |
95 | blinking = KLaunchSettings::blinking(); |
96 | bouncing = KLaunchSettings::bouncing(); |
97 | } |
98 | |
99 | void 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 | |
109 | void 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 | |
124 | void 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 | |
142 | bool 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 | |
162 | void StartupId::finishKDEStartup() |
163 | { |
164 | kde_startup_status = StartupDone; |
165 | kapp->removeX11EventFilter( this ); |
166 | if( startups.count() == 0 ) |
167 | stop_startupid(); |
168 | } |
169 | |
170 | void 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 | |
183 | static 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. |
200 | static 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 | |
210 | void 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 | |
283 | namespace |
284 | { |
285 | const int X_DIFF = 15; |
286 | const int Y_DIFF = 15; |
287 | const int color_to_pixmap[] = { 0, 1, 2, 3, 2, 1 }; |
288 | const 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 | }; |
292 | const 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 | |
298 | void 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 | |
355 | void StartupId::newOwner() |
356 | { |
357 | active_selection = true; |
358 | } |
359 | |
360 | void StartupId::lostOwner() |
361 | { |
362 | active_selection = false; |
363 | } |
364 | |
365 | #include "startupid.moc" |
366 | |