#!/usr/bin/env python3 # File: logout-manager-tcl.py # License: CC-BY-SA 4.0 # Author: bgstack15 # Startdate: 2019-06-12 20:05 # Title: Tcl/tk-based logout manager # Purpose: A tcl/tk graphical program for selecting shutdown, logout, etc. # History: # Usage: # References: # http://effbot.org/tkinterbook/button.htm # http://effbot.org/tkinterbook/tkinter-application-windows.htm # http://effbot.org/tkinterbook/ # pass parameters to function of tkinter.Button(command=) https://stackoverflow.com/questions/38749620/python-3-tkinter-button-commands#38750155 # alternate for passing params https://stackoverflow.com/questions/6920302/how-to-pass-arguments-to-a-button-command-in-tkinter # https://stackoverflow.com/questions/18537918/set-window-icon#18538416 # the exact syntax for master.bind https://stackoverflow.com/questions/16082243/how-to-bind-ctrl-in-python-tkinter # https://pillow.readthedocs.io/en/stable/reference/ImageTk.html # gtk-3.0 default icon theme https://coderwall.com/p/no3qfa/setting-gtk2-and-gtk3-theme-via-config-file # homedir https://stackoverflow.com/questions/4028904/how-to-get-the-home-directory-in-python # natural sort https://stackoverflow.com/questions/46228101/sort-list-of-strings-by-two-substrings-using-lambda-function/46228199#46228199 # tooltips https://stackoverflow.com/questions/3221956/how-do-i-display-tooltips-in-tkinter/41381685#41381685 # rsvg https://stackoverflow.com/questions/10393675/rsvg-with-python-3-2-on-ubuntu/19744099#19744099 # Improve: # add svg support # Dependencies: # package: RPM | DPKG # tkinter: python36-tkinter | python3-tk # PIL: python36-pillow-tk | python3-pil.imagetk # cairo: python36-cairo | python3-cairo # Rsvg: python36-gobject | python3-gobject from gi import require_version require_version('Rsvg', '2.0') from gi.repository import Rsvg import os, sys, configparser, glob, re, cairo import tkinter as tk import tkinter.ttk as ttk from functools import partial from pathlib import Path from PIL import Image, ImageTk sys.path.append("/home/bgirton/dev/logout-manager") import lmlib #if TkVersion < 8.6: # USE_PRIVATE_TCL_IMAGES = 1 # print("Loading private PIL translation layter") # def PhotoImage_from_png(file): # return ImageTk.PhotoImage(file=file) config = lmlib.Initialize_config() actions = lmlib.Actions # graphical classes and functions print("Loading graphics...") def _alpha_blending(rgba, back): "Return a rgb tuple composed from a rgba and back(ground) tuple/list." paired = zip(rgba[:-1], back) alpha = rgba[-1] tmp = list() for upper, lower in paired: blend = (((255 - alpha) * lower) + (alpha * upper)) / 255 tmp.append(blend) return(tuple(tmp)) def convert(bgra_buffer, width, height): "Convert bgra buffer to photoimage put" idx = 0 end = len(bgra_buffer) arguments = list() while idx < end: rgba = (ord(bgra_buffer[idx + 2]), ord(bgra_buffer[idx + 1]), ord(bgra_buffer[idx + 0]), ord(bgra_buffer[idx + 3])) back = (255, 255, 255) rgb = _alpha_blending(rgba, back) arguments += rgb idx += 4 template = ' '.join(height *['{%s}' % (''.join(width*["#%02x%02x%02x"]))]) return(template % tuple(arguments)) def photoimage_from_svg(file_path_name): '''Return a Tkinter.PhotoImage with the content set to the rendered SVG.''' svg = rsvg.Handle(file=file_path_name) width, height = svg.get_dimension_data()[:2] surface = cairo.ImageSurface(cairo.FORMAT_RGB24, int(width), int(height)) context = cairo.Context(surface) svg.render_cairo(context) image = tk.PhotoImage(width=width, height=height) data = convert(surface.get_data(), width, height) image.put(data) return(image) class Tooltip: ''' It creates a tooltip for a given widget as the mouse goes on it. see: http://stackoverflow.com/questions/3221956/ what-is-the-simplest-way-to-make-tooltips- in-tkinter/36221216#36221216 http://www.daniweb.com/programming/software-development/ code/484591/a-tooltip-class-for-tkinter - Originally written by vegaseat on 2014.09.09. - Modified to include a delay time by Victor Zaccardo on 2016.03.25. - Modified - to correct extreme right and extreme bottom behavior, - to stay inside the screen whenever the tooltip might go out on the top but still the screen is higher than the tooltip, - to use the more flexible mouse positioning, - to add customizable background color, padding, waittime and wraplength on creation by Alberto Vassena on 2016.11.05. Tested on Ubuntu 16.04/16.10, running Python 3.5.2 TODO: themes styles support ''' def __init__(self, widget, *, bg='#FFFFEA', pad=(5, 3, 5, 3), text='widget info', waittime=400, wraplength=250): self.waittime = waittime # in miliseconds, originally 500 self.wraplength = wraplength # in pixels, originally 180 self.widget = widget self.text = text self.widget.bind("", self.onEnter) self.widget.bind("", self.onLeave) self.widget.bind("", self.onLeave) self.bg = bg self.pad = pad self.id = None self.tw = None def onEnter(self, event=None): self.schedule() def onLeave(self, event=None): self.unschedule() self.hide() def schedule(self): self.unschedule() self.id = self.widget.after(self.waittime, self.show) def unschedule(self): id_ = self.id self.id = None if id_: self.widget.after_cancel(id_) def show(self): def tip_pos_calculator(widget, label, *, tip_delta=(10, 5), pad=(5, 3, 5, 3)): w = widget s_width, s_height = w.winfo_screenwidth(), w.winfo_screenheight() width, height = (pad[0] + label.winfo_reqwidth() + pad[2], pad[1] + label.winfo_reqheight() + pad[3]) mouse_x, mouse_y = w.winfo_pointerxy() x1, y1 = mouse_x + tip_delta[0], mouse_y + tip_delta[1] x2, y2 = x1 + width, y1 + height x_delta = x2 - s_width if x_delta < 0: x_delta = 0 y_delta = y2 - s_height if y_delta < 0: y_delta = 0 offscreen = (x_delta, y_delta) != (0, 0) if offscreen: if x_delta: x1 = mouse_x - tip_delta[0] - width if y_delta: y1 = mouse_y - tip_delta[1] - height offscreen_again = y1 < 0 # out on the top if offscreen_again: # No further checks will be done. # TIP: # A further mod might automagically augment the # wraplength when the tooltip is too high to be # kept inside the screen. y1 = 0 return x1, y1 bg = self.bg pad = self.pad widget = self.widget # creates a toplevel window self.tw = tk.Toplevel(widget) # Leaves only the label and removes the app window self.tw.wm_overrideredirect(True) win = tk.Frame(self.tw, background=bg, borderwidth=0) label = tk.Label(win, text=self.text, justify=tk.LEFT, background=bg, relief=tk.SOLID, borderwidth=0, wraplength=self.wraplength) label.grid(padx=(pad[0], pad[2]), pady=(pad[1], pad[3]), sticky=tk.NSEW) win.grid() x, y = tip_pos_calculator(widget, label) self.tw.wm_geometry("+%d+%d" % (x, y)) def hide(self): tw = self.tw if tw: tw.destroy() self.tw = None def get_gtk3_default_theme(): # WORKHERE: move this to lmlib # abstracted so it does not clutter get_scaled_icon name = "hicolor" gtk3_config_path = os.path.join(os.path.expanduser("~"),".config","gtk-3.0","settings.ini") gtk3_config = configparser.ConfigParser() gtk3_config.read(gtk3_config_path) try: if 'Settings' in gtk3_config: name = gtk3_config['Settings']['gtk-icon-theme-name'] elif 'settings' in gtk3_config: name = gtk3_config['settings']['gtk-icon-theme-name'] except: # supposed failsafe name = "hicolor" print("Found gtk3 default theme:",name) return name def tryint(s): try: return int(s) except: return s def sort_sizes(x): # I don't even know how this works. I mashed it together from so#46228101 # WORKS return [tryint(c) for c in re.split('([0-9]+)',x.split("/")[5])] value = x.split("/")[5] #return int(re.split("[^0-9]+",value)[0]) * 100 + int( re.split("[^0-9]+",value)[1] if len(value.split("@")) >= 2 else 0 ) return mynum(value, "all") def mynum(x, type = "all"): # return the complicated numerical value for the weird size options f = re.split("[^0-9]+",x) try: f0 = int(f[0]) except: f0 = 0 try: f1 = int(f[1]) except: f1 = 0 if type == "all": return f0 * 100 + f1 if len(f) >= 2 else 0 else: return f0 def find_best_size_match(size, thelist): # return item from sorted thelist whose split("/")[5] is the first to meet or exceed the requested size try: default = thelist[-1] except: default = None return next(( i for i in thelist if mynum(i.split("/")[5],"real") >= size ), default) def get_filename_of_icon(name, theme = "hicolor", size = 48, category = "actions"): # poor man's attempt at walking through fd.o icon theme filename = None # example: Adwaita system-log-out if theme == "default" or theme is None: theme = "hicolor" # first, find all files underneath /usr/share/icons/$THEME/$SIZE # WORKHERE, allow a prioritization of category, but tolerate any category if the requested category has no matches but another category does print("Finding filename of icon, theme=",theme,"category=",category,"name=",name) # WORKHERE: this glob affects if scalable/ dir is included. When support for svg is added (which is beyond PIL), need to add a conditional and use a second glob if USE_SVG=1. results = glob.glob("/usr/share/icons/"+theme+"/*/"+category+"/"+name+".*") # the sort arranges it so a Numix/24 dir comes before a Numix/24@2x dir results = sorted(results, key=sort_sizes) #print(results) # now find the first one that matches filename = find_best_size_match(size,results) return filename def get_scaled_icon(icon_name, size = 24, icon_theme = "default", fallback_icon_name = ""): iconfilename = None # if name is a specific filename, just use it. if Path(icon_name).is_file(): #print("This is a file:",icon_name) iconfilename = icon_name else: if icon_theme == "default": # retrieve default theme from config file print("Discovering default icon theme...") icon_theme = get_gtk3_default_theme() # so now that icon_theme is defined, let us go find the icon that matches the requested name and size, in the actions category #print("Using icon theme",icon_theme) iconfilename = get_filename_of_icon(name=icon_name, theme=icon_theme, size=size, category=config.get_icon_category()) # So now that we think we have derived the correct filename... try: print("Trying icon file",iconfilename) # try an svg if re.compile(".*\.svg").match(iconfilename): print("Trying svg...") photo = photoimage_from_svg(iconfilename) else: photo = Image.open(iconfilename) except: print("Error with icon file.") return None # If I ever add HiDPI stuff, multiple size here by the factor. So, size * 1.25 photo.thumbnail(size=[size, size]) try: photo2 = ImageTk.PhotoImage(photo) except Exception as e: print("Error was ",e) return photo2 class App: def __init__(self, master): frame = tk.Frame(master) frame.grid(row=0) self.buttonLock = tk.Button(frame, text="Lock", underline=3, command=partial(actions.lock,config)) self.buttonLock.grid(row=0,column=0) # WORKS master.bind_all("", something) # PASSES 2 params when expecting 1 master.bind_all("", self.buttonLock.invoke) master.bind_all("", partial(actions.lock,config)) # WORKS, for basic image loading. #self.photoLogout = get_scaled_icon("/usr/share/icons/Adwaita/48x48/actions/system-log-out.png") #self.buttonLogout = tk.Button(frame, text="Logout", underline=0, command=lambda: actions.logout(config), image=self.photoLogout, compound=TOP) #self.photoLogout1 = Image.open("/usr/share/icons/Adwaita/48x48/actions/system-log-out.png") #self.photoLogout1.thumbnail(size=[24,24]) #self.photoLogout = ImageTk.PhotoImage(self.photoLogout1,size="24x24") #self.photoLogout = get_scaled_icon("/usr/share/icons/Adwaita/48x48/actions/system-log-out.png", 24) #self.photoLogout = get_scaled_icon("system-log-out", 24, icon_theme="default") print("Using configed icon theme:",config.get_icon_theme()) self.photoLogout = get_scaled_icon(config.get_logout_icon(), config.get_icon_size(), config.get_icon_theme()) self.buttonLogout = tk.Button(frame, text="Logout", underline=0, command=lambda: actions.logout(config), image=self.photoLogout, compound=tk.LEFT) master.bind_all("", partial(actions.logout,config)) Tooltip(self.buttonLogout, text="HERE IS SOMETHING!") self.buttonLogout.grid(row=0,column=1) self.photoHibernate = get_scaled_icon(config.get_hibernate_icon(), config.get_icon_size(), config.get_icon_theme()) self.buttonHibernate = tk.Button(frame, text="Hibernate", underline=0, command=lambda: actions.hibernate(config), image=self.photoHibernate, compound=tk.LEFT) self.buttonHibernate.grid(row=0,column=2) master.bind_all("", partial(actions.hibernate,config)) self.photoShutdown = get_scaled_icon(config.get_shutdown_icon(), config.get_icon_size(), config.get_icon_theme()) self.buttonShutdown = tk.Button(frame, text="Shutdown", underline=0, command=lambda: actions.shutdown(config), image=self.photoShutdown, compound=tk.LEFT) self.buttonShutdown.grid(row=0,column=3) master.bind_all("", partial(actions.shutdown,config)) self.photoReboot = get_scaled_icon(config.get_reboot_icon(), config.get_icon_size(), config.get_icon_theme()) self.buttonReboot = tk.Button(frame, text="Reboot", underline=0, command=lambda: actions.reboot(config), image=self.photoReboot, compound=tk.LEFT) self.buttonReboot.grid(row=0,column=4) master.bind_all("", partial(actions.reboot,config)) #self.buttonCancel = tk.Button(frame, text="Cancel", underline=0, command=frame.quit) self.buttonCancel = tk.Button(frame, text="Cancel", underline=0, command=self.quitaction) self.buttonCancel.grid(row=1,columnspan=8,sticky=tk.W+tk.E) master.bind_all("", self.quitaction) # Found this after trial and error. def quitaction(self,b=None): print("Cancel any logout action.") root.destroy() # Left here as an example for a mster.bind_all that works. #def something(event=None): # print("Got here!") root = tk.Tk() # MAIN LOOP root.title("Log out options") #root.iconbitmap(r'/usr/share/icons/Numix/48/actions/system-logout.svg') imgicon = get_scaled_icon(config.get_logout_icon(),24) #if USE_PRIVATE_TCL_IMAGES is None: # #imgicon = PhotoImage(file=os.path.join("/usr/share/icons/Adwaita/24x24/actions","system-log-out.png")) # imgicon = get_scaled_icon(config.get_logout_icon(),24) #else: # #imgicon = PhotoImage_from_png(file=os.path.join("/usr/share/icons/Adwaita/24x24/actions","system-log-out.png")) # imgicon = get_scaled_icon(config.get_logout_icon(),24) print(imgicon) root.tk.call('wm','iconphoto', root._w, imgicon) app = App(root) root.mainloop() try: root.destroy() except: pass