Merge lp:~macslow/kazam/old-school-countdown-effect into lp:kazam

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
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://www.youtube.com/watch?v=94eIiNdRxAk

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 :

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.

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

Subscribers

People subscribed via source and target branches

to all changes: