mirror of
https://github.com/Py-KMS-Organization/py-kms.git
synced 2025-05-29 22:45:19 -04:00
517 lines
26 KiB
Python
517 lines
26 KiB
Python
#!/usr/bin/env python3
|
|
|
|
import os
|
|
import re
|
|
import sys
|
|
from collections import Counter
|
|
from time import sleep
|
|
import threading
|
|
import tkinter as tk
|
|
from tkinter import ttk
|
|
import tkinter.font as tkFont
|
|
|
|
from pykms_Format import MsgMap, unshell_message, unformat_message
|
|
|
|
#------------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
|
|
# https://stackoverflow.com/questions/3221956/how-do-i-display-tooltips-in-tkinter
|
|
class ToolTip(object):
|
|
""" Create a tooltip for a given widget """
|
|
def __init__(self, widget, bg = '#FFFFEA', pad = (5, 3, 5, 3), text = 'widget info', waittime = 400, wraplength = 250):
|
|
self.waittime = waittime # ms
|
|
self.wraplength = wraplength # pixels
|
|
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 = ttk.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
|
|
|
|
##-----------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
|
|
class TextRedirect(object):
|
|
class Pretty(object):
|
|
grpmsg = unformat_message([MsgMap[1], MsgMap[7], MsgMap[12], MsgMap[20]])
|
|
arrows = [ item[0] for item in grpmsg ]
|
|
clt_msg_nonewline = [ item[1] for item in grpmsg ]
|
|
arrows = list(set(arrows))
|
|
lenarrow = len(arrows[0])
|
|
srv_msg_nonewline = [ item[0] for item in unformat_message([MsgMap[2], MsgMap[5], MsgMap[13], MsgMap[18]]) ]
|
|
msg_align = [ msg[0].replace('\t', '').replace('\n', '') for msg in unformat_message([MsgMap[-2], MsgMap[-4]]) ]
|
|
|
|
def __init__(self, srv_text_space, clt_text_space, customcolors):
|
|
self.srv_text_space = srv_text_space
|
|
self.clt_text_space = clt_text_space
|
|
self.customcolors = customcolors
|
|
|
|
def textbox_write(self, tag, message, color, extras):
|
|
widget = self.textbox_choose(message)
|
|
self.w_maxpix, self.h_maxpix = widget.winfo_width(), widget.winfo_height()
|
|
self.xfont = tkFont.Font(font = widget['font'])
|
|
widget.configure(state = 'normal')
|
|
widget.insert('end', self.textbox_format(message), tag)
|
|
self.textbox_color(tag, widget, color, self.customcolors['black'], extras)
|
|
widget.after(100, widget.see('end'))
|
|
widget.configure(state = 'disabled')
|
|
|
|
def textbox_choose(self, message):
|
|
if any(item.startswith('logsrv') for item in [message, self.str_to_print]):
|
|
self.srv_text_space.focus_set()
|
|
self.where = "srv"
|
|
return self.srv_text_space
|
|
elif any(item.startswith('logclt') for item in [message, self.str_to_print]):
|
|
self.clt_text_space.focus_set()
|
|
self.where = "clt"
|
|
return self.clt_text_space
|
|
|
|
def textbox_color(self, tag, widget, forecolor = 'white', backcolor = 'black', extras = []):
|
|
for extra in extras:
|
|
if extra == 'bold':
|
|
self.xfont.configure(weight = "bold")
|
|
elif extra == 'italic':
|
|
self.xfont.configure(slant = "italic")
|
|
elif extra == 'underlined':
|
|
self.xfont.text_font.configure(underline = True)
|
|
elif extra == 'strike':
|
|
self.xfont.configure(overstrike = True)
|
|
elif extra == 'reverse':
|
|
forecolor, backcolor = backcolor, forecolor
|
|
|
|
widget.tag_configure(tag, foreground = forecolor, background = backcolor, font = self.xfont)
|
|
widget.tag_add(tag, "insert linestart", "insert lineend")
|
|
|
|
def textbox_newline(self, message):
|
|
if not message.endswith('\n'):
|
|
return message + '\n'
|
|
else:
|
|
return message
|
|
|
|
def textbox_format(self, message):
|
|
# vertical align.
|
|
self.w_maxpix = self.w_maxpix - 5 # pixel reduction for distance from border.
|
|
w_fontpix, h_fontpix = (self.xfont.measure('0'), self.xfont.metrics('linespace'))
|
|
msg_unformat = message.replace('\t', '').replace('\n', '')
|
|
lenfixed_chars = int((self.w_maxpix / w_fontpix) - len(msg_unformat))
|
|
|
|
if message in self.srv_msg_nonewline + self.clt_msg_nonewline:
|
|
lung = lenfixed_chars - self.lenarrow
|
|
if message in self.clt_msg_nonewline:
|
|
message = self.textbox_newline(message)
|
|
else:
|
|
lung = lenfixed_chars
|
|
if (self.where == "srv") or (self.where == "clt" and message not in self.arrows):
|
|
message = self.textbox_newline(message)
|
|
# horizontal align.
|
|
if msg_unformat in self.msg_align:
|
|
msg_strip = message.lstrip('\n')
|
|
message = '\n' * (len(message) - len(msg_strip) + TextRedirect.Pretty.newlinecut[0]) + msg_strip
|
|
TextRedirect.Pretty.newlinecut.pop(0)
|
|
|
|
count = Counter(message)
|
|
countab = (count['\t'] if count['\t'] != 0 else 1)
|
|
message = message.replace('\t' * countab, ' ' * lung)
|
|
return message
|
|
|
|
def textbox_do(self):
|
|
msgs, TextRedirect.Pretty.tag_num = unshell_message(self.str_to_print, TextRedirect.Pretty.tag_num)
|
|
for tag in msgs:
|
|
self.textbox_write(tag, msgs[tag]['text'], self.customcolors[msgs[tag]['color']], msgs[tag]['extra'])
|
|
|
|
def flush(self):
|
|
pass
|
|
|
|
def write(self, string):
|
|
if string != '\n':
|
|
self.str_to_print = string
|
|
self.textbox_do()
|
|
|
|
class Stderr(Pretty):
|
|
def __init__(self, srv_text_space, clt_text_space, customcolors, side):
|
|
self.srv_text_space = srv_text_space
|
|
self.clt_text_space = clt_text_space
|
|
self.customcolors = customcolors
|
|
self.side = side
|
|
self.tag_err = 'STDERR'
|
|
self.xfont = tkFont.Font(font = self.srv_text_space['font'])
|
|
|
|
def textbox_choose(self, message):
|
|
if self.side == "srv":
|
|
return self.srv_text_space
|
|
elif self.side == "clt":
|
|
return self.clt_text_space
|
|
|
|
def write(self, string):
|
|
widget = self.textbox_choose(string)
|
|
self.textbox_color(self.tag_err, widget, self.customcolors['red'], self.customcolors['black'])
|
|
self.srv_text_space.configure(state = 'normal')
|
|
self.srv_text_space.insert('end', string, self.tag_err)
|
|
self.srv_text_space.see('end')
|
|
self.srv_text_space.configure(state = 'disabled')
|
|
|
|
class Log(Pretty):
|
|
def textbox_format(self, message):
|
|
if message.startswith('logsrv'):
|
|
message = message.replace('logsrv ', '')
|
|
if message.startswith('logclt'):
|
|
message = message.replace('logclt ', '')
|
|
return message + '\n'
|
|
|
|
##-----------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
class TextDoubleScroll(tk.Frame):
|
|
def __init__(self, master, **kwargs):
|
|
""" Initialize.
|
|
- horizontal scrollbar
|
|
- vertical scrollbar
|
|
- text widget
|
|
"""
|
|
tk.Frame.__init__(self, master)
|
|
self.master = master
|
|
|
|
self.textbox = tk.Text(self.master, **kwargs)
|
|
self.sizegrip = ttk.Sizegrip(self.master)
|
|
self.hs = ttk.Scrollbar(self.master, orient = "horizontal", command = self.on_scrollbar_x)
|
|
self.vs = ttk.Scrollbar(self.master, orient = "vertical", command = self.on_scrollbar_y)
|
|
self.textbox.configure(yscrollcommand = self.on_textscroll, xscrollcommand = self.hs.set)
|
|
|
|
def on_scrollbar_x(self, *args):
|
|
""" Horizontally scrolls text widget. """
|
|
self.textbox.xview(*args)
|
|
|
|
def on_scrollbar_y(self, *args):
|
|
""" Vertically scrolls text widget. """
|
|
self.textbox.yview(*args)
|
|
|
|
def on_textscroll(self, *args):
|
|
""" Moves the scrollbar and scrolls text widget when the mousewheel is moved on a text widget. """
|
|
self.vs.set(*args)
|
|
self.on_scrollbar_y('moveto', args[0])
|
|
|
|
def put(self, **kwargs):
|
|
""" Grid the scrollbars and textbox correctly. """
|
|
self.textbox.grid(row = 0, column = 0, padx = 3, pady = 3, sticky = "nsew")
|
|
self.vs.grid(row = 0, column = 1, sticky = "ns")
|
|
self.hs.grid(row = 1, column = 0, sticky = "we")
|
|
self.sizegrip.grid(row = 1, column = 1, sticky = "news")
|
|
|
|
def get(self):
|
|
""" Return the "frame" useful to place inner controls. """
|
|
return self.textbox
|
|
|
|
##-----------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
def custom_background(window):
|
|
# first level canvas.
|
|
allwidgets = window.grid_slaves(0,0)[0].grid_slaves() + window.grid_slaves(0,0)[0].place_slaves()
|
|
widgets_alphalow = [ widget for widget in allwidgets if widget.winfo_class() == 'Canvas']
|
|
widgets_alphahigh = []
|
|
# sub-level canvas.
|
|
for side in ["Srv", "Clt"]:
|
|
widgets_alphahigh.append(window.pagewidgets[side]["BtnWin"])
|
|
for position in ["Left", "Right"]:
|
|
widgets_alphahigh.append(window.pagewidgets[side]["AniWin"][position])
|
|
for pagename in window.pagewidgets[side]["PageWin"].keys():
|
|
widgets_alphalow.append(window.pagewidgets[side]["PageWin"][pagename])
|
|
|
|
try:
|
|
from PIL import Image, ImageTk
|
|
|
|
# Open Image.
|
|
img = Image.open(os.path.dirname(os.path.abspath( __file__ )) + "/graphics/pykms_Keys.gif")
|
|
img = img.convert('RGBA')
|
|
# Resize image.
|
|
img.resize((window.winfo_width(), window.winfo_height()), Image.ANTIALIAS)
|
|
# Put semi-transparent background chunks.
|
|
window.backcrops_alphalow, window.backcrops_alphahigh = ([] for _ in range(2))
|
|
|
|
def cutter(master, image, widgets, crops, alpha):
|
|
for widget in widgets:
|
|
x, y, w, h = master.get_position(widget)
|
|
cropped = image.crop((x, y, x + w, y + h))
|
|
cropped.putalpha(alpha)
|
|
crops.append(ImageTk.PhotoImage(cropped))
|
|
# Not in same loop to prevent reference garbage.
|
|
for crop, widget in zip(crops, widgets):
|
|
widget.create_image(1, 1, image = crop, anchor = 'nw')
|
|
|
|
cutter(window, img, widgets_alphalow, window.backcrops_alphalow, 36)
|
|
cutter(window, img, widgets_alphahigh, window.backcrops_alphahigh, 96)
|
|
|
|
# Put semi-transparent background overall.
|
|
img.putalpha(128)
|
|
window.backimg = ImageTk.PhotoImage(img)
|
|
window.masterwin.create_image(1, 1, image = window.backimg, anchor = 'nw')
|
|
|
|
except ImportError:
|
|
for widget in widgets_alphalow + widgets_alphahigh:
|
|
widget.configure(background = window.customcolors['lavender'])
|
|
|
|
# Hide client.
|
|
window.clt_on_show(force_remove = True)
|
|
# Show Gui.
|
|
window.deiconify()
|
|
|
|
##-----------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
class Animation(object):
|
|
def __init__(self, gifpath, master, widget, loop = False):
|
|
from PIL import Image, ImageTk, ImageSequence
|
|
|
|
self.master = master
|
|
self.widget = widget
|
|
self.loop = loop
|
|
self.cancelid = None
|
|
self.flagstop = False
|
|
self.index = 0
|
|
self.frames = []
|
|
|
|
img = Image.open(gifpath)
|
|
size = img.size
|
|
for frame in ImageSequence.Iterator(img):
|
|
static_img = ImageTk.PhotoImage(frame.convert('RGBA'))
|
|
try:
|
|
static_img.delay = int(frame.info['duration'])
|
|
except KeyError:
|
|
static_img.delay = 100
|
|
self.frames.append(static_img)
|
|
|
|
self.widget.configure(width = size[0], height = size[1])
|
|
self.initialize()
|
|
|
|
def initialize(self):
|
|
self.widget.configure(image = self.frames[0])
|
|
self.widget.image = self.frames[0]
|
|
|
|
def deanimate(self):
|
|
while not self.flagstop:
|
|
pass
|
|
self.flagstop = False
|
|
self.index = 0
|
|
self.widget.configure(relief = "raised")
|
|
|
|
def animate(self):
|
|
frame = self.frames[self.index]
|
|
self.widget.configure(image = frame, relief = "sunken")
|
|
self.index += 1
|
|
self.cancelid = self.master.after(frame.delay, self.animate)
|
|
if self.index == len(self.frames):
|
|
if self.loop:
|
|
self.index = 0
|
|
else:
|
|
self.stop()
|
|
|
|
def start(self, event = None):
|
|
if str(self.widget['state']) != 'disabled':
|
|
if self.cancelid is None:
|
|
if not self.loop:
|
|
self.btnani_thread = threading.Thread(target = self.deanimate, name = "Thread-BtnAni")
|
|
self.btnani_thread.setDaemon(True)
|
|
self.btnani_thread.start()
|
|
self.cancelid = self.master.after(self.frames[0].delay, self.animate)
|
|
|
|
def stop(self, event = None):
|
|
if self.cancelid:
|
|
self.master.after_cancel(self.cancelid)
|
|
self.cancelid = None
|
|
self.flagstop = True
|
|
self.initialize()
|
|
|
|
|
|
def custom_pages(window, side):
|
|
buttons = window.pagewidgets[side]["BtnAni"]
|
|
labels = window.pagewidgets[side]["LblAni"]
|
|
|
|
for position in buttons.keys():
|
|
buttons[position].config(anchor = "center",
|
|
font = window.customfonts['btn'],
|
|
background = window.customcolors['white'],
|
|
activebackground = window.customcolors['white'],
|
|
borderwidth = 2)
|
|
|
|
try:
|
|
anibtn = Animation(os.path.dirname(os.path.abspath( __file__ )) + "/graphics/pykms_Keyhole_%s.gif" %position,
|
|
window, buttons[position], loop = False)
|
|
anilbl = Animation(os.path.dirname(os.path.abspath( __file__ )) + "/graphics/pykms_Arrow_%s.gif" %position,
|
|
window, labels[position], loop = True)
|
|
|
|
def animationwait(master, button, btn_animation, lbl_animation):
|
|
while btn_animation.cancelid:
|
|
pass
|
|
sleep(1)
|
|
x, y = master.winfo_pointerxy()
|
|
if master.winfo_containing(x, y) == button:
|
|
lbl_animation.start()
|
|
|
|
def animationcombo(master, button, btn_animation, lbl_animation):
|
|
wait_thread = threading.Thread(target = animationwait,
|
|
args = (master, button, btn_animation, lbl_animation),
|
|
name = "Thread-WaitAni")
|
|
wait_thread.setDaemon(True)
|
|
wait_thread.start()
|
|
lbl_animation.stop()
|
|
btn_animation.start()
|
|
|
|
buttons[position].bind("<ButtonPress>", lambda event, anim1 = anibtn, anim2 = anilbl,
|
|
bt = buttons[position], win = window:
|
|
animationcombo(win, bt, anim1, anim2))
|
|
buttons[position].bind("<Enter>", anilbl.start)
|
|
buttons[position].bind("<Leave>", anilbl.stop)
|
|
|
|
except ImportError:
|
|
buttons[position].config(activebackground = window.customcolors['blue'],
|
|
foreground = window.customcolors['blue'])
|
|
labels[position].config(background = window.customcolors['lavender'])
|
|
|
|
if position == "Left":
|
|
buttons[position].config(text = '<<')
|
|
elif position == "Right":
|
|
buttons[position].config(text = '>>')
|
|
|
|
##-----------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
class ListboxOfRadiobuttons(tk.Frame):
|
|
def __init__(self, master, radios, font, changed, **kwargs):
|
|
tk.Frame.__init__(self, master)
|
|
|
|
self.master = master
|
|
self.radios = radios
|
|
self.font = font
|
|
self.changed = changed
|
|
|
|
self.scrollv = tk.Scrollbar(self, orient = "vertical")
|
|
self.textbox = tk.Text(self, yscrollcommand = self.scrollv.set, **kwargs)
|
|
self.scrollv.config(command = self.textbox.yview)
|
|
# layout.
|
|
self.scrollv.pack(side = "right", fill = "y")
|
|
self.textbox.pack(side = "left", fill = "both", expand = True)
|
|
# create radiobuttons.
|
|
self.radiovar = tk.StringVar()
|
|
self.radiovar.set('FILE')
|
|
self.create()
|
|
|
|
def create(self):
|
|
self.rdbtns = []
|
|
for n, nameradio in enumerate(self.radios):
|
|
rdbtn = tk.Radiobutton(self, text = nameradio, value = nameradio, variable = self.radiovar,
|
|
font = self.font, indicatoron = 0, width = 15,
|
|
borderwidth = 3, selectcolor = 'yellow', command = self.change)
|
|
self.textbox.window_create("end", window = rdbtn)
|
|
# to force one checkbox per line
|
|
if n != len(self.radios) - 1:
|
|
self.textbox.insert("end", "\n")
|
|
self.rdbtns.append(rdbtn)
|
|
self.textbox.configure(state = "disabled")
|
|
|
|
def change(self):
|
|
st = self.state()
|
|
for widget, default in self.changed:
|
|
wclass = widget.winfo_class()
|
|
if st in ['STDOUT', 'FILEOFF']:
|
|
if wclass == 'Entry':
|
|
widget.delete(0, 'end')
|
|
widget.configure(state = "disabled")
|
|
elif wclass == 'TCombobox':
|
|
if st == 'STDOUT':
|
|
widget.set(default)
|
|
widget.configure(state = "readonly")
|
|
elif st == 'FILEOFF':
|
|
widget.set('')
|
|
widget.configure(state = "disabled")
|
|
elif st in ['FILE', 'FILESTDOUT', 'STDOUTOFF']:
|
|
if wclass == 'Entry':
|
|
widget.configure(state = "normal")
|
|
widget.delete(0, 'end')
|
|
widget.insert('end', default)
|
|
widget.xview_moveto(1)
|
|
elif wclass == 'TCombobox':
|
|
widget.configure(state = "readonly")
|
|
widget.set(default)
|
|
elif wclass == 'Button':
|
|
widget.configure(state = "normal")
|
|
|
|
def configure(self, state):
|
|
for rb in self.rdbtns:
|
|
rb.configure(state = state)
|
|
|
|
def state(self):
|
|
return self.radiovar.get()
|