7 changed files with 367 additions and 0 deletions
@ -0,0 +1,8 @@ |
|||
On http://www.brunningonline.net/simon/blog/archives/001835.html, |
|||
the author licensed SysTrayIcon.py under a variant of the WTFPL: |
|||
|
|||
> Any road up, help yourself. Consider SysTrayIcon.py to be under an |
|||
> "Aleister Crowley" style license - "Do what thou wilt shall be the |
|||
> only law". |
|||
> |
|||
> Err, but don't sue me if it doesn't work. ;-) |
@ -0,0 +1,114 @@ |
|||
#!/usr/bin/python -OO |
|||
# Copyright 2008-2011 The SABnzbd-Team <team@sabnzbd.org> |
|||
# |
|||
# This program is free software; you can redistribute it and/or |
|||
# modify it under the terms of the GNU General Public License |
|||
# as published by the Free Software Foundation; either version 2 |
|||
# of the License, or (at your option) any later version. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU General Public License for more details. |
|||
# |
|||
# You should have received a copy of the GNU General Public License |
|||
# along with this program; if not, write to the Free Software |
|||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|||
|
|||
|
|||
""" |
|||
sabtray.py - Systray icon for SABnzbd on Windows, contributed by Jan Schejbal |
|||
""" |
|||
|
|||
from systrayiconthread import SysTrayIconThread |
|||
from threading import Thread |
|||
from time import sleep |
|||
|
|||
import sabnzbd |
|||
from sabnzbd.panic import launch_a_browser |
|||
import sabnzbd.api as api |
|||
import sabnzbd.scheduler as scheduler |
|||
from sabnzbd.downloader import Downloader |
|||
import cherrypy |
|||
import win32gui |
|||
|
|||
|
|||
# contains the tray icon, which demands its own thread |
|||
class SABTrayThread(SysTrayIconThread): |
|||
sabpaused = False |
|||
sabicons = { |
|||
'default': 'sabnzbd16.ico', |
|||
'green': 'sabnzbd16green.ico', |
|||
'pause': 'sabnzbd16paused.ico' |
|||
} |
|||
|
|||
|
|||
def __init__(self): |
|||
|
|||
|
|||
text = "SABnzbd" |
|||
|
|||
menu_options = ( |
|||
('Show interface', None, self.browse), |
|||
('Pause/Resume', None, self.pauseresume), |
|||
('Shutdown', None, self.shutdown), |
|||
) |
|||
|
|||
SysTrayIconThread.__init__(self, self.sabicons['default'], text, menu_options, None, 0, "SabTrayIcon") |
|||
|
|||
|
|||
# called every few ms by SysTrayIconThread |
|||
def doUpdates(self): |
|||
status = api.qstatus_data() |
|||
state = status.get('state', "SABnzbd"); |
|||
self.sabpaused = status.get('paused', False); |
|||
|
|||
if state == 'IDLE': |
|||
self.hover_text = 'SABnzbd idle' |
|||
self.icon = self.sabicons['default'] |
|||
elif state == 'PAUSED': |
|||
self.hover_text = 'SABnzbd paused' |
|||
self.icon = self.sabicons['pause'] |
|||
elif state == 'DOWNLOADING': |
|||
self.hover_text = status.get('speed', "---") + "B/s, Remaining: " + status.get('timeleft', "---") + " (" + str(int(status.get('mbleft', "0"))) + " MB)" |
|||
self.icon = self.sabicons['green'] |
|||
else: |
|||
self.hover_text = 'UNKNOWN STATE' |
|||
self.icon = self.sabicons['pause'] |
|||
|
|||
|
|||
self.refresh_icon() |
|||
if sabnzbd.SABSTOP: |
|||
self.terminate = True |
|||
|
|||
# menu handler |
|||
def browse(self, icon): launch_a_browser(sabnzbd.BROWSER_URL, True) |
|||
|
|||
# menu handler |
|||
def pauseresume(self, icon): |
|||
if self.sabpaused: |
|||
self.resume() |
|||
else: |
|||
self.pause() |
|||
|
|||
# menu handler - adapted from interface.py |
|||
def shutdown(self, icon): |
|||
sabnzbd.halt() |
|||
cherrypy.engine.exit() |
|||
sabnzbd.SABSTOP = True |
|||
|
|||
# adapted from interface.py |
|||
def pause(self): |
|||
scheduler.plan_resume(0) |
|||
Downloader.do.pause() |
|||
|
|||
# adapted from interface.py |
|||
def resume(self): |
|||
scheduler.plan_resume(0) |
|||
sabnzbd.unpause_all() |
|||
|
|||
|
|||
|
|||
|
|||
# start the tray |
|||
SABTrayThread() |
@ -0,0 +1,243 @@ |
|||
#!/usr/bin/env python |
|||
# based on SysTrayIcon.py by Simon Brunning - simon@brunningonline.net |
|||
# http://www.brunningonline.net/simon/blog/archives/001835.html |
|||
# http://www.brunningonline.net/simon/blog/archives/SysTrayIcon.py.html |
|||
# modified on 2011-10-04 by Jan Schejbal to support threading and preload icons |
|||
# override doUpdates to perform actions inside the icon thread |
|||
|
|||
import os |
|||
import sys |
|||
|
|||
import win32api |
|||
import win32con |
|||
import win32gui_struct |
|||
try: |
|||
import winxpgui as win32gui |
|||
except ImportError: |
|||
import win32gui |
|||
from threading import Thread |
|||
from time import sleep |
|||
|
|||
class SysTrayIconThread(Thread): |
|||
'''TODO''' |
|||
QUIT = 'QUIT' |
|||
SPECIAL_ACTIONS = [QUIT] |
|||
|
|||
FIRST_ID = 1023 |
|||
terminate = False |
|||
|
|||
def __init__(self, |
|||
icon, |
|||
hover_text, |
|||
menu_options, |
|||
on_quit=None, |
|||
default_menu_index=None, |
|||
window_class_name=None,): |
|||
Thread.__init__(self) |
|||
self.icon = icon |
|||
self.icons = {} |
|||
self.hover_text = hover_text |
|||
self.on_quit = on_quit |
|||
|
|||
# menu_options = menu_options + (('Quit', None, self.QUIT),) |
|||
self._next_action_id = self.FIRST_ID |
|||
self.menu_actions_by_id = set() |
|||
self.menu_options = self._add_ids_to_menu_options(list(menu_options)) |
|||
self.menu_actions_by_id = dict(self.menu_actions_by_id) |
|||
del self._next_action_id |
|||
|
|||
|
|||
self.default_menu_index = (default_menu_index or 0) |
|||
self.window_class_name = window_class_name or "SysTrayIconPy" |
|||
|
|||
self.start() |
|||
|
|||
|
|||
def initialize(self): |
|||
message_map = {win32gui.RegisterWindowMessage("TaskbarCreated"): self.restart, |
|||
win32con.WM_DESTROY: self.destroy, |
|||
win32con.WM_COMMAND: self.command, |
|||
win32con.WM_USER+20 : self.notify,} |
|||
# Register the Window class. |
|||
window_class = win32gui.WNDCLASS() |
|||
hinst = window_class.hInstance = win32gui.GetModuleHandle(None) |
|||
window_class.lpszClassName = self.window_class_name |
|||
window_class.style = win32con.CS_VREDRAW | win32con.CS_HREDRAW; |
|||
window_class.hCursor = win32gui.LoadCursor(0, win32con.IDC_ARROW) |
|||
window_class.hbrBackground = win32con.COLOR_WINDOW |
|||
window_class.lpfnWndProc = message_map # could also specify a wndproc. |
|||
classAtom = win32gui.RegisterClass(window_class) |
|||
# Create the Window. |
|||
style = win32con.WS_OVERLAPPED | win32con.WS_SYSMENU |
|||
self.hwnd = win32gui.CreateWindow(classAtom, |
|||
self.window_class_name, |
|||
style, |
|||
0, |
|||
0, |
|||
win32con.CW_USEDEFAULT, |
|||
win32con.CW_USEDEFAULT, |
|||
0, |
|||
0, |
|||
hinst, |
|||
None) |
|||
win32gui.UpdateWindow(self.hwnd) |
|||
self.notify_id = None |
|||
self.refresh_icon() |
|||
|
|||
def run(self): |
|||
self.initialize() |
|||
while not self.terminate: |
|||
win32gui.PumpWaitingMessages() |
|||
self.doUpdates() |
|||
sleep(0.100) |
|||
win32gui.Shell_NotifyIcon(win32gui.NIM_DELETE, (self.hwnd, 0)) |
|||
|
|||
# override this |
|||
def doUpdates(self): |
|||
pass |
|||
|
|||
def _add_ids_to_menu_options(self, menu_options): |
|||
result = [] |
|||
for menu_option in menu_options: |
|||
option_text, option_icon, option_action = menu_option |
|||
if callable(option_action) or option_action in self.SPECIAL_ACTIONS: |
|||
self.menu_actions_by_id.add((self._next_action_id, option_action)) |
|||
result.append(menu_option + (self._next_action_id,)) |
|||
elif non_string_iterable(option_action): |
|||
result.append((option_text, |
|||
option_icon, |
|||
self._add_ids_to_menu_options(option_action), |
|||
self._next_action_id)) |
|||
else: |
|||
print 'Unknown item', option_text, option_icon, option_action |
|||
self._next_action_id += 1 |
|||
return result |
|||
|
|||
def get_icon(self, path): |
|||
hicon = self.icons.get(path); |
|||
if hicon != None: return hicon |
|||
|
|||
# Try and find a custom icon |
|||
hinst = win32gui.GetModuleHandle(None) |
|||
if os.path.isfile(path): |
|||
icon_flags = win32con.LR_LOADFROMFILE | win32con.LR_DEFAULTSIZE |
|||
hicon = win32gui.LoadImage(hinst, |
|||
path, |
|||
win32con.IMAGE_ICON, |
|||
0, |
|||
0, |
|||
icon_flags) |
|||
else: |
|||
print "Can't find icon file - using default." |
|||
hicon = win32gui.LoadIcon(0, win32con.IDI_APPLICATION) |
|||
|
|||
self.icons[path] = hicon |
|||
return hicon |
|||
|
|||
def refresh_icon(self): |
|||
hicon = self.get_icon(self.icon) |
|||
if self.notify_id: message = win32gui.NIM_MODIFY |
|||
else: message = win32gui.NIM_ADD |
|||
self.notify_id = (self.hwnd, |
|||
0, |
|||
win32gui.NIF_ICON | win32gui.NIF_MESSAGE | win32gui.NIF_TIP, |
|||
win32con.WM_USER+20, |
|||
hicon, |
|||
self.hover_text) |
|||
win32gui.Shell_NotifyIcon(message, self.notify_id) |
|||
|
|||
def restart(self, hwnd, msg, wparam, lparam): |
|||
self.refresh_icon() |
|||
|
|||
def destroy(self, hwnd, msg, wparam, lparam): |
|||
if self.on_quit: self.on_quit(self) |
|||
nid = (self.hwnd, 0) |
|||
win32gui.Shell_NotifyIcon(win32gui.NIM_DELETE, nid) |
|||
win32gui.PostQuitMessage(0) # Terminate the app. |
|||
|
|||
def notify(self, hwnd, msg, wparam, lparam): |
|||
if lparam==win32con.WM_LBUTTONDBLCLK: |
|||
self.execute_menu_option(self.default_menu_index + self.FIRST_ID) |
|||
elif lparam==win32con.WM_RBUTTONUP: |
|||
self.show_menu() |
|||
elif lparam==win32con.WM_LBUTTONUP: |
|||
pass |
|||
return True |
|||
|
|||
def show_menu(self): |
|||
menu = win32gui.CreatePopupMenu() |
|||
self.create_menu(menu, self.menu_options) |
|||
#win32gui.SetMenuDefaultItem(menu, 1000, 0) |
|||
|
|||
pos = win32gui.GetCursorPos() |
|||
# See http://msdn.microsoft.com/library/default.asp?url=/library/en-us/winui/menus_0hdi.asp |
|||
win32gui.SetForegroundWindow(self.hwnd) |
|||
win32gui.TrackPopupMenu(menu, |
|||
win32con.TPM_LEFTALIGN, |
|||
pos[0], |
|||
pos[1], |
|||
0, |
|||
self.hwnd, |
|||
None) |
|||
win32gui.PostMessage(self.hwnd, win32con.WM_NULL, 0, 0) |
|||
|
|||
def create_menu(self, menu, menu_options): |
|||
for option_text, option_icon, option_action, option_id in menu_options[::-1]: |
|||
if option_icon: |
|||
option_icon = self.prep_menu_icon(option_icon) |
|||
|
|||
if option_id in self.menu_actions_by_id: |
|||
item, extras = win32gui_struct.PackMENUITEMINFO(text=option_text, |
|||
hbmpItem=option_icon, |
|||
wID=option_id) |
|||
win32gui.InsertMenuItem(menu, 0, 1, item) |
|||
else: |
|||
submenu = win32gui.CreatePopupMenu() |
|||
self.create_menu(submenu, option_action) |
|||
item, extras = win32gui_struct.PackMENUITEMINFO(text=option_text, |
|||
hbmpItem=option_icon, |
|||
hSubMenu=submenu) |
|||
win32gui.InsertMenuItem(menu, 0, 1, item) |
|||
|
|||
def prep_menu_icon(self, icon): |
|||
# First load the icon. |
|||
ico_x = win32api.GetSystemMetrics(win32con.SM_CXSMICON) |
|||
ico_y = win32api.GetSystemMetrics(win32con.SM_CYSMICON) |
|||
hicon = win32gui.LoadImage(0, icon, win32con.IMAGE_ICON, ico_x, ico_y, win32con.LR_LOADFROMFILE) |
|||
|
|||
hdcBitmap = win32gui.CreateCompatibleDC(0) |
|||
hdcScreen = win32gui.GetDC(0) |
|||
hbm = win32gui.CreateCompatibleBitmap(hdcScreen, ico_x, ico_y) |
|||
hbmOld = win32gui.SelectObject(hdcBitmap, hbm) |
|||
# Fill the background. |
|||
brush = win32gui.GetSysColorBrush(win32con.COLOR_MENU) |
|||
win32gui.FillRect(hdcBitmap, (0, 0, 16, 16), brush) |
|||
# unclear if brush needs to be feed. Best clue I can find is: |
|||
# "GetSysColorBrush returns a cached brush instead of allocating a new |
|||
# one." - implies no DeleteObject |
|||
# draw the icon |
|||
win32gui.DrawIconEx(hdcBitmap, 0, 0, hicon, ico_x, ico_y, 0, 0, win32con.DI_NORMAL) |
|||
win32gui.SelectObject(hdcBitmap, hbmOld) |
|||
win32gui.DeleteDC(hdcBitmap) |
|||
|
|||
return hbm |
|||
|
|||
def command(self, hwnd, msg, wparam, lparam): |
|||
id = win32gui.LOWORD(wparam) |
|||
self.execute_menu_option(id) |
|||
|
|||
def execute_menu_option(self, id): |
|||
menu_action = self.menu_actions_by_id[id] |
|||
if menu_action == self.QUIT: |
|||
win32gui.DestroyWindow(self.hwnd) |
|||
else: |
|||
menu_action(self) |
|||
|
|||
def non_string_iterable(obj): |
|||
try: |
|||
iter(obj) |
|||
except TypeError: |
|||
return False |
|||
else: |
|||
return not isinstance(obj, basestring) |
|||
|
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.1 KiB |
Loading…
Reference in new issue