#!/usr/bin/env python3
# File: logout-manager-tcl
# 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:
#    logout-manager-tcl
# 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 <Alt-k> 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
# Improve:
# Dependencies:
#    Devuan: python3-tk python3-pil.imagetk python3-cairosvg
#    el7: python36-tkinter python36-pillow-tk ( pip3 install cairosvg )

import glob, os, re, sys
import tkinter as tk
from distro import linux_distribution
from functools import partial
from pathlib import Path
from dotenv import load_dotenv
# 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

LM_USE_SVG = 0
try:
   from cairosvg import svg2png
   LM_USE_SVG = 1
except:
   print("WARNING: Unable to import cairosvg. No svg images will be displayed.")
   LM_USE_SVG = 0

# all this to load the libpath
try:
   defaultdir="/etc/sysconfig"
   thisplatform = linux_distribution()[0].lower()
   if 'debian' in thisplatform or 'devuan' in thisplatform:
      defaultdir="/etc/default"
   # load_dotenv keeps existing environment variables as higher precedent
   load_dotenv(os.path.join(defaultdir,"logout-manager"))
except:
   pass
if 'LOGOUT_MANAGER_LIBPATH' in os.environ:
   for i in os.environ['LOGOUT_MANAGER_LIBPATH'].split(":"):
      sys.path.append(i)
sys.path.append("/usr/share/logout-manager")
import lmlib

# 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("<Enter>", self.onEnter)
        self.widget.bind("<Leave>", self.onLeave)
        self.widget.bind("<ButtonPress>", 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)
    # to exclude the scalable/ contents, replace dir 5 asterisk with [0-9]*
   results = []
   base_dir="/usr/share/icons/"
   file_filters = ".*"
   if LM_USE_SVG == 0:
      file_filters = ".{png,PNG}"
   # I have no idea if this is xdg icon theme compliant, but it is a valiant attempt.
   # 1. try (requested) req-theme, req-category, req-name first
   results = glob.glob(base_dir+theme+"/*/"+category+"/"+name+file_filters)
   # 2. try req-theme, (generic) gen-category, req-name
   if len(results) == 0:
      # no results with that category, so try all categories
      results = glob.glob(base_dir+theme+"/*/*/"+name+file_filters)
   # 3. try "gnome", req-category, req-name
   if len(results) == 0:
      results = glob.glob(base_dir+"gnome"+"/*/"+category+"/"+name+file_filters)
   # 4. try "gnome", gen-category, req-name
   if len(results) == 0:
      results = glob.glob(base_dir+"gnome"+"/*/*/"+name+file_filters)
   # 5. try "hicolor", req-category, req-name
   if len(results) == 0:
      results = glob.glob(base_dir+"hicolor"+"/*/"+category+"/"+name+file_filters)
   # 6. try "hicolor", gen-category, req-name
   if len(results) == 0:
      results = glob.glob(base_dir+"hicolor"+"/*/*/"+name+file_filters)

   # 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.
   # this is kept as an example of how to open a svg without saving to a file.
   # open svg
   item = svg2png(url=filename, parent_width = size, parent_height = size)
   return ImageTk.PhotoImage(data=item)

def empty_photoimage(size=24):
   photo = Image.new("RGBA",[size,size])
   return ImageTk.PhotoImage(image=photo)

def image_from_svg(filename = "",size = "48"):
   # open svg
   if LM_USE_SVG == 1:
      svg2png(url=filename,write_to="/tmp/lm_temp_image.png",parent_width = size,parent_height = size)
      photo = Image.open("/tmp/lm_temp_image.png")
   else:
      photo = Image.new("RGBA",[size,size])
   return photo

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 = image_from_svg(filename=iconfilename, size=size)
      else:
         photo = Image.open(iconfilename)
   except Exception as f:
      print("Error with icon file.")
      print(f)
      return empty_photoimage()
   photo.thumbnail(size=[size, size])
   try:
      photo = ImageTk.PhotoImage(photo)
   except Exception as e:
      print("Error was ",e)
   # 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("<Alt-k>", 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("<Alt-l>", 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("<Alt-h>", 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("<Alt-s>", 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("<Alt-r>", 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("<Alt-c>", 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!")

# load configs
config = lmlib.Initialize_config(os.environ['LOGOUT_MANAGER_CONF'])
actions = lmlib.Actions

# MAIN LOOP
root = tk.Tk()
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
