From d8cb62a92c85a2298e648b82d82d3197426c38eb Mon Sep 17 00:00:00 2001 From: David Amrhein Date: Sun, 21 Aug 2016 18:44:16 -0400 Subject: [PATCH] Run win32 as systray app --- lbrynet/conf.py | 6 +- lbrynet/lbrynet_daemon/LBRYDaemonServer.py | 3 +- .../windows/lbry-win32-app/LBRYWin32App.py | 280 ++++++++++++++++++ .../{ => lbry-win32-app}/icons/lbry128.ico | Bin .../windows/lbry-win32-app/icons/lbry16.ico | Bin 0 -> 1150 bytes .../{ => lbry-win32-app}/icons/lbry256.ico | Bin .../{ => lbry-win32-app}/icons/lbry32.ico | Bin .../{ => lbry-win32-app}/icons/lbry48.ico | Bin .../{ => lbry-win32-app}/icons/lbry96.ico | Bin setup_win32.py | 11 +- 10 files changed, 293 insertions(+), 7 deletions(-) create mode 100644 packaging/windows/lbry-win32-app/LBRYWin32App.py rename packaging/windows/{ => lbry-win32-app}/icons/lbry128.ico (100%) create mode 100644 packaging/windows/lbry-win32-app/icons/lbry16.ico rename packaging/windows/{ => lbry-win32-app}/icons/lbry256.ico (100%) rename packaging/windows/{ => lbry-win32-app}/icons/lbry32.ico (100%) rename packaging/windows/{ => lbry-win32-app}/icons/lbry48.ico (100%) rename packaging/windows/{ => lbry-win32-app}/icons/lbry96.ico (100%) diff --git a/lbrynet/conf.py b/lbrynet/conf.py index 118a52f1f..c5c35b142 100644 --- a/lbrynet/conf.py +++ b/lbrynet/conf.py @@ -1,6 +1,7 @@ """ Some network wide and also application specific parameters """ +import os MAX_HANDSHAKE_SIZE = 2**16 @@ -38,7 +39,10 @@ CRYPTSD_FILE_EXTENSION = ".cryptsd" API_INTERFACE = "localhost" API_ADDRESS = "lbryapi" API_PORT = 5279 -ICON_PATH = "app.icns" +if os.name == "nt": + ICON_PATH = "icons" +else: + ICON_PATH = "app.icns" APP_NAME = "LBRY" API_CONNECTION_STRING = "http://%s:%i/%s" % (API_INTERFACE, API_PORT, API_ADDRESS) UI_ADDRESS = "http://%s:%i" % (API_INTERFACE, API_PORT) diff --git a/lbrynet/lbrynet_daemon/LBRYDaemonServer.py b/lbrynet/lbrynet_daemon/LBRYDaemonServer.py index d210a830d..775761b41 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemonServer.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemonServer.py @@ -378,13 +378,12 @@ class LBRYFileUpload(resource.Resource): newdirpath = tempfile.mkdtemp() newpath = os.path.join(newdirpath, origfilename) if os.name == "nt": - print('Debugging: shutil.copy({0}, {1}'.format(uploaded_file.name, newpath)) shutil.copy(uploaded_file.name, newpath) # TODO Still need to remove the file try: os.remove(uploaded_file.name) except WindowsError as e: - print(e) + pass else: shutil.move(uploaded_file.name, newpath) self._api.uploaded_temp_files.append(newpath) diff --git a/packaging/windows/lbry-win32-app/LBRYWin32App.py b/packaging/windows/lbry-win32-app/LBRYWin32App.py new file mode 100644 index 000000000..fb3041c0b --- /dev/null +++ b/packaging/windows/lbry-win32-app/LBRYWin32App.py @@ -0,0 +1,280 @@ +import logging +import os +import socket +import sys +import threading +import webbrowser + +# from appdirs import user_data_dir +from twisted.internet import reactor +from twisted.web import server +# from twisted.internet.cfreactor import install +# install(runner=AppHelper.runEventLoop) +import win32api +import win32con +import win32gui_struct + +try: + import winxpgui as win32gui +except ImportError: + import win32gui + +from lbrynet.lbrynet_daemon.LBRYDaemonServer import LBRYDaemonServer, LBRYDaemonRequest +from lbrynet.conf import API_PORT, API_INTERFACE, ICON_PATH, APP_NAME +from lbrynet.conf import UI_ADDRESS + + +if getattr(sys, 'frozen', False) and os.name == "nt": + os.environ["REQUESTS_CA_BUNDLE"] = os.path.join(os.path.dirname(sys.executable), "cacert.pem") + +log = logging.getLogger(__name__) + +REMOTE_SERVER = "www.google.com" + + +def test_internet_connection(): + try: + host = socket.gethostbyname(REMOTE_SERVER) + s = socket.create_connection((host, 80), 2) + return True + except: + return False + + +def non_string_iterable(obj): + try: + iter(obj) + except TypeError: + return False + else: + return not isinstance(obj, basestring) + + +class SysTrayIcon(object): + """TODO""" + QUIT = 'QUIT' + SPECIAL_ACTIONS = [QUIT] + + FIRST_ID = 1023 + + def __init__(self, + icon, + hover_text, + menu_options, + on_quit=None, + default_menu_index=None, + window_class_name=None, ): + + self.icon = icon + 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" + + 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() + + win32gui.PumpMessages() + + 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 refresh_icon(self): + # Try and find a custom icon + hinst = win32gui.GetModuleHandle(None) + if os.path.isfile(self.icon): + icon_flags = win32con.LR_LOADFROMFILE | win32con.LR_DEFAULTSIZE + hicon = win32gui.LoadImage(hinst, + self.icon, + win32con.IMAGE_ICON, + 0, + 0, + icon_flags) + else: + print "Can't find icon file - using default." + hicon = win32gui.LoadIcon(0, win32con.IDI_APPLICATION) + + 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 main(): + def LBRYApp(): + SysTrayIcon(icon, hover_text, menu_options, on_quit=stop) + + def openui_(sender): + webbrowser.open(UI_ADDRESS) + + def replyToApplicationShouldTerminate_(): + reactor.stop() + + def stop(sysTrayIcon): + replyToApplicationShouldTerminate_() + + icon = os.path.join(ICON_PATH, 'lbry16.ico') + hover_text = APP_NAME + menu_options = (('Open UI', icon, openui_),) + + if not test_internet_connection(): + log.warn('No Internet Connection') + sys.exit(1) + + systray_thread = threading.Thread(target=LBRYApp) + systray_thread.start() + + lbry = LBRYDaemonServer() + d = lbry.start() + d.addCallback(lambda _: webbrowser.open(UI_ADDRESS)) + lbrynet_server = server.Site(lbry.root) + lbrynet_server.requestFactory = LBRYDaemonRequest + reactor.listenTCP(API_PORT, lbrynet_server, interface=API_INTERFACE) + # reactor.addSystemEventTrigger("after", "shutdown", AppHelper.stopEventLoop) + reactor.run() + +if __name__ == '__main__': + main() diff --git a/packaging/windows/icons/lbry128.ico b/packaging/windows/lbry-win32-app/icons/lbry128.ico similarity index 100% rename from packaging/windows/icons/lbry128.ico rename to packaging/windows/lbry-win32-app/icons/lbry128.ico diff --git a/packaging/windows/lbry-win32-app/icons/lbry16.ico b/packaging/windows/lbry-win32-app/icons/lbry16.ico new file mode 100644 index 0000000000000000000000000000000000000000..40d84962861902f98d5d64b2627ae605932aebcb GIT binary patch literal 1150 zcmcIi-A|HX6n{uJ*4)Z<;f1Y_tL8<2K^OHObmM(jwOs3_wYgarIo47tr&B%!LPelR z6jCU%Fe%YxQ%P8WihKlv(H8@imzVcFo#z#X7r45p^X@#)`TCu6p7S0;MEEr~6Fkq7 zjz&US2qA6AXhO@r9WO%AS8Cd4LH|z&01#vmauaeR)&GWQT!l}@Vq3RduWTOw`icx~ z=dNFC7ku^d9KfB9$0MCDY-YbwE-jl)8c-^vV4pGtGnrHe#vQB1R`XQ!Q_<*Vr)|m< zP{?~Z$INp`CgXrMNW{0nZZp$7CGUdUQ7rCVL`(z4QTm#QN^FzHfI`;GIUQC=r4oQW zgZ{}NC}cgLRz3osZy7=%Kj^drU>H+oH#dU=g+jiIVVJgYgNiqsv;yuk#_2QzFghZK zP;ebsmKA!?>jvQ|#>8V0c<-5o{=RO?{Km!?nMU0Qe*Y@WOj`t8I2?d-xs2`As8nKL z(K`q7p&szeyWq|2G~DgF0Sh18-*85p*{A`WH*g$R1J#j!e9wY)Vhq%(#}Ejt!S3$P z%XB(dF$N z#J+?vfU^dy1z4+$yRnwf=PskSx-;Tj?q~aJm;IxB=mF<`>wvx89pHIRn4#sF9&jgE zr(@z1{@a^~Yo*vKQ5ok=bG1eyVTs8&oF2y68b%eg9)tC~_|~iFZ(-TepLX#G!xS$p zEzVo84_7vuxq`S8_=ZDq)jsG=d#SJ(@1s@yV9dXg=?KdA2q|77Bzl~X?uNrNzY`Ji OON_B+2;mV!vA+Qcgj3Q0 literal 0 HcmV?d00001 diff --git a/packaging/windows/icons/lbry256.ico b/packaging/windows/lbry-win32-app/icons/lbry256.ico similarity index 100% rename from packaging/windows/icons/lbry256.ico rename to packaging/windows/lbry-win32-app/icons/lbry256.ico diff --git a/packaging/windows/icons/lbry32.ico b/packaging/windows/lbry-win32-app/icons/lbry32.ico similarity index 100% rename from packaging/windows/icons/lbry32.ico rename to packaging/windows/lbry-win32-app/icons/lbry32.ico diff --git a/packaging/windows/icons/lbry48.ico b/packaging/windows/lbry-win32-app/icons/lbry48.ico similarity index 100% rename from packaging/windows/icons/lbry48.ico rename to packaging/windows/lbry-win32-app/icons/lbry48.ico diff --git a/packaging/windows/icons/lbry96.ico b/packaging/windows/lbry-win32-app/icons/lbry96.ico similarity index 100% rename from packaging/windows/icons/lbry96.ico rename to packaging/windows/lbry-win32-app/icons/lbry96.ico diff --git a/setup_win32.py b/setup_win32.py index 1e6b48387..602fb6b45 100644 --- a/setup_win32.py +++ b/setup_win32.py @@ -13,7 +13,7 @@ import requests.certs from lbrynet import __version__ -win_icon = os.path.join('packaging', 'windows', 'icons', 'lbry256.ico') +win_icon = os.path.join('packaging', 'windows', 'lbry-win32-app', 'icons', 'lbry256.ico') wordlist_path = pkg_resources.resource_filename('lbryum', 'wordlist') base_dir = os.path.abspath(os.path.dirname(__file__)) @@ -113,7 +113,10 @@ build_exe_options = { 'Tkinter', 'tk', 'tcl', 'PyQt4', 'nose', 'mock' 'zope.interface._zope_interface_coptimizations'], 'include_files': [(distutils_path, 'distutils'), (requests.certs.where(), 'cacert.pem'), - (os.path.join(wordlist_path, 'chinese_simplified.txt'), os.path.join('wordlist', 'chinese_simplified.txt')), + (os.path.join('packaging', 'windows', 'lbry-win32-app', 'icons', 'lbry16.ico'), + os.path.join('icons', 'lbry16.ico')), + (os.path.join(wordlist_path, 'chinese_simplified.txt'), + os.path.join('wordlist', 'chinese_simplified.txt')), (os.path.join(wordlist_path, 'english.txt'), os.path.join('wordlist', 'english.txt')), (os.path.join(wordlist_path, 'japanese.txt'), os.path.join('wordlist', 'japanese.txt')), (os.path.join(wordlist_path, 'portuguese.txt'), os.path.join('wordlist', 'portuguese.txt')), @@ -122,8 +125,8 @@ build_exe_options = { 'namespace_packages': ['zope', 'google']} exe = Executable( - script=os.path.join('lbrynet', 'lbrynet_daemon', 'LBRYDaemonControl.py'), - # base='Win32GUI', + script=os.path.join('packaging', 'windows', 'lbry-win32-app', 'LBRYWin32App.py'), + base='Win32GUI', icon=win_icon, compress=True, shortcutName='LBRY',