Merge lp:~macslow/kazam/old-school-countdown-effect into lp:kazam
- old-school-countdown-effect
- Merge into stable
Proposed by
Mirco Müller
Status: | Merged |
---|---|
Approved by: | David Klasinc |
Approved revision: | 143 |
Merge reported by: | David Klasinc |
Merged at revision: | not available |
Proposed branch: | lp:~macslow/kazam/old-school-countdown-effect |
Merge into: | lp:kazam |
Diff against target: |
384 lines (+286/-59) 2 files modified
AUTHORS (+1/-1) kazam/frontend/window_countdown.py (+285/-58) |
To merge this branch: | bzr merge lp:~macslow/kazam/old-school-countdown-effect |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
David Klasinc | Approve | ||
Review via email: mp+216505@code.launchpad.net |
Commit message
Replaced countdown with old-school animation widget.
Description of the change
Replaced countdown with old-school animation widget shown here...
https:/
This needs pycairo 1.10.1 (Ubuntu 14.04 LTS only comes with pycairo 1.10.0) and ctypes (to load shared-library of pixman). The countdown now adapts fully to screen-resolution and the numbers can now even be localized.
To post a comment you must log in.
- 140. By Mirco Müller
-
Fix short flashing of initial countdown-state when counter ran out.
- 141. By Mirco Müller
-
Revert an accidentally committed change to kazam.pot
- 142. By Mirco Müller
-
Fix bug where indicator icon turns to 'recording' when user cancels countdown.
- 143. By Mirco Müller
-
Added my name to the list of contributors/
authors.
Revision history for this message
David Klasinc (bigwhale) : | # |
review:
Approve
Revision history for this message
David Klasinc (bigwhale) wrote : | # |
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'AUTHORS' |
2 | --- AUTHORS 2013-02-28 13:27:34 +0000 |
3 | +++ AUTHORS 2014-04-19 20:26:43 +0000 |
4 | @@ -12,4 +12,4 @@ |
5 | Georgi Karavasilev - Design, Testing |
6 | Alan Pope - Testing |
7 | Ken VanDine - Packaging |
8 | - |
9 | +Mirco "MacSlow" Müller - old-school countdown widget |
10 | |
11 | === modified file 'kazam/frontend/window_countdown.py' |
12 | --- kazam/frontend/window_countdown.py 2013-04-21 04:47:41 +0000 |
13 | +++ kazam/frontend/window_countdown.py 2014-04-19 20:26:43 +0000 |
14 | @@ -22,12 +22,22 @@ |
15 | |
16 | import os |
17 | import cairo |
18 | +import time |
19 | +import math |
20 | +from ctypes import * |
21 | |
22 | from gettext import gettext as _ |
23 | -from gi.repository import Gtk, GObject, GLib |
24 | +from gi.repository import Gtk, GObject, GLib, Gdk, Pango, PangoCairo |
25 | |
26 | from kazam.backend.prefs import * |
27 | |
28 | +HEIGHT_FRACTION = 2.5 |
29 | +FPS = 24 |
30 | +LIBPIXMAN_NAME = "libpixman-1.so" |
31 | +PIXMAN_FILTER_CONVOLUTION = 5 |
32 | +PIXMAN_OP_SRC = 1 |
33 | +PIXMAN_a8r8g8b8 = 0x20028888 |
34 | + |
35 | class CountdownWindow(GObject.GObject): |
36 | |
37 | __gsignals__ = { |
38 | @@ -37,72 +47,289 @@ |
39 | ), |
40 | } |
41 | |
42 | - def __init__(self, indicator, number = 5, show_window = True): |
43 | + def __init__(self, indicator, show_window = True): |
44 | super(CountdownWindow, self).__init__() |
45 | self.indicator = indicator |
46 | - self.number = number |
47 | self.canceled = False |
48 | self.show_window = show_window |
49 | |
50 | - self.window = Gtk.Window() |
51 | + # setup libpixman via ctypes |
52 | + self.setupPixman (LIBPIXMAN_NAME) |
53 | + |
54 | + self.window = Gtk.Window (Gtk.WindowType.TOPLEVEL) |
55 | self.window.connect("delete-event", Gtk.main_quit) |
56 | - self.window.connect("draw", self.cb_draw) |
57 | - self.width = 380 |
58 | - self.height = 380 |
59 | - self.window.set_default_geometry(self.height, self.width) |
60 | - self.window.set_default_size(self.width, self.height) |
61 | - self.window.set_position(Gtk.WindowPosition.CENTER) |
62 | - self.window.set_app_paintable(True) |
63 | - self.window.set_has_resize_grip(False) |
64 | - self.window.set_resizable(True) |
65 | - |
66 | - self.window.set_decorated(False) |
67 | - self.window.set_property("skip-taskbar-hint", True) |
68 | - self.window.set_keep_above(True) |
69 | - self.screen = self.window.get_screen() |
70 | - self.visual = self.screen.get_rgba_visual() |
71 | - |
72 | - if self.visual is not None and self.screen.is_composited(): |
73 | - self.window.set_visual(self.visual) |
74 | - |
75 | - |
76 | - def run(self, counter): |
77 | - if counter > 0: |
78 | - self.number = counter + 1 |
79 | - if self.show_window: |
80 | - self.window.show_all() |
81 | - else: |
82 | - self.number = 0 |
83 | - self.countdown() |
84 | - |
85 | - def countdown(self): |
86 | - if not self.canceled: |
87 | - if self.number < 5: |
88 | - self.indicator.blink_set_state(BLINK_FAST) |
89 | - if self.number > 1: |
90 | - self.window.queue_draw() |
91 | - GLib.timeout_add(1000, self.countdown) |
92 | - self.number -= 1 |
93 | - else: |
94 | - self.window.destroy() |
95 | - GLib.timeout_add(400, self.counter_finished) |
96 | - |
97 | - def cancel_countdown(self): |
98 | + self.window.connect("draw", self.onDraw) |
99 | + self.window.connect ("screen-changed", self.onScreenChanged) |
100 | + self.window.set_app_paintable (True) |
101 | + self.window.set_decorated (False) |
102 | + self.window.set_title ("CountdownWindow") |
103 | + self.window.set_keep_above (True) |
104 | + self.window.set_focus_on_map (False) |
105 | + self.window.set_accept_focus (False) |
106 | + self.window.set_skip_pager_hint (True) |
107 | + self.window.set_skip_taskbar_hint (True) |
108 | + |
109 | + # make window click-through, this needs pycairo 1.10.0 for python3 |
110 | + # to work |
111 | + rect = cairo.RectangleInt (0, 0, 1, 1) |
112 | + region = cairo.Region (rect) |
113 | + if (not region.is_empty ()): |
114 | + self.window.input_shape_combine_region (None) |
115 | + self.window.input_shape_combine_region (region) |
116 | + |
117 | + # make sure that gtk-window opens with a RGBA-visual |
118 | + self.onScreenChanged (self.window, None) |
119 | + self.window.realize () |
120 | + self.window.set_type_hint (Gdk.WindowTypeHint.DOCK) |
121 | + transparent = Gdk.RGBA (0.0, 0.0, 0.0, 0.0) |
122 | + gdkwindow = self.window.get_window () |
123 | + gdkwindow.set_background_rgba (transparent) |
124 | + |
125 | + # center the gtk-window on the screen |
126 | + screen = self.window.get_screen () |
127 | + monitor = screen.get_monitor_at_window (gdkwindow) |
128 | + geo = screen.get_monitor_geometry (monitor) |
129 | + size = geo.height / HEIGHT_FRACTION |
130 | + self.window.set_size_request (size, size) |
131 | + self.window.set_position (Gtk.WindowPosition.CENTER) |
132 | + |
133 | + # setup libpixman via ctypes and init other stuff |
134 | + self.setupPixman (LIBPIXMAN_NAME) |
135 | + self.layout = None |
136 | + self.desc = None |
137 | + self.dropShadow = self.createDropShadow (int (size), int (size), 10) |
138 | + self.secondsf = 0.0 |
139 | + |
140 | + def setupPixman (self, name): |
141 | + libpixman = cdll.LoadLibrary (name) |
142 | + self.pixman_image_create_bits = libpixman.pixman_image_create_bits |
143 | + self.pixman_image_set_filter = libpixman.pixman_image_set_filter |
144 | + self.pixman_image_composite = libpixman.pixman_image_composite |
145 | + self.pixman_image_unref = libpixman.pixman_image_unref |
146 | + |
147 | + def createGaussianBlurKerne1D (self, radius, sigma): |
148 | + scale2 = 2.0 * sigma * sigma |
149 | + scale1 = 1.0 / (math.pi * scale2) |
150 | + size = 2 * radius + 1; |
151 | + n_params = size |
152 | + |
153 | + tmp = (c_double * n_params)() |
154 | + tmpSum = 0.0 |
155 | + i = 0 |
156 | + |
157 | + # caluclate gaussian kernel in floating point format |
158 | + for x in range (-radius, radius + 1): |
159 | + u = x * x |
160 | + tmp[i] = scale1 * math.exp (-u / scale2) |
161 | + tmpSum += tmp[i] |
162 | + i += 1 |
163 | + |
164 | + # normalize gaussian kernel and convert to fixed point format |
165 | + params = (c_int32 * (n_params + 2))() |
166 | + |
167 | + params[0] = size << 16 |
168 | + params[1] = 1 << 16 |
169 | + |
170 | + for i in range (n_params): |
171 | + params[2 + i] = int ((tmp[i] / tmpSum) * 65536.0) |
172 | + |
173 | + n_params += 2 |
174 | + |
175 | + return params, n_params |
176 | + |
177 | + def blurSurface (self, surface, data, radius, sigma): |
178 | + width = surface.get_width () |
179 | + height = surface.get_height () |
180 | + stride = surface.get_stride () |
181 | + format = surface.get_format () |
182 | + |
183 | + # create pixman image for cairo image surface |
184 | + src = self.pixman_image_create_bits (PIXMAN_a8r8g8b8, width, height, data, stride) |
185 | + |
186 | + # attach gaussian kernel to pixman image |
187 | + params, n_params = self.createGaussianBlurKerne1D (radius, sigma) |
188 | + self.pixman_image_set_filter (src, PIXMAN_FILTER_CONVOLUTION, params, n_params) |
189 | + |
190 | + # render blured image to new pixman image |
191 | + pass1Data = (c_uint32 * stride * height)() |
192 | + pass1 = self.pixman_image_create_bits (PIXMAN_a8r8g8b8, width, height, pass1Data, stride) |
193 | + self.pixman_image_composite (PIXMAN_OP_SRC, src, None, pass1, 0, 0, 0, 0, 0, 0, width, height) |
194 | + self.pixman_image_unref (src) |
195 | + |
196 | + # flip the 1D kernel |
197 | + tmp = params[0] |
198 | + params[0] = params[1] |
199 | + params[1] = tmp |
200 | + self.pixman_image_set_filter (pass1, PIXMAN_FILTER_CONVOLUTION, params, n_params) |
201 | + |
202 | + pass2Data = (c_ubyte * stride * height)() |
203 | + pass2 = self.pixman_image_create_bits (PIXMAN_a8r8g8b8, width, height, pass2Data, stride) |
204 | + self.pixman_image_composite (PIXMAN_OP_SRC, pass1, None, pass2, 0, 0, 0, 0, 0, 0, width, height) |
205 | + self.pixman_image_unref (pass1) |
206 | + |
207 | + # create new cairo image for blured pixman image |
208 | + surface = cairo.ImageSurface.create_for_data (pass2Data, format, width, height, stride) |
209 | + self.pixman_image_unref (pass2) |
210 | + |
211 | + return surface |
212 | + |
213 | + def createDropShadow (self, width, height, blurRadius): |
214 | + stride = 4 * width |
215 | + format = cairo.FORMAT_ARGB32 |
216 | + data = (c_uint32 * stride * height)() |
217 | + surface = cairo.ImageSurface.create_for_data (data, format, width, height) |
218 | + cr = cairo.Context (surface) |
219 | + |
220 | + # clear context |
221 | + cr.scale (width, height) |
222 | + cr.set_operator (cairo.OPERATOR_CLEAR) |
223 | + cr.paint () |
224 | + |
225 | + # drop-shadow |
226 | + cr.set_operator (cairo.OPERATOR_SOURCE) |
227 | + cr.set_source_rgba (0.35, 0.35, 0.35, 0.8) |
228 | + cr.move_to (0.5, 0.5) |
229 | + cr.arc (0.5, 0.5, 0.4125, 0.0, math.pi / 180.0 * 360.0) |
230 | + cr.close_path () |
231 | + cr.fill () |
232 | + |
233 | + # blur surface |
234 | + blurredSurface = self.blurSurface (surface, data, 20, 3.0) |
235 | + |
236 | + return blurredSurface |
237 | + |
238 | + def drawNumber (self, cr, width, height, number): |
239 | + # create pango desc/layout |
240 | + if (self.layout == None): |
241 | + self.layout = PangoCairo.create_layout (cr) |
242 | + self.desc = Pango.font_description_from_string ("Ubuntu Mono") |
243 | + self.desc.set_absolute_size (0.75 * height * Pango.SCALE) |
244 | + self.desc.set_weight (Pango.Weight.NORMAL) |
245 | + self.desc.set_style (Pango.Style.NORMAL) |
246 | + self.layout.set_font_description (self.desc) |
247 | + |
248 | + # print and layout string (pango-wise) |
249 | + self.layout.set_text (str (number), -1) |
250 | + |
251 | + # determine center position for number |
252 | + rects = self.layout.get_extents () |
253 | + x = width / 2 - rects[0].x / Pango.SCALE - rects[0].width / Pango.SCALE / 2; |
254 | + y = height / 2 - rects[0].y / Pango.SCALE - rects[0].height / Pango.SCALE / 2; |
255 | + |
256 | + # draw text |
257 | + cr.move_to (x, y) |
258 | + PangoCairo.layout_path (cr, self.layout) |
259 | + cr.set_operator (cairo.OPERATOR_SOURCE) |
260 | + cr.set_source_rgb (0.0, 0.0, 0.0) |
261 | + cr.fill () |
262 | + |
263 | + def onScreenChanged (self, widget, oldScreen): |
264 | + screen = widget.get_screen () |
265 | + visual = screen.get_rgba_visual () |
266 | + if (visual == None): |
267 | + visual = screen.get_system_visual () |
268 | + widget.set_visual (visual) |
269 | + |
270 | + def onTimeout (self, widget): |
271 | + if self.secondsf >= float (self.number): |
272 | + self.counter_finished () |
273 | + return False |
274 | + |
275 | + widget.queue_draw () |
276 | + return True |
277 | + |
278 | + def render (self, cr, width, height, angle, number): |
279 | + # clear context |
280 | + cr.set_operator (cairo.OPERATOR_CLEAR) |
281 | + cr.paint () |
282 | + |
283 | + if self.secondsf >= float (self.number): |
284 | + self.window.set_opacity (0.0) |
285 | + self.window.hide () |
286 | + return |
287 | + |
288 | + cr.set_operator (cairo.OPERATOR_SOURCE) |
289 | + |
290 | + # "drop-shadow" with blurrrrrrr!!! |
291 | + if (self.dropShadow): |
292 | + cr.set_source_surface (self.dropShadow, 0.0, 0.0) |
293 | + cr.paint () |
294 | + |
295 | + cr.save () |
296 | + |
297 | + cr.set_operator (cairo.OPERATOR_SOURCE) |
298 | + cr.scale (width, height) |
299 | + |
300 | + # draw "tinted" area |
301 | + cr.set_source_rgba (0.65, 0.65, 0.65, 0.9) |
302 | + cr.move_to (0.5, 0.5) |
303 | + fillRadian = -math.pi / 180.0 * (angle + 90.0); |
304 | + cr.arc (0.5, 0.5, 0.40375, math.pi / 180.0 * 270.0, fillRadian) |
305 | + cr.close_path () |
306 | + cr.fill () |
307 | + |
308 | + # draw black "grid" |
309 | + cr.set_line_join (cairo.LINE_JOIN_ROUND) |
310 | + cr.set_line_cap (cairo.LINE_CAP_BUTT) |
311 | + cr.set_line_width (0.0025) |
312 | + cr.set_source_rgb (0.0, 0.0, 0.0) |
313 | + cr.move_to (0.5 - 0.40375, 0.5) |
314 | + cr.rel_line_to (2.0 * 0.40375, 0.0) |
315 | + cr.move_to (0.5, 0.5 - 0.40375) |
316 | + cr.rel_line_to (0.0, 2.0 * 0.40375) |
317 | + cr.stroke () |
318 | + |
319 | + # draw two thick black lines |
320 | + strokeRadian = math.pi / 180.0 * (angle - 180.0) |
321 | + x = math.sin (strokeRadian) * 0.40375 |
322 | + y = math.cos (strokeRadian) * 0.40375 |
323 | + cr.set_line_width (0.0125) |
324 | + cr.move_to (0.5, 0.5 - 0.40375) |
325 | + cr.line_to (0.5, 0.5) |
326 | + cr.rel_line_to (x, y) |
327 | + cr.stroke () |
328 | + |
329 | + # draw two white circles |
330 | + cr.set_source_rgb (1.0, 1.0, 1.0) |
331 | + cr.set_line_width (0.0085) |
332 | + cr.arc (0.5, 0.5, 0.4, 0.0, 2 * math.pi) |
333 | + cr.stroke () |
334 | + cr.arc (0.5, 0.5, 0.35, 0.0, 2 * math.pi) |
335 | + cr.stroke () |
336 | + |
337 | + cr.restore () |
338 | + self.drawNumber (cr, width, height, number) |
339 | + |
340 | + def run (self, counter): |
341 | + self.number = counter |
342 | + if self.show_window: |
343 | + self.window.show_all () |
344 | + self.timeoutId = GLib.timeout_add (1000 / FPS, self.onTimeout, self.window) |
345 | + self.starttime = time.time () |
346 | + |
347 | + def cancel_countdown (self): |
348 | self.indicator.blink_set_state(BLINK_STOP) |
349 | self.canceled = True |
350 | - self.window.destroy() |
351 | - self.number = 0 |
352 | + GLib.source_remove (self.timeoutId) |
353 | + self.window.destroy () |
354 | |
355 | - def counter_finished(self): |
356 | + def counter_finished (self): |
357 | self.emit("counter-finished") |
358 | + self.window.destroy () |
359 | return False |
360 | |
361 | - def cb_draw(self, widget, cr): |
362 | - w = self.width |
363 | - h = self.height |
364 | - background = cairo.ImageSurface.create_from_png(os.path.join(prefs.datadir, "icons", "counter", "cb-{0}.png".format(int(self.number)))) |
365 | - cr.set_source_rgba(0.0, 0.0, 0.0, 0.45) |
366 | - cr.set_operator(cairo.OPERATOR_SOURCE) |
367 | - cr.paint() |
368 | - cr.set_source_surface(background, 0, 0) |
369 | - cr.paint() |
370 | + def onDraw (self, widget, cr): |
371 | + self.secondsf = time.time () - self.starttime |
372 | + seconds = math.trunc (self.secondsf) % self.number |
373 | + angle = (math.trunc (self.secondsf) - self.secondsf) * 360.0 |
374 | + self.render (cr, widget.get_allocated_width (), widget.get_allocated_height (), angle, self.number - seconds) |
375 | + |
376 | + if seconds < 5.0: |
377 | + self.indicator.blink_set_state(BLINK_FAST) |
378 | + |
379 | + if self.number - seconds == 1: |
380 | + widget.set_opacity (1.0 + (math.trunc (self.secondsf) - self.secondsf)) |
381 | + else: |
382 | + widget.set_opacity (1.0) |
383 | + |
384 | + return True |
Merged into unstable. I also added a check of pycairo 1.10.1 is present, if not, the fallback is do skip the background blur and shadow. PPA will provide pycairo 1.10.1.