2023-11-14 14:27:43 +00:00
from os . path import exists as __os_path_exists
from pystray import Icon as pystray_Icon , Menu as pystray_Menu , MenuItem as pystray_MenuItem
from PIL import Image
from threading import Thread as threading_Thread
from enum import Enum
from types import FunctionType
from functools import partial
class Menu ( ) :
class Default ( Enum ) :
DEFAULT = " DefaultValue "
class ItemType ( Enum ) :
LABEL = " LabelType "
BUTTON = " ButtonType "
CHECK = " CheckType "
SUBMENU = " SubmenuType "
SEPARATOR = " SeparatorType "
def __init__ ( self ) :
""" Create a pystray.MenuItem \n
Used to set the options of the app in the notification tray """
self . id = 0
self . ItemsFromID = { } # id: pystray.MenuItem, (DicKey, callback, item_type, {"current": bool | None, "requested": bool | None}, index)
self . IDsFromItem = { } # pystray.MenuItem: id
def create_item ( self , text : str , callback : FunctionType | None = None , item_type : ItemType = ItemType . LABEL , checkdefault : bool = False , index : int | None = None , FORCE_ID : int | None = None ) :
""" Create an item that will be displayed in the options of the app notification in the notification tray \n
Parameters
- - - - - - - - - -
* DicKey \n
The key of the word you want to display from the Lang dict
* callback ( Facultative ) \n
The function to callback after the button is clicked
* item_type ( Facultative ) \n
The type of the item to create ( see Menu . ItemType )
* checkdefault ( Facultative ) \n
Only use with item_type as Menu . ItemType . CHECK , define the default status of the item , True = checked , False = not checked
* index ( Facultative ) \n
The place of the item when it display , if None add at the end , if conflict , first added will use the correct index , followed by the ones in conflict
Returns
- - - - - - -
* Item ID \n
The ID of the item , used to remove it with delete_item """
#Check item type
if item_type == self . ItemType . LABEL :
#Reserve id
if FORCE_ID :
id = FORCE_ID
else :
self . id + = 1
id = self . id
#Create item and add it to lists
self . ItemsFromID [ id ] = ( pystray_MenuItem ( text , lambda icon , item : None ) , ( text , callback , item_type , { " current " : None , " requested " : checkdefault } , index ) )
self . IDsFromItem [ self . ItemsFromID [ id ] [ 0 ] ] = id
return id
elif item_type == self . ItemType . BUTTON :
#Check if callback is a callable object
if callable ( callback ) :
#Reserve id
if FORCE_ID :
id = FORCE_ID
else :
self . id + = 1
id = self . id
#Use partial to pass our arguments to our callback
menu_callback = partial ( self . __menu_callback , id , callback )
#Create item and add it to lists
self . ItemsFromID [ id ] = ( pystray_MenuItem ( text , menu_callback ) , ( text , callback , item_type , { " current " : None , " requested " : checkdefault } , index ) )
self . IDsFromItem [ self . ItemsFromID [ id ] [ 0 ] ] = id
return id
else :
raise ValueError ( " callback is not callable " )
elif item_type == self . ItemType . CHECK :
#Check if callback is a callable object
if callable ( callback ) :
#Reserve id
if FORCE_ID :
id = FORCE_ID
else :
self . id + = 1
id = self . id
#Use partial to pass our arguments to our callback
menu_callback = partial ( self . __menu_callback , id , callback )
#Create item and add it to lists
self . ItemsFromID [ id ] = ( pystray_MenuItem ( text , menu_callback , checked = lambda item : self . __change_status ( item ) ) , ( text , callback , item_type , { " current " : None , " requested " : checkdefault } , index ) )
self . IDsFromItem [ self . ItemsFromID [ id ] [ 0 ] ] = id
return id
else :
raise ValueError ( " callback isn ' t callable ! Please link callback to a function " )
elif item_type == self . ItemType . SUBMENU :
raise NotImplementedError
elif item_type == self . ItemType . SEPARATOR :
#Reserve id
if FORCE_ID :
id = FORCE_ID
else :
self . id + = 1
id = self . id
self . ItemsFromID [ id ] = ( pystray_Menu . SEPARATOR , ( None , None , None , None , index ) )
self . IDsFromItem [ self . ItemsFromID [ id ] [ 0 ] ] = id
return id
else :
#Unknow item
raise ValueError ( " Unknow ItemType " )
def delete_item ( self , item_id : int ) :
#Check if item exist
if item_id in self . ItemsFromID :
#Remove item from list
self . IDsFromItem . pop ( self . ItemsFromID [ item_id ] [ 0 ] )
item = self . ItemsFromID . pop ( item_id )
#Return Item and parameters (Used by edit_item and if user wants to get the parameters back)
return item
else :
raise IndexError ( " Item doesn ' t exist " )
def edit_item ( self , item_id : int , text : str | Default = Default . DEFAULT , callback : FunctionType | None | Default = Default . DEFAULT , item_type : ItemType | Default = Default . DEFAULT , checkdefault : bool | Default = Default . DEFAULT , index : int | None | Default = Default . DEFAULT ) :
""" Combination of delete_item() and create_item() Note: Does not change the id of the item \n
Parameters
- - - - - - - - - -
* item_id ( Required ) \n
The id of the item you want to edit
* DicKey ( Facultative ) \n
The key of the word you want to display from the Lang dict , if Default . DEFAULT don ' t change value
* callback ( Facultative ) \n
The function to callback after the button is clicked , if None , don ' t link to a callback, if Default.DEFAULT, don ' t change value
* item_type ( Facultative ) \n
The type of the item to create ( see Menu . ItemType ) , if Default . DEFAULT don ' t change value
* checkdefault ( Facultative ) \n
Only use with item_type as ItemType . CHECK , define the default status of the item , True = checked , False = not checked , if Default . DEFAULT don ' t change value
* index ( Facultative ) \n
The place of the item when it display , if add at the end , if conflict , first added will use the correct index , followed by the ones in conflict , if Default . DEFAULT don ' t change value
* FORCE_ID ( Usage not recommended ) \n
Force the item to use a specific id . WARNING MIGHT OVERWRITE EXISTING ITEMS IF NOT DELETED CORRECTLY
"""
#Delete item and get the parameters
item = self . delete_item ( item_id )
#For every test : if new value = Default.DEFAULT, use previous one
if text == self . Default . DEFAULT :
text = item [ 1 ] [ 0 ]
if callback == self . Default . DEFAULT :
callback = item [ 1 ] [ 1 ]
if item_type == self . Default . DEFAULT :
item_type = item [ 1 ] [ 2 ]
if checkdefault == self . Default . DEFAULT :
checkdefault = item [ 1 ] [ 3 ]
if index == self . Default . DEFAULT :
index = item [ 1 ] [ 4 ]
#Return function
return self . create_item ( text , callback , item_type , checkdefault , index , FORCE_ID = item_id )
def get_items ( self , get_parameters : bool = False ) :
""" Get all the items in the dict of items and return them as a list of pystray.Menu objects \n
Parameter
- - - - - - - - -
* get_parameters ( Facultative ) \n
Return the items and parameters as a list of tuples """
NonIndexedItems = [ ]
NonIndexedTuples = [ ]
IndexedItems = [ ]
#Get all ids in the dic
for key in self . ItemsFromID . keys ( ) :
#Check for index key
if self . ItemsFromID [ key ] [ 1 ] [ 4 ] == None :
#Sort items that don't have a specified value for index
if get_parameters :
#Add both item and parameters
NonIndexedItems . append ( self . ItemsFromID [ key ] )
else :
#Add only items
NonIndexedItems . append ( self . ItemsFromID [ key ] [ 0 ] )
else :
#Sort items that have a specified value for index
NonIndexedTuples . append ( self . ItemsFromID [ key ] )
IndexedTuples = sorted ( NonIndexedTuples , key = lambda x : x [ 1 ] [ 4 ] )
#Add indexed tuples to the list
for Item in IndexedTuples :
if get_parameters :
#Add both item and parameters
IndexedItems . append ( Item )
else :
#Add only item
IndexedItems . append ( Item [ 0 ] )
#Add non-indexed tuple to the end of the list
for Item in NonIndexedItems :
IndexedItems . append ( Item )
return IndexedItems
def set_status ( self , item_id : int , new_value : bool | None ) :
""" Set a check box status manually \n
Parameters
- - - - - - - - - -
* item_id ( Requiered )
The id of the item you want to edit
* new_value ( Requiered ) \n
The value to set for the item ( True : Checked , False : Not Checked , None : Disabled ) """
#Check if item is in list
if item_id in self . ItemsFromID :
#Check if item is a checkable object
if self . ItemsFromID [ item_id ] [ 1 ] [ 2 ] == self . ItemType . CHECK :
#Set new value
self . ItemsFromID [ item_id ] [ 1 ] [ 3 ] [ " requested " ] = new_value
return True
else :
raise ValueError ( " Item doesn ' t have check attribute " )
raise ValueError ( " Item doesn ' t exist " )
def get_status ( self , item_id : int ) :
""" Get the status of checkbox item using id
Parameters
- - - - - - - - - -
* item_id ( Requiered ) \n
The id of the item to check \n
Return
- - - - - -
True : Box is checked | False : Box isn ' t checked | None: Box ins ' t enabled """
if item_id in self . ItemsFromID :
item = self . ItemsFromID [ item_id ] [ 0 ]
return item . checked
raise ValueError ( " Item doesn ' t exist " )
def __change_status ( self , item : pystray_MenuItem ) :
""" Private function DO NOT USE """
#Get dic
CheckStatusDic = self . ItemsFromID [ self . IDsFromItem [ item ] ] [ 1 ] [ 3 ]
#Get requested status
Requested = CheckStatusDic [ " requested " ]
#If requestedd is None, it means it've been trigerred by a menu update, don't change anything
if Requested == None :
return CheckStatusDic [ " current " ]
#Set new value and set requested to None
elif Requested == True :
CheckStatusDic [ " current " ] = True
CheckStatusDic [ " requested " ] = None
return True
#Set new value and set requested to None
elif Requested == False :
CheckStatusDic [ " current " ] = False
CheckStatusDic [ " requested " ] = None
return False
else :
raise ValueError ( " Invalid value requested " )
def __menu_callback ( self , item_id : int , callback : FunctionType , tray : pystray_Icon , item : pystray_MenuItem ) :
""" Private function DO NOT USE """
#if item is checkable, update it's stauts
if item . checked != None :
self . ItemsFromID [ item_id ] [ 1 ] [ 3 ] [ " requested " ] = not item . checked
#Call the callback functionk
callback ( )
class TrayManager ( ) :
def __init__ ( self , AppName : str , ImagePath : str | None = None , Icon : Image . Image | None = None , default_show : bool = False ) :
""" Create a pystray.Icon object linked to a Menu() object \n
Parameters
- - - - - - - - - -
* Menu ( Required ) \n
Menu ( ) instance
* AppName ( Facultative ) \n
AppName is the name of your app in the notification tray ( default = " Test App " )
* Path or Icon or Index ( Facultative ) \n
Import the image from one of those 3 ways : \n
Using ImagePath , ImagePath is the image path to use as icon ( str ) \n
Using Icon , Icon is the PIL . Image . Image object \n
Note : Your first image imported is the index 1 NOT 0 \n
If None is provided , use ' default ' icon ( white square of 32 * 32 ) which IS ALWAYS index 0 """
#Self object :
#self.Menu Menu() object
#self.Icons List of PIL.Image.Image objects, first one is a white square of 32*32
#self.tray pystray.Icon() object
self . Menu = Menu ( )
default_image = Image . new ( " L " , ( 32 , 32 ) , 255 )
self . Icons = [ default_image ]
#Set the icon of the app in the notification tray
if ImagePath :
if __os_path_exists ( ImagePath ) :
#Open image as PIL.Image.Image object and add it to Icons list
image = Image . open ( ImagePath )
self . Icons . append ( image )
else :
raise FileNotFoundError ( f " Can ' t find image at : ' { ImagePath } ' " )
elif Icon is Image . Image :
#Add image to Icons list
self . Icons . append ( Icon )
#Create pystray_Icon object
self . tray = pystray_Icon ( AppName , self . Icons [ - 1 ] , title = AppName , menu = pystray_Menu ( lambda : self . Menu . get_items ( ) ) )
#Run it in different thread (pystray.Icon.run() is a blocking function)
Thread = threading_Thread ( target = self . __run , args = ( default_show , ) )
Thread . start ( )
def set_name ( self , name : str ) :
self . tray . title = name
def set_icon ( self , ImagePath : str | None = None , Icon : Image . Image | None = None , Index : int | None = None , Show : bool = True ) :
""" Set icon of app in the notification tray \n
Parameters
- - - - - - - - - -
* Self ( Requiered ) \n
pystray . Icon object
* Path or Icon or Index ( Only one Requiered ) \n
Import the image from one of those 3 ways : \n
Using ImagePath , ImagePath is the image path to use as icon ( str ) \n
Using Icon , Icon is the PIL . Image . Image object \n
Using Index , Index is the custom key of one of the previous imported images ( Note : the ' default ' key is a white square of 32 * 32 ) \n
If None is provided , don ' t change anything and retutn False \n
* Show ( Facultative ) \n
Define if whether the icon should be displayed in the notification tray if . hide ( ) was called before ( Simillar as . show ( ) ) \n
Return
- - - - - - -
False | Index of loaded Image
"""
#Check if IconPath exist
if ImagePath :
if __os_path_exists ( ImagePath ) :
#Open image as PIL.Image.Image object and add it to Icons list
image = Image . open ( ImagePath )
self . Icons . append ( image )
Index = len ( self . Icons ) - 1
else :
raise FileNotFoundError ( f " Can ' t find image at : ' { ImagePath } ' " )
elif Image . isImageType ( Icon ) :
#Add image to Icons list
image = Icon
self . Icons . append ( image )
Index = len ( self . Icons ) - 1
elif Index != None :
if len ( self . Icons ) - 1 > = Index :
image = self . Icons [ Index ]
else :
raise IndexError ( " Index of icons out of range " )
else :
return False
#Set image as icon
self . tray . icon = image
if Show :
#Call Tray.Show() to be sure only .Show() and .Hide() have control over whether the icon is visible or not
self . show ( )
return Index
def delete_icon ( self , Index : int , All : bool = False ) :
""" Remove the icon of Index or All the icons (Except the default one) from memory \n
Parameters
- - - - - - - - - -
* Index \n
The index of the icon to remove from memory
* All \n
If True , remove all icons except the default one from memory \n
Return
- - - - - -
PIL . Image . Image object of icon removed , if All = True , return list of PIL . Image . Image object of icons removed """
if All :
Images = [ ]
#Append all images to list
for x in self . Icons [ 1 : - 1 ] :
Images . append ( self . Icons . pop ( x ) )
return Images
#If Index is correct, return value
if len ( self . Icons ) - 1 > = Index and Index != 0 :
return self . Icons . pop ( Index )
else :
raise IndexError ( " Index of icon out of range " )
def show ( self , IconPath : str = None ) :
""" Show the icon in the notification tray, if icon is already displayed, don ' t change anything \n
Parameters
- - - - - - - - - -
* Self ( Requiered ) \n
pystray . Icon object
* IconPath ( Facultatives ) \n
IconPath is the path of the image you want to use as the icon of your app in the notification tray , if not foud , raise FileNotFoundError
"""
if IconPath :
#Use Tray.SetIcon to set the icon
self . set_icon ( IconPath )
#Set status to visible
self . tray . visible = True
return
def hide ( self ) :
""" Hide the icon in the notification tray, if icon is already hidden, don ' t have an effect """
#Set status to visible
self . tray . visible = False
return
def kill ( self ) :
""" Kill the app notification in notification tray and all the menu items, returns them as list of tuple (item, parameters) \n
Note : TrayManager ( ) and Menu ( ) objetcs become useless """
#Stop the tray process and kill every menu instances, returns them as list of tuple
Items = self . Menu . get_items ( get_parameters = True )
self . tray . stop ( )
return Items
def __run ( self , default_show ) :
if default_show == False :
#Use lamda _: To avoid the problem related to the number of arguments
callback = lambda _ : self . hide ( )
#Run pystray.Icon object with callback
self . tray . run ( callback )
else :
self . tray . run ( )
class Lang ( ) :
def __init__ ( self , lang : str ) :
2023-11-14 14:40:58 +00:00
self . lang = " "