Hide keyboard shortcuts

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/>. 

20 

21import os 

22from gi.repository import GLib, Gtk, Gdk, GdkPixbuf 

23 

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 

29 

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 

38 

39 

40 

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 

47 

48class GTK_Notifier(NotifierBase): 

49 

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. 

74 

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 

80 

81 self._notify_stack = [] 

82 self._offset = 0 

83 

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) 

100 

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) 

107 

108 

109 def get_origin_x(self): 

110 return self.x 

111 

112 def get_origin_y(self): 

113 return self.y 

114 

115 

116 def close_notify(self, nid): 

117 for x in self._notify_stack: 

118 if x.nid==nid: 

119 x.hide_notification() 

120 

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) 

125 

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 

143 

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 

152 

153 def popup_closed(self, nid, reason, text=""): 

154 if self.closed_cb: 

155 self.closed_cb(nid, reason, text) 

156 

157 def popup_action(self, nid, action_id): 

158 if self.action_cb: 

159 self.action_cb(nid, action_id) 

160 

161 

162 

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) 

169 

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 

184 

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) 

204 

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 

223 

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) 

227 

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) 

259 

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 

272 

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 

281 

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 

292 

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 

299 

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 

309 

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 

320 

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 

332 

333 def on_hover(self, _window, _event, hover): 

334 """Starts/Stops the notification timer on a mouse in/out event""" 

335 self.hover = hover 

336 

337 def user_closed(self, *_args): 

338 self.hide_notification() 

339 self.popup_closed(self.nid, 2) 

340 

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) 

352 

353 

354 

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() 

375 

376 notifier = GTK_Notifier(timeout=6) 

377 GLib.timeout_add(4000, notify_factory) 

378 GLib.timeout_add(20000, gtk_main_quit) 

379 Gtk.main() 

380 

381if __name__ == "__main__": 

382 main()