| 1 | ## src/systraywin32.py |
|---|
| 2 | ## |
|---|
| 3 | ## Contributors for this file: |
|---|
| 4 | ## - Yann Le Boulanger <asterix@lagaule.org> |
|---|
| 5 | ## - Nikos Kouremenos <kourem@gmail.com> |
|---|
| 6 | ## - Dimitur Kirov <dkirov@gmail.com> |
|---|
| 7 | ## |
|---|
| 8 | ## code initially based on |
|---|
| 9 | ## http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/334779 |
|---|
| 10 | ## with some ideas/help from pysystray.sf.net |
|---|
| 11 | ## |
|---|
| 12 | ## Copyright (C) 2003-2004 Yann Le Boulanger <asterix@lagaule.org> |
|---|
| 13 | ## Vincent Hanquez <tab@snarc.org> |
|---|
| 14 | ## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org> |
|---|
| 15 | ## Vincent Hanquez <tab@snarc.org> |
|---|
| 16 | ## Nikos Kouremenos <nkour@jabber.org> |
|---|
| 17 | ## Dimitur Kirov <dkirov@gmail.com> |
|---|
| 18 | ## Travis Shirk <travis@pobox.com> |
|---|
| 19 | ## Norman Rasmussen <norman@rasmussen.co.za> |
|---|
| 20 | ## |
|---|
| 21 | ## This program is free software; you can redistribute it and/or modify |
|---|
| 22 | ## it under the terms of the GNU General Public License as published |
|---|
| 23 | ## by the Free Software Foundation; version 2 only. |
|---|
| 24 | ## |
|---|
| 25 | ## This program is distributed in the hope that it will be useful, |
|---|
| 26 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of |
|---|
| 27 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|---|
| 28 | ## GNU General Public License for more details. |
|---|
| 29 | ## |
|---|
| 30 | |
|---|
| 31 | |
|---|
| 32 | import win32gui |
|---|
| 33 | import pywintypes |
|---|
| 34 | import win32con # winapi constants |
|---|
| 35 | import systray |
|---|
| 36 | import gtk |
|---|
| 37 | import os |
|---|
| 38 | |
|---|
| 39 | WM_TASKBARCREATED = win32gui.RegisterWindowMessage('TaskbarCreated') |
|---|
| 40 | WM_TRAYMESSAGE = win32con.WM_USER + 20 |
|---|
| 41 | |
|---|
| 42 | from common import gajim |
|---|
| 43 | from common import i18n |
|---|
| 44 | _ = i18n._ |
|---|
| 45 | APP = i18n.APP |
|---|
| 46 | gtk.glade.bindtextdomain(APP, i18n.DIR) |
|---|
| 47 | gtk.glade.textdomain(APP) |
|---|
| 48 | |
|---|
| 49 | GTKGUI_GLADE = 'gtkgui.glade' |
|---|
| 50 | |
|---|
| 51 | class SystrayWINAPI: |
|---|
| 52 | def __init__(self, gtk_window): |
|---|
| 53 | self._window = gtk_window |
|---|
| 54 | self._hwnd = gtk_window.window.handle |
|---|
| 55 | self._message_map = {} |
|---|
| 56 | |
|---|
| 57 | self.notify_icon = None |
|---|
| 58 | |
|---|
| 59 | # Sublass the window and inject a WNDPROC to process messages. |
|---|
| 60 | self._oldwndproc = win32gui.SetWindowLong(self._hwnd, |
|---|
| 61 | win32con.GWL_WNDPROC, self._wndproc) |
|---|
| 62 | |
|---|
| 63 | |
|---|
| 64 | def add_notify_icon(self, menu, hicon=None, tooltip=None): |
|---|
| 65 | """ Creates a notify icon for the gtk window. """ |
|---|
| 66 | if not self.notify_icon: |
|---|
| 67 | if not hicon: |
|---|
| 68 | hicon = win32gui.LoadIcon(0, win32con.IDI_APPLICATION) |
|---|
| 69 | self.notify_icon = NotifyIcon(self._hwnd, hicon, tooltip) |
|---|
| 70 | |
|---|
| 71 | # Makes redraw if the taskbar is restarted. |
|---|
| 72 | self.message_map({WM_TASKBARCREATED: self.notify_icon._redraw}) |
|---|
| 73 | |
|---|
| 74 | |
|---|
| 75 | def message_map(self, msg_map={}): |
|---|
| 76 | """ Maps message processing to callback functions ala win32gui. """ |
|---|
| 77 | if msg_map: |
|---|
| 78 | if self._message_map: |
|---|
| 79 | duplicatekeys = [key for key in msg_map.keys() |
|---|
| 80 | if self._message_map.has_key(key)] |
|---|
| 81 | |
|---|
| 82 | for key in duplicatekeys: |
|---|
| 83 | new_value = msg_map[key] |
|---|
| 84 | |
|---|
| 85 | if isinstance(new_value, list): |
|---|
| 86 | raise TypeError('Dict cannot have list values') |
|---|
| 87 | |
|---|
| 88 | value = self._message_map[key] |
|---|
| 89 | |
|---|
| 90 | if new_value != value: |
|---|
| 91 | new_value = [new_value] |
|---|
| 92 | |
|---|
| 93 | if isinstance(value, list): |
|---|
| 94 | value += new_value |
|---|
| 95 | else: |
|---|
| 96 | value = [value] + new_value |
|---|
| 97 | |
|---|
| 98 | msg_map[key] = value |
|---|
| 99 | self._message_map.update(msg_map) |
|---|
| 100 | |
|---|
| 101 | def message_unmap(self, msg, callback=None): |
|---|
| 102 | if self._message_map.has_key(msg): |
|---|
| 103 | if callback: |
|---|
| 104 | cblist = self._message_map[key] |
|---|
| 105 | if isinstance(cblist, list): |
|---|
| 106 | if not len(cblist) < 2: |
|---|
| 107 | for i in xrange(len(cblist)): |
|---|
| 108 | if cblist[i] == callback: |
|---|
| 109 | del self._message_map[key][i] |
|---|
| 110 | return |
|---|
| 111 | del self._message_map[key] |
|---|
| 112 | |
|---|
| 113 | def remove_notify_icon(self): |
|---|
| 114 | """ Removes the notify icon. """ |
|---|
| 115 | if self.notify_icon: |
|---|
| 116 | self.notify_icon.remove() |
|---|
| 117 | self.notify_icon = None |
|---|
| 118 | |
|---|
| 119 | def remove(self, *args): |
|---|
| 120 | """ Unloads the extensions. """ |
|---|
| 121 | self._message_map = {} |
|---|
| 122 | self.remove_notify_icon() |
|---|
| 123 | self = None |
|---|
| 124 | |
|---|
| 125 | def show_balloon_tooltip(self, title, text, timeout=10, |
|---|
| 126 | icon=win32gui.NIIF_NONE): |
|---|
| 127 | """ Shows a baloon tooltip. """ |
|---|
| 128 | if not self.notify_icon: |
|---|
| 129 | self.add_notifyicon() |
|---|
| 130 | self.notify_icon.show_balloon(title, text, timeout, icon) |
|---|
| 131 | |
|---|
| 132 | def _wndproc(self, hwnd, msg, wparam, lparam): |
|---|
| 133 | """ A WINDPROC to process window messages. """ |
|---|
| 134 | if self._message_map.has_key(msg): |
|---|
| 135 | callback = self._message_map[msg] |
|---|
| 136 | if isinstance(callback, list): |
|---|
| 137 | for cb in callback: |
|---|
| 138 | cb(hwnd, msg, wparam, lparam) |
|---|
| 139 | else: |
|---|
| 140 | callback(hwnd, msg, wparam, lparam) |
|---|
| 141 | |
|---|
| 142 | return win32gui.CallWindowProc(self._oldwndproc, hwnd, msg, wparam, |
|---|
| 143 | lparam) |
|---|
| 144 | |
|---|
| 145 | |
|---|
| 146 | class NotifyIcon: |
|---|
| 147 | |
|---|
| 148 | def __init__(self, hwnd, hicon, tooltip=None): |
|---|
| 149 | self._hwnd = hwnd |
|---|
| 150 | self._id = 0 |
|---|
| 151 | self._flags = win32gui.NIF_MESSAGE | win32gui.NIF_ICON |
|---|
| 152 | self._callbackmessage = WM_TRAYMESSAGE |
|---|
| 153 | self._hicon = hicon |
|---|
| 154 | |
|---|
| 155 | try: |
|---|
| 156 | win32gui.Shell_NotifyIcon(win32gui.NIM_ADD, self._get_nid()) |
|---|
| 157 | except pywintypes.error: |
|---|
| 158 | pass |
|---|
| 159 | if tooltip: self.set_tooltip(tooltip) |
|---|
| 160 | |
|---|
| 161 | |
|---|
| 162 | def _get_nid(self): |
|---|
| 163 | """ Function to initialise & retrieve the NOTIFYICONDATA Structure. """ |
|---|
| 164 | nid = [self._hwnd, self._id, self._flags, self._callbackmessage, self._hicon] |
|---|
| 165 | |
|---|
| 166 | if not hasattr(self, '_tip'): self._tip = '' |
|---|
| 167 | nid.append(self._tip) |
|---|
| 168 | |
|---|
| 169 | if not hasattr(self, '_info'): self._info = '' |
|---|
| 170 | nid.append(self._info) |
|---|
| 171 | |
|---|
| 172 | if not hasattr(self, '_timeout'): self._timeout = 0 |
|---|
| 173 | nid.append(self._timeout) |
|---|
| 174 | |
|---|
| 175 | if not hasattr(self, '_infotitle'): self._infotitle = '' |
|---|
| 176 | nid.append(self._infotitle) |
|---|
| 177 | |
|---|
| 178 | if not hasattr(self, '_infoflags'):self._infoflags = win32gui.NIIF_NONE |
|---|
| 179 | nid.append(self._infoflags) |
|---|
| 180 | |
|---|
| 181 | return tuple(nid) |
|---|
| 182 | |
|---|
| 183 | def remove(self): |
|---|
| 184 | """ Removes the tray icon. """ |
|---|
| 185 | try: |
|---|
| 186 | win32gui.Shell_NotifyIcon(win32gui.NIM_DELETE, self._get_nid()) |
|---|
| 187 | except pywintypes.error: |
|---|
| 188 | pass |
|---|
| 189 | |
|---|
| 190 | |
|---|
| 191 | def set_tooltip(self, tooltip): |
|---|
| 192 | """ Sets the tray icon tooltip. """ |
|---|
| 193 | self._flags = self._flags | win32gui.NIF_TIP |
|---|
| 194 | self._tip = tooltip |
|---|
| 195 | try: |
|---|
| 196 | win32gui.Shell_NotifyIcon(win32gui.NIM_MODIFY, self._get_nid()) |
|---|
| 197 | except pywintypes.error: |
|---|
| 198 | pass |
|---|
| 199 | |
|---|
| 200 | |
|---|
| 201 | def show_balloon(self, title, text, timeout=10, icon=win32gui.NIIF_NONE): |
|---|
| 202 | """ Shows a balloon tooltip from the tray icon. """ |
|---|
| 203 | self._flags = self._flags | win32gui.NIF_INFO |
|---|
| 204 | self._infotitle = title |
|---|
| 205 | self._info = text |
|---|
| 206 | self._timeout = timeout * 1000 |
|---|
| 207 | self._infoflags = icon |
|---|
| 208 | try: |
|---|
| 209 | win32gui.Shell_NotifyIcon(win32gui.NIM_MODIFY, self._get_nid()) |
|---|
| 210 | except pywintypes.error: |
|---|
| 211 | pass |
|---|
| 212 | |
|---|
| 213 | def _redraw(self, *args): |
|---|
| 214 | """ Redraws the tray icon. """ |
|---|
| 215 | self.remove() |
|---|
| 216 | try: |
|---|
| 217 | win32gui.Shell_NotifyIcon(win32gui.NIM_ADD, self._get_nid()) |
|---|
| 218 | except pywintypes.error: |
|---|
| 219 | pass |
|---|
| 220 | |
|---|
| 221 | |
|---|
| 222 | class SystrayWin32(systray.Systray): |
|---|
| 223 | def __init__(self): |
|---|
| 224 | # Note: gtk window must be realized before installing extensions. |
|---|
| 225 | systray.Systray.__init__(self) |
|---|
| 226 | self.jids = [] |
|---|
| 227 | self.status = 'offline' |
|---|
| 228 | self.xml = gtk.glade.XML(GTKGUI_GLADE, 'systray_context_menu', APP) |
|---|
| 229 | self.systray_context_menu = self.xml.get_widget('systray_context_menu') |
|---|
| 230 | self.added_hide_menuitem = False |
|---|
| 231 | |
|---|
| 232 | # self.tray_ico_imgs = self.load_icos() |
|---|
| 233 | |
|---|
| 234 | w = gtk.Window() # just a window to pass |
|---|
| 235 | w.realize() # realize it so gtk window exists |
|---|
| 236 | self.systray_winapi = SystrayWINAPI(w) |
|---|
| 237 | |
|---|
| 238 | self.xml.signal_autoconnect(self) |
|---|
| 239 | |
|---|
| 240 | # Set up the callback messages |
|---|
| 241 | self.systray_winapi.message_map({ |
|---|
| 242 | WM_TRAYMESSAGE: self.on_clicked |
|---|
| 243 | }) |
|---|
| 244 | |
|---|
| 245 | def show_icon(self): |
|---|
| 246 | #self.systray_winapi.add_notify_icon(self.systray_context_menu, tooltip = 'Gajim') |
|---|
| 247 | #self.systray_winapi.notify_icon.menu = self.systray_context_menu |
|---|
| 248 | # do not remove set_img does both above. |
|---|
| 249 | # maybe I can only change img without readding |
|---|
| 250 | # the notify icon? HOW?? |
|---|
| 251 | self.set_img() |
|---|
| 252 | |
|---|
| 253 | def hide_icon(self): |
|---|
| 254 | self.systray_winapi.remove() |
|---|
| 255 | |
|---|
| 256 | def on_clicked(self, hwnd, message, wparam, lparam): |
|---|
| 257 | if lparam == win32con.WM_RBUTTONUP: # Right click |
|---|
| 258 | self.make_menu() |
|---|
| 259 | self.systray_winapi.notify_icon.menu.popup(None, None, None, 0, 0) |
|---|
| 260 | elif lparam == win32con.WM_MBUTTONUP: # Middle click |
|---|
| 261 | self.on_middle_click() |
|---|
| 262 | elif lparam == win32con.WM_LBUTTONUP: # Left click |
|---|
| 263 | self.on_left_click() |
|---|
| 264 | |
|---|
| 265 | def add_jid(self, jid, account, typ): |
|---|
| 266 | systray.Systray.add_jid(self, jid, account, typ) |
|---|
| 267 | |
|---|
| 268 | nb = gajim.interface.roster.nb_unread |
|---|
| 269 | for acct in gajim.connections: |
|---|
| 270 | # in chat / groupchat windows |
|---|
| 271 | for kind in ('chats', 'gc'): |
|---|
| 272 | jids = gajim.interface.instances[acct][kind] |
|---|
| 273 | for jid in jids: |
|---|
| 274 | if jid != 'tabbed': |
|---|
| 275 | nb += jids[jid].nb_unread[jid] |
|---|
| 276 | |
|---|
| 277 | text = i18n.ngettext( |
|---|
| 278 | 'Gajim - %d unread message', |
|---|
| 279 | 'Gajim - %d unread messages', |
|---|
| 280 | nb, nb, nb) |
|---|
| 281 | |
|---|
| 282 | self.systray_winapi.notify_icon.set_tooltip(text) |
|---|
| 283 | |
|---|
| 284 | def remove_jid(self, jid, account, typ): |
|---|
| 285 | systray.Systray.remove_jid(self, jid, account, typ) |
|---|
| 286 | |
|---|
| 287 | nb = gajim.interface.roster.nb_unread |
|---|
| 288 | for acct in gajim.connections: |
|---|
| 289 | # in chat / groupchat windows |
|---|
| 290 | for kind in ('chats', 'gc'): |
|---|
| 291 | for jid in gajim.interface.instances[acct][kind]: |
|---|
| 292 | if jid != 'tabbed': |
|---|
| 293 | nb += gajim.interface.instances[acct][kind][jid].nb_unread[jid] |
|---|
| 294 | |
|---|
| 295 | if nb > 0: |
|---|
| 296 | text = i18n.ngettext( |
|---|
| 297 | 'Gajim - %d unread message', |
|---|
| 298 | 'Gajim - %d unread messages', |
|---|
| 299 | nb, nb, nb) |
|---|
| 300 | else: |
|---|
| 301 | text = 'Gajim' |
|---|
| 302 | self.systray_winapi.notify_icon.set_tooltip(text) |
|---|
| 303 | |
|---|
| 304 | def set_img(self): |
|---|
| 305 | self.tray_ico_imgs = self.load_icos() #FIXME: do not do this here |
|---|
| 306 | # see gajim.interface.roster.reload_jabber_state_images() to merge |
|---|
| 307 | self.systray_winapi.remove_notify_icon() |
|---|
| 308 | if len(self.jids) > 0: |
|---|
| 309 | state = 'message' |
|---|
| 310 | else: |
|---|
| 311 | state = self.status |
|---|
| 312 | hicon = self.tray_ico_imgs[state] |
|---|
| 313 | |
|---|
| 314 | self.systray_winapi.add_notify_icon(self.systray_context_menu, hicon, |
|---|
| 315 | 'Gajim') |
|---|
| 316 | self.systray_winapi.notify_icon.menu = self.systray_context_menu |
|---|
| 317 | |
|---|
| 318 | def load_icos(self): |
|---|
| 319 | '''load .ico files and return them to a dic of SHOW --> img_obj''' |
|---|
| 320 | iconset = str(gajim.config.get('iconset')) |
|---|
| 321 | if not iconset: |
|---|
| 322 | iconset = 'dcraven' |
|---|
| 323 | |
|---|
| 324 | imgs = {} |
|---|
| 325 | path = os.path.join(gajim.DATA_DIR, 'iconsets', iconset, '16x16', 'icos') |
|---|
| 326 | # icon folder for missing icons |
|---|
| 327 | path_dcraven_iconset = os.path.join(gajim.DATA_DIR, 'iconsets', 'dcraven', |
|---|
| 328 | '16x16', 'icos') |
|---|
| 329 | states_list = gajim.SHOW_LIST |
|---|
| 330 | # trayicon apart from show holds message state too |
|---|
| 331 | states_list.append('message') |
|---|
| 332 | for state in states_list: |
|---|
| 333 | path_to_ico = os.path.join(path, state + '.ico') |
|---|
| 334 | if not os.path.isfile(path_to_ico): # fallback to dcraven iconset |
|---|
| 335 | path_to_ico = os.path.join(path_dcraven_iconset, state + '.ico') |
|---|
| 336 | if os.path.exists(path_to_ico): |
|---|
| 337 | hinst = win32gui.GetModuleHandle(None) |
|---|
| 338 | img_flags = win32con.LR_LOADFROMFILE | win32con.LR_DEFAULTSIZE |
|---|
| 339 | image = win32gui.LoadImage(hinst, path_to_ico, win32con.IMAGE_ICON, |
|---|
| 340 | 0, 0, img_flags) |
|---|
| 341 | imgs[state] = image |
|---|
| 342 | |
|---|
| 343 | return imgs |
|---|