#!/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 # svg for tkinter http://code.activestate.com/lists/python-list/595078/ # Improve: # add svg support # Dependencies: # package: RPM | DPKG # tkinter: python36-tkinter | python3-tk # PIL: python36-pillow-tk | python3-pil.imagetk # pip3 install cairosvg import glob, re, cairosvg import tkinter as tk from functools import partial from pathlib import Path from sys import path # loading PIL.ImageTk after tkinter makes ImageTk use the PIL version, which supports PNG. This is important on tcl < 8.6 (that is, el7) from PIL import Image, ImageTk path.append("/home/bgirton/dev/logout-manager") import lmlib config = lmlib.Initialize_config() actions = lmlib.Actions # graphical classes and functions print("Loading graphics...") 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 tryint(s): try: return int(s) except: return s def sort_sizes(x): # Original reference so#46228101 value = x.split("/")[5] 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: try: theme = lmlib.get_gtk3_default_icon_theme() except: theme = "hicolor" # first, find all files underneath /usr/share/icons/$THEME/$SIZE print("Finding filename of icon, theme=",theme,"category=",category,"name=",name) # FUTUREIMPROVEMENT: 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. # to exclude the scalable/ contents, replace dir 5 asterisk with [0-9]* results = [] # COMMENTED to test with the svg. results = glob.glob("/usr/share/icons/"+theme+"/*/"+category+"/"+name+".{png,PNG}") results = glob.glob("/usr/share/icons/"+theme+"/*/"+category+"/"+name+".*") if len(results) == 0: # no results with that category, so try all categories results = glob.glob("/usr/share/icons/"+theme+"/*/*/"+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 photoimage_from_svg(filename = "",size = "48"): # this one works, but does not allow me to set the size. # open svg item = cairosvg.svg2png(url=filename, parent_width = size, parent_height = size) return ImageTk.PhotoImage(data=item) def image_from_svg(filename = "",size = "48"): # open svg item = cairosvg.svg2png(url=filename,parent_width = size,parent_height = size) return ImageTk.PhotoImage(data=item) 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": # this should not happen, because the Initialize_config should have checked gtk3 default value. icon_theme = "hicolor" # 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 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(filename=iconfilename, size=size) else: photo = Image.open(iconfilename) photo.thumbnail(size=[size, size]) try: photo = ImageTk.PhotoImage(photo) except Exception as e: print("Error was ",e) except: print("Error with icon file.") return None # If I ever add HiDPI support, multiple size here by the factor. So, size * 1.25 return photo class App: def __init__(self, master): frame = tk.Frame(master) frame.grid(row=0) self.photoLock = get_scaled_icon(config.get_lock_icon(), config.get_icon_size(), config.get_icon_theme()) self.buttonLock = tk.Button(frame, text="Lock", underline=3, command=partial(actions.lock,config), image=self.photoLock, compound=tk.LEFT) master.bind_all("", partial(actions.lock,config)) Tooltip(self.buttonLock, text="Hide session and require authentication to return to it") self.buttonLock.grid(row=0,column=0) 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="Close the current user session") 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) master.bind_all("", partial(actions.hibernate,config)) Tooltip(self.buttonHibernate, text="Save state to disk and power off") self.buttonHibernate.grid(row=0,column=2) 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) master.bind_all("", partial(actions.shutdown,config)) Tooltip(self.buttonShutdown, text="Power off the computer") self.buttonShutdown.grid(row=0,column=3) 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) master.bind_all("", partial(actions.reboot,config)) Tooltip(self.buttonReboot, text="Reboot the computer back to the login screen") self.buttonReboot.grid(row=0,column=4) self.buttonCancel = tk.Button(frame, text="Cancel", underline=0, command=self.quitaction) master.bind_all("", self.quitaction) Tooltip(self.buttonCancel, text="Do nothing; just close this window") self.buttonCancel.grid(row=1,columnspan=8,sticky=tk.W+tk.E) # 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") imgicon = get_scaled_icon(config.get_logout_icon(),24,config.get_icon_theme()) root.tk.call('wm','iconphoto', root._w, imgicon) app = App(root) root.mainloop() try: root.destroy() except: pass