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