Coverage for /home/antoine/projects/xpra-git/dist/python3/lib64/python/xpra/gtk_common/gtk_notifier.py : 13%
Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#!/usr/bin/env python
2#
3#gtkPopupNotify.py
4#
5# Copyright 2009 Daniel Woodhouse
6# Copyright 2013-2018 Antoine Martin <antoine@xpra.org>
7#
8#This program is free software: you can redistribute it and/or modify
9#it under the terms of the GNU Lesser General Public License as published by
10#the Free Software Foundation, either version 3 of the License, or
11#(at your option) any later version.
12#
13#This program is distributed in the hope that it will be useful,
14#but WITHOUT ANY WARRANTY; without even the implied warranty of
15#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16#GNU Lesser General Public License for more details.
17#
18#You should have received a copy of the GNU Lesser General Public License
19#along with this program. If not, see <http://www.gnu.org/licenses/>.
21import os
22from gi.repository import GLib, Gtk, Gdk, GdkPixbuf
24from xpra.os_util import OSX, bytestostr
25from xpra.gtk_common.gtk_util import (
26 add_close_accel, color_parse,
27 )
28from xpra.notifications.notifier_base import NotifierBase, log
30DEFAULT_FG_COLOUR = None
31DEFAULT_BG_COLOUR = None
32if OSX:
33 #black on white fits better with osx
34 DEFAULT_FG_COLOUR = color_parse("black")
35 DEFAULT_BG_COLOUR = color_parse("#f2f2f2")
36DEFAULT_WIDTH = 340
37DEFAULT_HEIGHT = 100
41def get_pixbuf(icon_name):
42 from xpra.platform.paths import get_icon_dir
43 icon_filename = os.path.join(get_icon_dir(), icon_name)
44 if os.path.exists(icon_filename):
45 return GdkPixbuf.Pixbuf.new_from_file(icon_filename)
46 return None
48class GTK_Notifier(NotifierBase):
50 def __init__(self, closed_cb=None, action_cb=None, size_x=DEFAULT_WIDTH, size_y=DEFAULT_HEIGHT, timeout=5):
51 super().__init__(closed_cb, action_cb)
52 self.handles_actions = True
53 """
54 Create a new notification stack. The recommended way to create Popup instances.
55 Parameters:
56 `size_x` : The desired width of the notifications.
57 `size_y` : The desired minimum height of the notifications. If the text is
58 longer it will be expanded to fit.
59 `timeout` : Popup instance will disappear after this timeout if there
60 is no human intervention. This can be overridden temporarily by passing
61 a new timout to the new_popup method.
62 """
63 self.size_x = size_x
64 self.size_y = size_y
65 self.timeout = timeout
66 """
67 Other parameters:
68 These will take effect for every popup created after the change.
69 `max_popups` : The maximum number of popups to be shown on the screen
70 at one time.
71 `bg_color` : if None default is used (usually grey). set with a gdk.Color.
72 `fg_color` : if None default is used (usually black). set with a gdk.Color.
73 `show_timeout : if True, a countdown till destruction will be displayed.
75 """
76 self.max_popups = 5
77 self.fg_color = DEFAULT_FG_COLOUR
78 self.bg_color = DEFAULT_BG_COLOUR
79 self.show_timeout = False
81 self._notify_stack = []
82 self._offset = 0
84 display = Gdk.Display.get_default()
85 screen = display.get_default_screen()
86 n = screen.get_n_monitors()
87 log("screen=%s, monitors=%s", screen, n)
88 if n<2:
89 self.max_width = screen.get_width()
90 self.max_height = screen.get_height()
91 log("screen dimensions: %dx%d", self.max_width, self.max_height)
92 else:
93 rect = screen.get_monitor_geometry(0)
94 self.max_width = rect.width
95 self.max_height = rect.height
96 log("first monitor dimensions: %dx%d", self.max_width, self.max_height)
97 self.x = self.max_width - 20 #keep away from the edge
98 self.y = self.max_height - 64 #space for a panel
99 log("our reduced dimensions: %dx%d", self.x, self.y)
101 def cleanup(self):
102 popups = tuple(self._notify_stack)
103 self._notify_stack = []
104 for x in popups:
105 x.hide_notification()
106 NotifierBase.cleanup(self)
109 def get_origin_x(self):
110 return self.x
112 def get_origin_y(self):
113 return self.y
116 def close_notify(self, nid):
117 for x in self._notify_stack:
118 if x.nid==nid:
119 x.hide_notification()
121 def show_notify(self, dbus_id, tray, nid,
122 app_name, replaces_nid, app_icon,
123 summary, body, actions, hints, timeout, icon):
124 self.new_popup(nid, summary, body, actions, icon, timeout, 0<timeout<=600)
126 def new_popup(self, nid, summary, body, actions, icon, timeout=10*1000, show_timeout=False):
127 """Create a new Popup instance."""
128 if len(self._notify_stack) == self.max_popups:
129 oldest = self._notify_stack[0]
130 oldest.hide_notification()
131 self.popup_closed(oldest.nid, 4)
132 image = None
133 if icon and icon[0]=="png":
134 img_data = icon[3]
135 loader = GdkPixbuf.PixbufLoader()
136 loader.write(img_data)
137 loader.close()
138 image = loader.get_pixbuf()
139 popup = Popup(self, nid, summary, body, actions, image=image, timeout=timeout//1000, show_timeout=show_timeout)
140 self._notify_stack.append(popup)
141 self._offset += self._notify_stack[-1].h
142 return popup
144 def destroy_popup_cb(self, popup):
145 if popup in self._notify_stack:
146 self._notify_stack.remove(popup)
147 #move popups down if required
148 offset = 0
149 for note in self._notify_stack:
150 offset = note.reposition(offset, self)
151 self._offset = offset
153 def popup_closed(self, nid, reason, text=""):
154 if self.closed_cb:
155 self.closed_cb(nid, reason, text)
157 def popup_action(self, nid, action_id):
158 if self.action_cb:
159 self.action_cb(nid, action_id)
163class Popup(Gtk.Window):
164 def __init__(self, stack, nid, title, message, actions, image, timeout=5, show_timeout=False):
165 log("Popup%s", (stack, nid, title, message, actions, image, timeout, show_timeout))
166 self.stack = stack
167 self.nid = nid
168 Gtk.Window.__init__(self)
170 self.set_accept_focus(False)
171 self.set_focus_on_map(False)
172 self.set_size_request(stack.size_x, -1)
173 self.set_decorated(False)
174 self.set_deletable(False)
175 self.set_property("skip-pager-hint", True)
176 self.set_property("skip-taskbar-hint", True)
177 self.connect("enter-notify-event", self.on_hover, True)
178 self.connect("leave-notify-event", self.on_hover, False)
179 self.set_opacity(0.2)
180 self.set_keep_above(True)
181 self.destroy_cb = stack.destroy_popup_cb
182 self.popup_closed = stack.popup_closed
183 self.action_cb = stack.popup_action
185 main_box = Gtk.VBox()
186 header_box = Gtk.HBox()
187 self.header = Gtk.Label()
188 self.header.set_markup("<b>%s</b>" % title)
189 self.header.set_padding(3, 3)
190 self.header.set_alignment(0, 0)
191 header_box.pack_start(self.header, True, True, 5)
192 icon = get_pixbuf("close.png")
193 if icon:
194 close_button = Gtk.Image()
195 close_button.set_from_pixbuf(icon)
196 close_button.set_padding(3, 3)
197 close_window = Gtk.EventBox()
198 close_window.set_visible_window(False)
199 close_window.connect("button-press-event", self.user_closed)
200 close_window.add(close_button)
201 close_window.set_size_request(icon.get_width(), icon.get_height())
202 header_box.pack_end(close_window, False, False, 0)
203 main_box.pack_start(header_box)
205 body_box = Gtk.HBox()
206 if image is not None:
207 self.image = Gtk.Image()
208 self.image.set_size_request(70, 70)
209 self.image.set_alignment(0, 0)
210 self.image.set_from_pixbuf(image)
211 body_box.pack_start(self.image, False, False, 5)
212 self.message = Gtk.Label()
213 self.message.set_max_width_chars(80)
214 self.message.set_size_request(stack.size_x - 90, -1)
215 self.message.set_line_wrap(True)
216 self.message.set_alignment(0, 0)
217 self.message.set_padding(5, 10)
218 self.message.set_text(message)
219 self.counter = Gtk.Label()
220 self.counter.set_alignment(1, 1)
221 self.counter.set_padding(3, 3)
222 self.timeout = timeout
224 body_box.pack_start(self.message, True, False, 5)
225 body_box.pack_end(self.counter, False, False, 5)
226 main_box.pack_start(body_box, False, False, 5)
228 if len(actions)>=2:
229 buttons_box = Gtk.HBox(True)
230 while len(actions)>=2:
231 action_id, action_text = actions[:2]
232 actions = actions[2:]
233 button = self.action_button(action_id, action_text)
234 buttons_box.add(button)
235 alignment = Gtk.Alignment(xalign=1.0, yalign=0.5, xscale=0.0, yscale=0.0)
236 alignment.add(buttons_box)
237 main_box.pack_start(alignment)
238 self.add(main_box)
239 if stack.bg_color is not None:
240 self.modify_bg(Gtk.StateType.NORMAL, stack.bg_color)
241 if stack.fg_color is not None:
242 self.message.modify_fg(Gtk.StateType.NORMAL, stack.fg_color)
243 self.header.modify_fg(Gtk.StateType.NORMAL, stack.fg_color)
244 self.counter.modify_fg(Gtk.StateType.NORMAL, stack.fg_color)
245 self.show_timeout = show_timeout
246 self.hover = False
247 self.show_all()
248 self.w = self.get_preferred_width()[0]
249 self.h = self.get_preferred_height()[0]
250 self.move(self.get_x(self.w), self.get_y(self.h))
251 self.wait_timer = None
252 self.fade_out_timer = None
253 self.fade_in_timer = GLib.timeout_add(100, self.fade_in)
254 #ensure we dont show it in the taskbar:
255 self.realize()
256 self.get_window().set_skip_taskbar_hint(True)
257 self.get_window().set_skip_pager_hint(True)
258 add_close_accel(self, self.user_closed)
260 def action_button(self, action_id, action_text):
261 try:
262 button = Gtk.Button(action_text.decode("utf-8"))
263 except Exception:
264 button = Gtk.Button(bytestostr(action_text))
265 button.set_relief(Gtk.ReliefStyle.NORMAL)
266 def popup_cb_clicked(*args):
267 self.hide_notification()
268 log("popup_cb_clicked%s for action_id=%s, action_text=%s", args, action_id, action_text)
269 self.action_cb(self.nid, action_id)
270 button.connect("clicked", popup_cb_clicked)
271 return button
273 def get_x(self, w):
274 x = self.stack.get_origin_x() - w//2
275 if (x + w) >= self.stack.max_width: #dont overflow on the right
276 x = self.stack.max_width - w
277 if x <= 0: #or on the left
278 x = 0
279 log("get_x(%s)=%s", w, x)
280 return x
282 def get_y(self, h):
283 y = self.stack.get_origin_y()
284 if y >= (self.stack.max_height//2): #if near bottom, substract window height
285 y = y - h
286 if (y + h) >= self.stack.max_height:
287 y = self.stack.max_height - h
288 if y<= 0:
289 y = 0
290 log("get_y(%s)=%s", h, y)
291 return y
293 def reposition(self, offset, stack):
294 """Move the notification window down, when an older notification is removed"""
295 log("reposition(%s, %s)", offset, stack)
296 new_offset = self.h + offset
297 self.move(self.get_x(self.w), self.get_y(new_offset))
298 return new_offset
300 def fade_in(self):
301 opacity = self.get_opacity()
302 opacity += 0.15
303 if opacity >= 1:
304 self.wait_timer = GLib.timeout_add(1000, self.wait)
305 self.fade_in_timer = None
306 return False
307 self.set_opacity(opacity)
308 return True
310 def wait(self):
311 if not self.hover:
312 self.timeout -= 1
313 if self.show_timeout:
314 self.counter.set_markup(str("<b>%s</b>" % max(0, self.timeout)))
315 if self.timeout <= 0:
316 self.fade_out_timer = GLib.timeout_add(100, self.fade_out)
317 self.wait_timer = None
318 return False
319 return True
321 def fade_out(self):
322 opacity = self.get_opacity()
323 opacity -= 0.10
324 if opacity <= 0:
325 self.in_progress = False
326 self.hide_notification()
327 self.fade_out_timer = None #redundant
328 self.popup_closed(self.nid, 1)
329 return False
330 self.set_opacity(opacity)
331 return True
333 def on_hover(self, _window, _event, hover):
334 """Starts/Stops the notification timer on a mouse in/out event"""
335 self.hover = hover
337 def user_closed(self, *_args):
338 self.hide_notification()
339 self.popup_closed(self.nid, 2)
341 def hide_notification(self):
342 """Destroys the notification and tells the stack to move the
343 remaining notification windows"""
344 log("hide_notification()")
345 for timer in ("fade_in_timer", "fade_out_timer", "wait_timer"):
346 v = getattr(self, timer)
347 if v:
348 setattr(self, timer, None)
349 GLib.source_remove(v)
350 self.destroy()
351 self.destroy_cb(self)
355def main():
356 #example usage
357 import random
358 color_combos = (("red", "white"), ("white", "blue"), ("green", "black"))
359 messages = (("Hello", "This is a popup"),
360 ("Some Latin", "Quidquid latine dictum sit, altum sonatur."),
361 ("A long message", "The quick brown fox jumped over the lazy dog. " * 6))
362 #images = ("logo1_64.png", None)
363 def notify_factory():
364 color = random.choice(color_combos)
365 title, message = random.choice(messages)
366 icon = None #random.choice(images)
367 notifier.bg_color = color_parse(color[0])
368 notifier.fg_color = color_parse(color[1])
369 notifier.show_timeout = random.choice((True, False))
370 notifier.new_popup(0, title, message, (), icon)
371 return True
372 def gtk_main_quit():
373 print("quitting")
374 Gtk.main_quit()
376 notifier = GTK_Notifier(timeout=6)
377 GLib.timeout_add(4000, notify_factory)
378 GLib.timeout_add(20000, gtk_main_quit)
379 Gtk.main()
381if __name__ == "__main__":
382 main()