From 0c651c186cf022893943ec495a99e603952a3b81 Mon Sep 17 00:00:00 2001 From: MikeTheWatchGuy Date: Mon, 26 Nov 2018 23:05:07 -0500 Subject: [PATCH] Release 0.18.0 Qt --- .../Demo Programs/Qt_Demo_System_Tray_Icon.py | 13 +- .../Qt_Scrape_GitHub_Issues_System_Tray.py | 127 +++++++++++++ PySimpleGUIQt/PySimpleGUIQt.py | 114 +++++++++--- PySimpleGUIQt/readme.md | 172 ++++++++++++++---- 4 files changed, 353 insertions(+), 73 deletions(-) create mode 100644 PySimpleGUIQt/Demo Programs/Qt_Scrape_GitHub_Issues_System_Tray.py diff --git a/PySimpleGUIQt/Demo Programs/Qt_Demo_System_Tray_Icon.py b/PySimpleGUIQt/Demo Programs/Qt_Demo_System_Tray_Icon.py index 74a72ca6..f4dcdefe 100644 --- a/PySimpleGUIQt/Demo Programs/Qt_Demo_System_Tray_Icon.py +++ b/PySimpleGUIQt/Demo Programs/Qt_Demo_System_Tray_Icon.py @@ -11,12 +11,11 @@ logo = b'iVBORw0KGgoAAAANSUhEUgAAADIAAAAtCAMAAADbYcjNAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAMAUExURQAAADBpmDFqmDBqmTFqmjJrmzJsmzJsnDNtnTRrmjVtmjZsmzRunTRunjVvnzdwnjVwnzpxnzVwoDZxoTdyojlyoThzozhzpDh0pDp1pjp2pj51ozt3qDt4qDx4qDx5qj16qj57rD58rT98rkF1oEB4pUB4pkR6pkJ6qEN8q0B9rUB9rkB+rkV7qUZ8qUp9p0x+p0h/rEB+sEeArkqArEqBr0uCrkKAsUKAskOCs0OCtESCtEWEtkaFuEaGuE2FsUiHukiIukmJvEmKvEqLvkuMvk2KuUyLvEyMv1OErVWDqlWHr1qHrVaIsVCMvFSPvV2LsViOuVSQvmyUtmyXuXKbvXefv3ugv06NwE6OwFmUwl2XxGScyGmbw22hynikxnmmyv/UO//UPP/VPf/UPv/VP//UQP/VQf/VQv/WQP/WQf/WQv/WQ//XRP/WRf/WSf/YRf/YRv/YR//YSP/ZSf/ZSv/aS//aTP/aTf/bTv/YUf/ZUv/bUP/cUP/cUv/dVP/dVv/eVv/bW//dWf/cWv/eWP/fWv/dXf/fXf/eXv/cYP/fYP/dZP/dZv/eZf/fZv/eaP/gW//gXP/gXv/gYP/iYf/iYv/hZP/jZP/iZv/kZv/jaf/ja//kaP/lav/kbP/lb//mbP/mbv/ncP/mcv/iff/ocv/odP/odv/oeP/of//qf4GnxYOox4SoxYSpx4asyo+ux4isyouuyouvzIyuyYyvy4yvzI6wy46wzIyz0pCuyJSxyZWyy5u3zZ24zpW30pG52J250J+60aC60KS90aDC3a3E163F2K3F2bPI2bvO3rzP3qvJ4LHN4rnR5P/qgf/qgv/qiP/sif/sjf/sj//olf/ql//ulv/omf/qnv/tnP/qoP/ro//qpP/sov/upf/tqP/uqP/vrf/vrv/us//wpP/wpv/xrf/wsP/wsv/ys//xtP/ytf/ytv/zuf/zuv/0vP/0vsDS38XZ6cnb6f/xw//zwv/yxf/1w//zyP/1yf/2zP/3z//30wAAAM55ho4AAAEAdFJOU////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wBT9wclAAAACXBIWXMAABcQAAAXEAEYYRHbAAAAGHRFWHRTb2Z0d2FyZQBwYWludC5uZXQgNC4xLjFjKpxLAAADnElEQVRIS62OeVhUZRSHw6IosQzGBgoIszKXqGylghKwbHErNVPbEFQQTXYogzZtt2SmiSEHnBHJPdM2Ldv3xbW91Pay0tT29dc53znfvTP809Pz9P7x3Xvu8773fHvhP/M/JBce0GX/8/WduX3ipDt/1nclNrk4TnhR5y1FVzHX/KqzISY5Uou4uLVm/sgEzF1mFqKTruozL9D8gfrM1aIwbvJaF7WFJ4FJqgtb1XOTN1R1eBqYrLbwvpo2+SF2B/NEbFNY+IWoNum6t0UD4imgTO3CCROKiqaJqsnx8fHx+7ho/RBALsnFxcUlJSWlNxpXkkv2I/a1uPnLWEA626WlU6aUlf3uJomJiccdlJDAoQvFBwPsklw2deq06dNnO8nIbj3oHE4hkWDQ7HVcSzLb5eXlFRWVTtKj+/P8OJDojfO6Wahfi3uMW1FZWVVVVV39jk2Skl6RR9JhwOjunDJUPYfZ1q6uqampvcUmWZ4sOpcnJ9Pj8WQqHYAZ4tbW1tXV19ffbJNXPZ6sUUM8nqOBRzweT7LDBQDZdcYmZlz3rk2wNCUlxes9iXYcwlBmOAq4W12moeE2liXBg9QcA6yiB+P1eqk8FtgmdoOh8SbjaoJlqacAj6ZqYqBffCJyo+GGO0S1CVYDKw8VUg0nAJ87NnOrmk4CPJYmSNeHdjQ2Xm/kmcx9qkUlKzKU9PT0tLSTaYeVZ84i/KpFJQ9nZmYermRknAh8qu6sOU1EUDXCJit6UuJwFu1gm+WmJp/PR7f6xr9NVE2eOYLoaeEdVvb5/H7/XOC7QCDwoXE16d+L4IzpC3xmZLb99wYC9wPbm5mP2ZVkRH9DP0OvK/CLcUkmmpvn0Y5gsKWlJRRykwEOXAJBI5NNBNtoRyhEJxa3bnKS005ltBoAqGz+3E47Qq2tO9gLR+jQJAbaItdgWhdje1tbOPwHe5GFdEhyuiE7O1sTkQ1t4fZwOBKJbCTt6/lfOsllZ0TzE9rZpV8bORKZz2z46q2ODpYlwZmCJFfiTyuL3WHZzK4ma7QRgJ2dZcG4mmBoriEnJ4eSc4BvO9vMe0a1CQZKIwwDdqkWxRIxnWT9QOJsITd3KN1NRRc1nQTrzs3L40y4CNitprLwbxXdBD/mM3nCWPoQs+cBkYioBLi8oMBk+flcAHtUJ942HwwxCd4cM2hQATFO5+81WPSbfmBiE+Cl8ZcOHvusDsBfG+hKm/foJHRO/hXgH831bVAP1oP5AAAAAElFTkSuQmCC' - import PySimpleGUIQt as sg # create and then hide the tray icon -menu_def = ['My Menu Def', ['&Restore', '&Open', '&Save',['1', '2', ['a','b']], '&Properties', 'E&xit']] -tray = sg.SystemTray('Title', menu=menu_def, data_base64=logo) +menu_def = ['My Menu Def', ['&Restore', '&Open','---', '&Message','&Save',['1', '2', ['a','b']], '&Properties', 'E&xit']] +tray = sg.SystemTray(menu=menu_def, data_base64=logo) tray.Hide() # create the window @@ -45,8 +44,7 @@ while True: # the event loop tray.UnHide() tray_visible = True window_visible = False - elif event == 'Message': - tray.ShowMessage('Title of message', 'This is a tray message') + # do tray icon stuff. Tray events will cause the window.Read call to return with a Timeout event if tray_visible: menu_item = tray.Read() @@ -67,6 +65,7 @@ while True: # the event loop window.UnHide() else: # some other tray command was received print('Menu item %s'%menu_item) - if menu_item == 'Exit': + if menu_item == 'Exit' or menu_item == sg.EVENT_SYSTEM_TRAY_MESSAGE_CLICKED: break - + elif menu_item == 'Message': + tray.ShowMessage('Title of message', 'This is a tray message\nNote the icon shown in the message too\nIf you click the message it will close the program') \ No newline at end of file diff --git a/PySimpleGUIQt/Demo Programs/Qt_Scrape_GitHub_Issues_System_Tray.py b/PySimpleGUIQt/Demo Programs/Qt_Scrape_GitHub_Issues_System_Tray.py new file mode 100644 index 00000000..6726d928 --- /dev/null +++ b/PySimpleGUIQt/Demo Programs/Qt_Scrape_GitHub_Issues_System_Tray.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python +import PySimpleGUIQt as sg + +import re +# Import requests (to download the page) +import requests + +# Import BeautifulSoup (to parse what we download) +from bs4 import BeautifulSoup + +# search github for total open issues and Issue Number of first issue +def get_num_issues(): + url = "https://github.com/MikeTheWatchGuy/PySimpleGUI/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc" + # set the headers like we are a browser, + headers = { + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36'} + # download the page + response = requests.get(url, headers=headers) + # parse the downloaded homepage and grab all text, + soup = BeautifulSoup(response.text, "lxml") + # look for phrase "XXX Open" + findopen = re.compile(r"\d+ Open") + # get number of open issues + number_open_string = findopen.search(str(soup)).group(0) + num_open_issues = number_open_string[0:number_open_string.index(' ')] + # find the first issue in the list by earing for "issue-id-XXXX" + find_first_issue = re.compile(r'issue-id-\d+') + first_issue_string = find_first_issue.search(str(soup)).group(0) + first_issue = first_issue_string[9:] + return num_open_issues, first_issue + + +def gui(): + sg.ChangeLookAndFeel('Topanga') + + sg.SetOptions(border_width=0) + + layout = [ + [sg.T('GitHub Issues Watcher' + 5 * ' ', click_submits=True, key='GitHub'), + sg.Button('', size=(25,25), + image_data=red_x, + key='_quit_',button_color=(sg.LOOK_AND_FEEL_TABLE['Topanga']['TEXT'],sg.LOOK_AND_FEEL_TABLE['Topanga']['BACKGROUND']), + tooltip='Closes window')], + [sg.T('', key='_status_', size=(12, 1))], + [sg.T('', key='_numissues_', size=(20, 1))], + ] + + window = sg.Window('Issue watcher', + no_titlebar=True, + grab_anywhere=True, + keep_on_top=True, + alpha_channel=.8, # dim the lights a little + location=(2360,310), # locate in upper right corner of screen + ).Layout(layout).Finalize() + + window.Refresh() + status_elem = window.FindElement('_status_') + issues_elem = window.FindElement('_numissues_') + + initial_issue_count, initial_first_issue = get_num_issues() + seconds = 0 + poll_frequncy = 1000 + while True: + event, values = window.Read(timeout=poll_frequncy) + if event in ('_quit_', None): + break + if seconds % 60 == 0 or event.startswith('GitHub'): # Every 60 seconds read GitHub + status_elem.Update('Reading...') + window.Refresh() + issues, first_issue = get_num_issues() + issues_elem.Update('{} Issues. {} is first issue'.format(issues, initial_first_issue)) + window.Refresh() + # if something changed, then make a popup + if issues != initial_issue_count or first_issue != initial_first_issue: + sg.PopupNoWait('Issues changed on GitHub ', 'First issue # is {}'.format(first_issue), background_color='red', keep_on_top=True) + initial_issue_count = issues + initial_first_issue = first_issue + status_elem.Update('') + else: + status_elem.Update('.' if seconds%2 else '') # blink a '.' every 2 seconds so know still running + + seconds += poll_frequncy/1000 + window.Close() + + + +def system_tray(): + + menu_def = ['Root', + ['E&xit']] + tray = sg.SystemTray(data_base64=logo, tooltip='GitHub Issue Watcher') + + # tray.Hide() + initial_issue_count, initial_first_issue = get_num_issues() + # The Event Loop runs every 5000ms + poll_frequncy = 5000 + seconds = 0 + while True: + menu_item = tray.Read(timeout=5000) + if menu_item == 'Exit': + break + if menu_item == 'Run GUI': + gui() + if seconds % 12 == 0: # Every 60 seconds read GitHub + issues, first_issue = get_num_issues() + menu_def = ['root', + ['{} Issues'.format(issues), '{} First Issue'.format(first_issue), '---', '&Run GUI', '&Refresh', 'E&xit']] + tray.Update(menu_def, tooltip='{} First Issue'.format(first_issue)) + # if something changed, then make a popup + if issues != initial_issue_count or first_issue != initial_first_issue: + sg.PopupNoWait('Issues changed on GitHub ', 'First issue # is {}'.format(first_issue), background_color='red', keep_on_top=True) + initial_issue_count = issues + initial_first_issue = first_issue + if menu_item in('Refresh', sg.EVENT_SYSTEM_TRAY_ICON_DOUBLE_CLICKED): + issues, first_issue = get_num_issues() + tray.ShowMessage('Issue', '{} Issues\n{} First Issue'.format(issues, first_issue), messageicon=sg.SYSTEM_TRAY_MESSAGE_ICON_INFORMATION, ) + tray.Update(data_base64=red_x) + + seconds += poll_frequncy/1000 + +red_x = b"R0lGODlhEAAQAPeQAIsAAI0AAI4AAI8AAJIAAJUAAJQCApkAAJoAAJ4AAJkJCaAAAKYAAKcAAKcCAKcDA6cGAKgAAKsAAKsCAKwAAK0AAK8AAK4CAK8DAqUJAKULAKwLALAAALEAALIAALMAALMDALQAALUAALYAALcEALoAALsAALsCALwAAL8AALkJAL4NAL8NAKoTAKwbAbEQALMVAL0QAL0RAKsREaodHbkQELMsALg2ALk3ALs+ALE2FbgpKbA1Nbc1Nb44N8AAAMIWAMsvAMUgDMcxAKVABb9NBbVJErFYEq1iMrtoMr5kP8BKAMFLAMxKANBBANFCANJFANFEB9JKAMFcANFZANZcANpfAMJUEMZVEc5hAM5pAMluBdRsANR8AM9YOrdERMpIQs1UVMR5WNt8X8VgYMdlZcxtYtx4YNF/btp9eraNf9qXXNCCZsyLeNSLd8SSecySf82kd9qqc9uBgdyBgd+EhN6JgtSIiNuJieGHhOGLg+GKhOKamty1ste4sNO+ueenp+inp+HHrebGrefKuOPTzejWzera1O7b1vLb2/bl4vTu7fbw7ffx7vnz8f///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAJAALAAAAAAQABAAAAjUACEJHEiwYEEABniQKfNFgQCDkATQwAMokEU+PQgUFDAjjR09e/LUmUNnh8aBCcCgUeRmzBkzie6EeQBAoAAMXuA8ciRGCaJHfXzUMCAQgYooWN48anTokR8dQk4sELggBhQrU9Q8evSHiJQgLCIIfMDCSZUjhbYuQkLFCRAMAiOQGGLE0CNBcZYmaRIDLqQFGF60eTRoSxc5jwjhACFWIAgMLtgUocJFy5orL0IQRHAiQgsbRZYswbEhBIiCCH6EiJAhAwQMKU5DjHCi9gnZEHMTDAgAOw==" + + +logo = b'iVBORw0KGgoAAAANSUhEUgAAADIAAAAtCAMAAADbYcjNAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAMAUExURQAAADBpmDFqmDBqmTFqmjJrmzJsmzJsnDNtnTRrmjVtmjZsmzRunTRunjVvnzdwnjVwnzpxnzVwoDZxoTdyojlyoThzozhzpDh0pDp1pjp2pj51ozt3qDt4qDx4qDx5qj16qj57rD58rT98rkF1oEB4pUB4pkR6pkJ6qEN8q0B9rUB9rkB+rkV7qUZ8qUp9p0x+p0h/rEB+sEeArkqArEqBr0uCrkKAsUKAskOCs0OCtESCtEWEtkaFuEaGuE2FsUiHukiIukmJvEmKvEqLvkuMvk2KuUyLvEyMv1OErVWDqlWHr1qHrVaIsVCMvFSPvV2LsViOuVSQvmyUtmyXuXKbvXefv3ugv06NwE6OwFmUwl2XxGScyGmbw22hynikxnmmyv/UO//UPP/VPf/UPv/VP//UQP/VQf/VQv/WQP/WQf/WQv/WQ//XRP/WRf/WSf/YRf/YRv/YR//YSP/ZSf/ZSv/aS//aTP/aTf/bTv/YUf/ZUv/bUP/cUP/cUv/dVP/dVv/eVv/bW//dWf/cWv/eWP/fWv/dXf/fXf/eXv/cYP/fYP/dZP/dZv/eZf/fZv/eaP/gW//gXP/gXv/gYP/iYf/iYv/hZP/jZP/iZv/kZv/jaf/ja//kaP/lav/kbP/lb//mbP/mbv/ncP/mcv/iff/ocv/odP/odv/oeP/of//qf4GnxYOox4SoxYSpx4asyo+ux4isyouuyouvzIyuyYyvy4yvzI6wy46wzIyz0pCuyJSxyZWyy5u3zZ24zpW30pG52J250J+60aC60KS90aDC3a3E163F2K3F2bPI2bvO3rzP3qvJ4LHN4rnR5P/qgf/qgv/qiP/sif/sjf/sj//olf/ql//ulv/omf/qnv/tnP/qoP/ro//qpP/sov/upf/tqP/uqP/vrf/vrv/us//wpP/wpv/xrf/wsP/wsv/ys//xtP/ytf/ytv/zuf/zuv/0vP/0vsDS38XZ6cnb6f/xw//zwv/yxf/1w//zyP/1yf/2zP/3z//30wAAAM55ho4AAAEAdFJOU////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wBT9wclAAAACXBIWXMAABcQAAAXEAEYYRHbAAAAGHRFWHRTb2Z0d2FyZQBwYWludC5uZXQgNC4xLjFjKpxLAAADnElEQVRIS62OeVhUZRSHw6IosQzGBgoIszKXqGylghKwbHErNVPbEFQQTXYogzZtt2SmiSEHnBHJPdM2Ldv3xbW91Pay0tT29dc53znfvTP809Pz9P7x3Xvu8773fHvhP/M/JBce0GX/8/WduX3ipDt/1nclNrk4TnhR5y1FVzHX/KqzISY5Uou4uLVm/sgEzF1mFqKTruozL9D8gfrM1aIwbvJaF7WFJ4FJqgtb1XOTN1R1eBqYrLbwvpo2+SF2B/NEbFNY+IWoNum6t0UD4imgTO3CCROKiqaJqsnx8fHx+7ho/RBALsnFxcUlJSWlNxpXkkv2I/a1uPnLWEA626WlU6aUlf3uJomJiccdlJDAoQvFBwPsklw2deq06dNnO8nIbj3oHE4hkWDQ7HVcSzLb5eXlFRWVTtKj+/P8OJDojfO6Wahfi3uMW1FZWVVVVV39jk2Skl6RR9JhwOjunDJUPYfZ1q6uqampvcUmWZ4sOpcnJ9Pj8WQqHYAZ4tbW1tXV19ffbJNXPZ6sUUM8nqOBRzweT7LDBQDZdcYmZlz3rk2wNCUlxes9iXYcwlBmOAq4W12moeE2liXBg9QcA6yiB+P1eqk8FtgmdoOh8SbjaoJlqacAj6ZqYqBffCJyo+GGO0S1CVYDKw8VUg0nAJ87NnOrmk4CPJYmSNeHdjQ2Xm/kmcx9qkUlKzKU9PT0tLSTaYeVZ84i/KpFJQ9nZmYermRknAh8qu6sOU1EUDXCJit6UuJwFu1gm+WmJp/PR7f6xr9NVE2eOYLoaeEdVvb5/H7/XOC7QCDwoXE16d+L4IzpC3xmZLb99wYC9wPbm5mP2ZVkRH9DP0OvK/CLcUkmmpvn0Y5gsKWlJRRykwEOXAJBI5NNBNtoRyhEJxa3bnKS005ltBoAqGz+3E47Qq2tO9gLR+jQJAbaItdgWhdje1tbOPwHe5GFdEhyuiE7O1sTkQ1t4fZwOBKJbCTt6/lfOsllZ0TzE9rZpV8bORKZz2z46q2ODpYlwZmCJFfiTyuL3WHZzK4ma7QRgJ2dZcG4mmBoriEnJ4eSc4BvO9vMe0a1CQZKIwwDdqkWxRIxnWT9QOJsITd3KN1NRRc1nQTrzs3L40y4CNitprLwbxXdBD/mM3nCWPoQs+cBkYioBLi8oMBk+flcAHtUJ942HwwxCd4cM2hQATFO5+81WPSbfmBiE+Cl8ZcOHvusDsBfG+hKm/foJHRO/hXgH831bVAP1oP5AAAAAElFTkSuQmCC' + + +system_tray() diff --git a/PySimpleGUIQt/PySimpleGUIQt.py b/PySimpleGUIQt/PySimpleGUIQt.py index 95dfb475..691262e1 100644 --- a/PySimpleGUIQt/PySimpleGUIQt.py +++ b/PySimpleGUIQt/PySimpleGUIQt.py @@ -113,6 +113,7 @@ DEFAULT_DEBUG_WINDOW_SIZE = (800, 400) DEFAULT_WINDOW_LOCATION = (None, None) MAX_SCROLLED_TEXT_BOX_HEIGHT = 50 DEFAULT_TOOLTIP_TIME = 400 +DEFAULT_TOOLTIP_OFFSET = (20,-20) #################### COLOR STUFF #################### BLUES = ("#082567", "#0A37A3", "#00345B") PURPLES = ("#480656", "#4F2398", "#380474") @@ -2684,21 +2685,19 @@ class ErrorElement(Element): # Tray CLASS # # ------------------------------------------------------------------------- # class SystemTray: - def __init__(self, title, filename=None, menu=None, data=None, data_base64=None, tooltip=None): + def __init__(self, menu=None, filename=None, data=None, data_base64=None, tooltip=None): ''' SystemTray - create an icon in the system tray - :param title: - :param filename: - :param menu: - :param data: - :param data_base64: + :param menu: Menu definition + :param filename: filename for icon + :param data: in-ram image for icon + :param data_base64: basee-64 data for icon + :param tooltip: tooltip string ''' - self.Title = title self.Menu = menu self.TrayIcon = None self.Shown = False self.MenuItemChosen = TIMEOUT_KEY - self.Tooltip = tooltip global _my_windows @@ -2725,16 +2724,17 @@ class SystemTray: return self.TrayIcon = QSystemTrayIcon(qicon) - qmenu = QMenu() - qmenu.setTitle(self.Menu[0]) - AddTrayMenuItem(qmenu, self.Menu[1], self) + if self.Menu is not None: + qmenu = QMenu() + qmenu.setTitle(self.Menu[0]) + AddTrayMenuItem(qmenu, self.Menu[1], self) + self.TrayIcon.setContextMenu(qmenu) - if self.Tooltip is not None: - self.TrayIcon.setToolTip(str(self.Tooltip)) + if tooltip is not None: + self.TrayIcon.setToolTip(str(tooltip)) self.TrayIcon.messageClicked.connect(self.messageClicked) self.TrayIcon.activated.connect(self.doubleClicked) - self.TrayIcon.setContextMenu(qmenu) self.TrayIcon.show() @@ -2766,19 +2766,22 @@ class SystemTray: if not self.Shown: self.Shown = True self.TrayIcon.show() - if timeout is None: - self.App.exec_() - else: - self.App.processEvents() + if timeout is None: + self.App.exec_() + elif timeout == 0: + self.App.processEvents() else: - if timeout is None: - self.App.exec_() - else: - self.App.processEvents() + self.timer = start_systray_read_timer(self, timeout) + self.App.exec_() + if self.timer: + stop_timer(self.timer) + item = self.MenuItemChosen self.MenuItemChosen = TIMEOUT_KEY return item + def timer_timeout(self): + self.App.exit() # kick the users out of the mainloop def Hide(self): self.TrayIcon.hide() @@ -2823,7 +2826,52 @@ class SystemTray: return self def Close(self): - self.App.quit() + ''' + + :return: + ''' + self.TrayIcon.Hide() + # Don't close app because windows could be depending on it + # self.App.quit() + + + def Update(self, menu=None, tooltip=None,filename=None, data=None, data_base64=None,): + ''' + Updates the menu, tooltip or icon + :param menu: menu defintion + :param tooltip: string representing tooltip + :param filename: icon filename + :param data: icon raw image + :param data_base64: icon base 64 image + :return: + ''' + # Menu + if menu is not None: + self.Menu = menu + qmenu = QMenu() + qmenu.setTitle(self.Menu[0]) + AddTrayMenuItem(qmenu, self.Menu[1], self) + self.TrayIcon.setContextMenu(qmenu) + # Tooltip + if tooltip is not None: + self.TrayIcon.setToolTip(str(tooltip)) + # Icon + qicon = None + if filename is not None: + qicon = QIcon(filename) + elif data is not None: + ba = QtCore.QByteArray.fromRawData(data) + pixmap = QtGui.QPixmap() + pixmap.loadFromData(ba) + qicon = QIcon(pixmap) + elif data_base64 is not None: + ba = QtCore.QByteArray.fromBase64(data_base64) + pixmap = QtGui.QPixmap() + pixmap.loadFromData(ba) + qicon = QIcon(pixmap) + if qicon is not None: + self.TrayIcon.setIcon(qicon) + # ------------------------------------------------------------------------- # @@ -5026,6 +5074,13 @@ def start_window_read_timer(window, amount): return timer +def start_systray_read_timer(tray, amount): + timer = QtCore.QTimer() + timer.timeout.connect(tray.timer_timeout) + timer.start(amount) + return timer + + def start_window_autoclose_timer(window, amount): timer = QtCore.QTimer() window.autoclose_timer = timer @@ -5164,7 +5219,7 @@ def StartupTK(window): if not window.FormRemainedOpen: _my_windows.Decrement() if window.RootNeedsDestroying: - print('** Destroying window **') + # print('** Destroying window **') window.QT_QMainWindow.close() # destroy the window window.RootNeedsDestroying = False return @@ -5569,17 +5624,17 @@ def EasyPrintClose(): # ======================== Scrolled Text Box =====# # ===================================================# def PopupScrolled(*args, button_color=None, yes_no=False, auto_close=False, auto_close_duration=None, - size=(None, None)): + size=(None, None), location=(None, None)): if not args: return width, height = size width = width if width else MESSAGE_BOX_LINE_WIDTH form = Window(args[0], auto_size_text=True, button_color=button_color, auto_close=auto_close, - auto_close_duration=auto_close_duration) + auto_close_duration=auto_close_duration, location=location) max_line_total, max_line_width, total_lines, height_computed = 0, 0, 0, 0 complete_output = '' for message in args: # fancy code to check if string and convert if not is not need. Just always convert to string :-) - # if not isinstance(message, str): message = str(message) + # if not isinstance(message, str): message = str(message) - new message = str(message) longest_line_len = max([len(l) for l in message.split('\n')]) width_used = min(longest_line_len, width) @@ -5592,16 +5647,19 @@ def PopupScrolled(*args, button_color=None, yes_no=False, auto_close=False, auto height_computed = MAX_SCROLLED_TEXT_BOX_HEIGHT if height_computed > MAX_SCROLLED_TEXT_BOX_HEIGHT else height_computed if height: height_computed = height - form.AddRow(Multiline(complete_output, size=(max_line_width, height_computed))) + computed_size = (max_line_width*10, height_computed*16) + form.AddRow(MultilineOutput(complete_output, size=computed_size)) pad = max_line_total - 15 if max_line_total > 15 else 1 # show either an OK or Yes/No depending on paramater if yes_no: form.AddRow(Text('', size=(pad, 1), auto_size_text=False), Yes(), No()) button, values = form.Read() + form.Close() return button else: form.AddRow(Text('', size=(pad, 1), auto_size_text=False), Button('OK', size=(5, 1), button_color=button_color)) button, values = form.Read() + form.Close() return button diff --git a/PySimpleGUIQt/readme.md b/PySimpleGUIQt/readme.md index c5b538ff..2ecacef3 100644 --- a/PySimpleGUIQt/readme.md +++ b/PySimpleGUIQt/readme.md @@ -26,7 +26,7 @@ "Qt without the ugly" - ## The Alpha Release Version 0.13.0 + ## The Alpha Release Version 0.18.0 [Announcements of Latest Developments](https://github.com/MikeTheWatchGuy/PySimpleGUI/issues/142) @@ -37,7 +37,7 @@ Welcome to the Alpha Release of PySimpleGUI for Qt! You can use the exact same code that you are running on the older, tkinter, version of PySimpleGUI. -PySimpleGUIQt uses **PySide2** OR **PyQt5** for access to Qt. +PySimpleGUIQt uses **PySide2** OR **PyQt5** for access to Qt. PyQt5 has been having a number of problems recently however so tread lightly. ## Porting your PySimpleGUI code to PySimpleGUIQt @@ -66,15 +66,15 @@ Fonts should be in the format (font family, size). You can use the older string On Linux systems you need to run pip3. pip3 install --upgrade PySimpleGUIQt -### Installing PySide2 or PyQt5 for Python 3 +### Installing PySide2 for Python 3 It is recommended that you use PySide2, however, if that cannot be found, then PyQt5 will be attempted. To install either of these: ```pip install PySide2``` - or - -```pip install PyQt5``` + +**Nov 26th - There has been a number of problems found using PyQt5 recently. Unclear how if it can be supported after all.** + ## Testing your installation Once you have installed, or copied the .py file to your app folder, you can test the installation using python. At the command prompt start up Python. @@ -91,10 +91,9 @@ Here is the window you should see: ## Prerequisites Python 3 -PySide2 or PyQt5 - - +PySide2 or PyQt5 (experimental) + ## Using - Python 3 To use in your code, simply import.... `import PySimpleGUIQt as sg` @@ -164,60 +163,102 @@ There are a number of new features that are only available in PySimpleGUIQt. Th ## SystemTray -In addition to running normal windows, it's now also possible to have an icon down in the system tray that you can read to get menu events. There is a new SystemTray object that is used much like a Window object. You first get one, then you perform Reads in order to get events. In this case the only events you'll receive are menu selections and timeouts. +This is a PySimpleGUIQt only feature. Don't know of a way to do it using tkinter. It looks likely to work on WxPython however. + +In addition to running normal windows, it's now also possible to have an icon down in the system tray that you can read to get menu events. There is a new SystemTray object that is used much like a Window object. You first get one, then you perform Reads in order to get events. Here is the definition of the SystemTray object. ```python -SystemTray:(title, filename=None, menu=None, data=None, data_base64=None): +SystemTray(menu=None, filename=None, data=None, data_base64=None, tooltip=None): ''' SystemTray - create an icon in the system tray - :param title: Not currently used. A placeholder / name reminder - :param filename: PNG/ICO/? file that will be used for icon - :param menu: - :param data: In-RAM image to be used for icon - :param data_base64: Base64 data to be used for icon - ''' + :param menu: Menu definition + :param filename: filename for icon + :param data: in-ram image for icon + :param data_base64: basee-64 data for icon + :param tooltip: tooltip string ''' ``` You'll notice that there are 3 different ways to specify the icon image. The base-64 parameter allows you to define a variable in your .py code that is the encoded image so that you do not need any additional files. Very handy feature. -### System Tray Design Pattern +## System Tray Design Pattern Here is a design pattern you can use to get a jump-start. -This program will create a system tray icon and perform a blocking Read. If the item "Open" is chosen from the system tray, then a window is shown on the screen. +This program will create a system tray icon and perform a blocking Read. If the item "Open" is chosen from the system tray, then a popup is shown. ```python import PySimpleGUIQt as sg -menu_def = ['File', ['&Open', '&Save',['1', '2', ['a','b']], '&Properties', 'E&xit']] +menu_def = ['BLANK', ['&Open', '---', '&Save', ['1', '2', ['a', 'b']], '&Properties', 'E&xit']] -tray = sg.SystemTray('My Tray', menu=menu_def, filename=r'default_icon.ico') - -while True: - menu_item = tray.Read() - if menu_item is not None: print(menu_item) +tray = sg.SystemTray(menu=menu_def, filename=r'default_icon.ico') +while True: # The event loop + menu_item = tray.Read() + print(menu_item) if menu_item == 'Exit': break - if menu_item == 'Open': - window = sg.Window('My win').Layout([[sg.Text('My layout')]]) - event, values = window.Read() - print(event, values) + elif menu_item == 'Open': + sg.Popup('Menu item chosen', menu_item) + ``` +The design pattern creates an icon that will display this menu: +![snag-0293](https://user-images.githubusercontent.com/13696193/49057441-8bbfe980-f1cd-11e8-93e7-1aeda9ccd173.jpg) + +### Icons + +When specifying "icons", you can use 3 different formats. +* `filename`- filename +* `data_base64` - base64 byte string +* '`data` - in-ram bitmap or other "raw" image + +You will find 3 parameters used to specify these 3 options on both the initialize statement and on the Update method. + +## Menu Definition +```python +menu_def = ['BLANK', ['&Open', '&Save', ['1', '2', ['a', 'b']], '&Properties', 'E&xit']] +``` + +A menu is defined using a list. A "Menu entry" is a string that specifies: +* text shown +* keyboard shortcut +* key + +See section on Menu Keys for more informatoin on using keys with menus. + +An entry without a key and keyboard shortcut is a simple string +`'Menu Item'` + +If you want to make the "M" be a keyboard shortcut, place an `&` in front of the letter that is the shortcut. +`'&Menu Item'` + +You can add "keys" to make menu items unique or as another way of identifying a menu item than the text shown. The key is added to the text portion by placing `::` after the text. + +`'Menu Item::key'` + +The first entry can be ignored.`'BLANK`' was chosen for this example. It's this way because normally you would specify these menus under some heading on a menu-bar. But here there is no heading so it's filled in with any value you want. + +**Separators** +If you want a separator between 2 items, add the entry `'---'` and it will add a separator item at that place in your menu. + + ## SystemTray Methods ### Read - Read the context menu or check for events ```python -def Read(timeout=None): +def Read(timeout=None) ''' Reads the context menu :param timeout: Optional. Any value other than None indicates a non-blocking read :return: String representing meny item chosen. None if nothing read. ''' ``` +The `timeout` parameter specifies how long to wait for an event to take place. If nothing happens within the timeout period, then a "timeout event" is returned. These types of reads make it possible to run asynchronously. To run non-blocked, specify `timeout=0`on the Read call. + +Read returns the menu text, complete with key, for the menu item chosen. If you specified `Open::key` as the menu entry, and the user clicked on `Open`, then you will receive the string `Open::key` upon completion of the Read. #### Read special return values @@ -228,12 +269,20 @@ EVENT_SYSTEM_TRAY_ICON_ACTIVATED - Tray icon was single clicked EVENT_SYSTEM_TRAY_MESSAGE_CLICKED - a message balloon was clicked TIMEOUT_KEY is returned if no events are available if the timeout value is set in the Read call + ### Hide -Hides the icon +Hides the icon. Note that no message balloons are shown while an icon is hidden. ```python -def Hide(): +def Hide() +``` + +### Close + +Does the same thing as hide +```python +def Close() ``` @@ -242,15 +291,26 @@ def Hide(): Shows a previously hidden icon ```python -def UnHide(): +def UnHide() ``` ### ShowMessage -Shows a balloon above the icon in the system tray area +Shows a balloon above the icon in the system tray area. You can specify your own icon to be shown in the balloon, or you can set `messageicon` to one of the preset values. + +This message has a custom icon. + +![snag-0286](https://user-images.githubusercontent.com/13696193/49057459-a85c2180-f1cd-11e8-9a66-aa331d7e034c.jpg) + +The preset `messageicon` values are: + + SYSTEM_TRAY_MESSAGE_ICON_INFORMATION + SYSTEM_TRAY_MESSAGE_ICON_WARNING + SYSTEM_TRAY_MESSAGE_ICON_CRITICAL + SYSTEM_TRAY_MESSAGE_ICON_NOICON ```python -def ShowMessage(title, message, filename=None, data=None, data_base64=None, time=10000): +ShowMessage(title, message, filename=None, data=None, data_base64=None, messageicon=None, time=10000): ''' Shows a balloon above icon in system tray :param title: Title shown in balloon @@ -258,11 +318,35 @@ def ShowMessage(title, message, filename=None, data=None, data_base64=None, time :param filename: Optional icon filename :param data: Optional in-ram icon :param data_base64: Optional base64 icon - :param time: How long to display message in milliseconds - :return: self (for call chaining) - ''' + :param time: How long to display message in milliseconds :return: + ''' ``` +Note, on windows it may be necessary to make a registry change to enable message balloons to be seen. To fix this, you must create the DWORD you see in this screenshot. +![snag-0285](https://user-images.githubusercontent.com/13696193/49056144-6381bc00-f1c8-11e8-9f44-199394823369.jpg) + + +### Update + +You can update any of these items within a SystemTray object +* Menu definition +* Icon +* Tooltip + + Change them all or just 1. + +```python +Update(menu=None, tooltip=None,filename=None, data=None, data_base64=None,) + ''' + Updates the menu, tooltip or icon + :param menu: menu defintion + :param tooltip: string representing tooltip + :param filename: icon filename + :param data: icon raw image + :param data_base64: icon base 64 image + :return: + ''' +``` ## Menus with Keys PySimpleGUIQt offers the ability to add a key to your menu items. To do so, you add :: and the key value to the end of your menu definition. @@ -373,6 +457,18 @@ Menubar now returns values as does the ButtonMenu Window.Hide and UnHide methods +### 0.18.0 26-Nov-2018 + +Tooltips for all elements +Completion of all SystemTray features +Read with or without timeout +Specify icons from 3 sources +Show message with custom or preset icons +Update +* Menu +* Tooltip +* Icon +PopupScrolled - new location parameter, fixed bug that wasn't closing window when completed # Design ## Author