trick/trick_source/trick_gsetup/main.py
2021-07-20 13:59:56 -05:00

1200 lines
48 KiB
Python
Executable File

#!/usr/bin/python3
# import PIL
from posixpath import curdir
import tkinter
# import ttkthemes
import tkinter as tk
from tkinter import PhotoImage, StringVar, Tk, ttk
#from ttkthemes import ThemedTk
from tkinter import BooleanVar, Toplevel, Text, Menu, Canvas
from tkinter.constants import ANCHOR, NONE, SUNKEN
from tkinter.ttk import Frame, Button, Entry, Label, Checkbutton, LabelFrame, Radiobutton, Scrollbar
from tkinter import ttk
import json
from tkinter import filedialog
from tkinter.ttk import Notebook
import subprocess
from tkinter import messagebox
import logging
from tkinter.scrolledtext import ScrolledText
import traceback
import os
import argparse
import shutil
import time
import errno
import ntpath
import glob
# from idlelib.ToolTip import *
#TODO: Write test case for this function
def QuoteForPOSIX(string): #Adapted from https://code.activestate.com/recipes/498202-quote-python-strings-for-safe-use-in-posix-shells/
'''quote a string so it can be used as an argument in a posix shell
According to: http://www.unix.org/single_unix_specification/
2.2.1 Escape Character (Backslash)
A backslash that is not quoted shall preserve the literal value
of the following character, with the exception of a <newline>.
2.2.2 Single-Quotes
Enclosing characters in single-quotes ( '' ) shall preserve
the literal value of each character within the single-quotes.
A single-quote cannot occur within single-quotes.
'''
return "\\'".join("'" + p + "'" for p in string.split("'"))
def get_configure_command(command, config_json, include_vars=False):
def get_with_catch(my_dict, key):
try:
return my_dict[key]
except KeyError as e:
raise RuntimeError(f"Required key {e} not found in the following json: {my_dict}")
sep = " "
vars = ""
for section_name, section in get_with_catch(config_json, "sections").items():
for option_name, option in get_with_catch(section, "options").items():
if get_with_catch(option, "type") in ("bool", "flag"):
value = bool_to_string(string_to_bool(str(get_with_catch(option, "value"))))
elif get_with_catch(option, "type") in ("dir", "string"):
value = str(get_with_catch(option, "value"))
if value == "":
continue
elif get_with_catch(option, "type") == "envvar":
value = str(get_with_catch(option, "value"))
if value == "":
if option_name in os.environ:
del os.environ[option_name]
else:
os.environ[option_name] = value
if include_vars:
vars += f"{option_name} = {value}\n"
continue
elif get_with_catch(option, "type") in ("radio"):
value = str(get_with_catch(option, "value"))
else:
my_type = get_with_catch(option, "type")
raise RuntimeError(f"In function call get_configure_command: Option type '{my_type}' in {option} is not implemented yet.")
if value not in ("no"): #TODO: Check what possible values there are for false
#TODO: Should we add the no's to the comand
command += f"{sep}--{option_name}"
if option["type"] != "flag" and value not in ("EMPTY"): #TODO: Tell the developer this is a key word
value = QuoteForPOSIX(value)
command += f"={value}"
if include_vars:
command = vars + command
return command
def string_to_bool(string):
if string.lower() in ("yes", "true"):
return True
else:
return False
def bool_to_string(bool):
if bool:
return "yes"
else:
return "no"
def run(program, *args, **kargs):
time = kargs.get("time", False)
new_args = []
for key in kargs:
value = kargs[key]
new_args.append(f"--{key}={value}")
for value in args:
new_args.append(f"--{value}")
if time:
program = "time " + program
cmd = str(program + " " + " ".join(new_args))
logging.info("Running: " + cmd)
process = subprocess.run(cmd, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, shell=True)
return process.stdout.decode()
def textEvent(e):
logging.debug(f"state: {e.state}")
logging.debug(f"key: {e.keysym}")
if (e.state == 20 and e.keysym == "c"): #TODO: Add other exceptions like Ctrl+a
return
else:
return "break"
def set_widget_geometry(widget, width=None, height=None):
"""Set the geometry of a widget. Default to half of the screen size. If width or height is greater then the screen size will use default value for that demension"""
screen_width = widget.winfo_screenwidth()
screen_height = widget.winfo_screenheight()
if not (width and width <= screen_width):
if width:
logging.warning("Trying to set width larger than screen size. Defaulting to half of screen width")
width = screen_width/2
if not (height and height <= screen_height):
if height:
logging.warning("Trying to set height larger than screen size. Defaulting to half of screen height")
height = screen_height/2
widget.geometry(f"{int(width)}x{int(height)}")
#Adapted from https://stackoverflow.com/questions/4770993/how-can-i-make-silent-exceptions-louder-in-tkinter
class Stderr(object):
def __init__(self, parent):
self.txt = Text(parent)
self.pack(self.txt, )
def write(self, s):
self.txt.insert('insert', s)
def fileno(self):
return 2
class Data:
def __create_attribute_list(self):
try:
self._attrs_
except:
dict.__setattr__(self, "_attrs_", []) #We use this list to perserve order
def __init__(self, **kargs) -> None:
self.__create_attribute_list()
for key, value in kargs.items():
self._attrs_.append(key)
if type(value) != dict:
setattr(self, key, value)
else:
setattr(self, key, Data(**value))
def _dict_(self):
d = {}
for attribute in self._attrs_:
# for attribute in dir(self):
if not attribute.startswith("_"):
var = getattr(self, attribute)
if type(var) == Data:
d[attribute] = var._dict_()
else:
d[attribute] = var
return d
def __setattr__(self, name: str, value) -> None:
self.__create_attribute_list()
self._attrs_.append(name)
dict.__setattr__(self, name, value)
class Component:
def __init__(self, parent, name, source:Data, special_valid_params, special_required_params) -> None:
self.parent = parent
self.frame = Frame(parent)
self.name = name
self.source = source
self.params = [x for x in dir(self.source) if not x.startswith("_")]
self.required_params = special_required_params
self.valid_params = special_valid_params
for p in self.required_params:
if p not in self.params:
raise RuntimeError(f"Parameter {p} is required and not found in object '{source}.{name}'")
for key in self.params:
if key not in self.valid_params:
raise RuntimeError(f"Parameter '{key}' in '{name}' is not a valid param. Valid params are {self.valid_params}.")
setattr(self, key, getattr(self.source, key))
for key in list(set(self.params).symmetric_difference(set(self.valid_params))):
setattr(self, key, "default")
self.params.append(key)
def get_hidden(self):
try:
return string_to_bool(self.hidden)
except:
return False
def pack(self, tk, **kargs):
if not self.get_hidden():
tk.pack(kargs)
def grid(self, tk, **kargs):
if not self.get_hidden():
tk.grid(kargs)
def get_frame(self):
return self.frame
class Option(Component):
def __init__(self, parent, section, name, data, special_valid_params = [], special_required_params=[]) -> None:
self.source_attribute = "value"
required_params = ["type"]
valid_params = ["type", "value", "label", "desc", "hidden", "fill", "side", "expand"]
super().__init__(parent, name, getattr(getattr(getattr(getattr(data, "sections"), section), "options"), name), special_required_params=special_required_params + required_params, special_valid_params=special_valid_params + valid_params)
self.fill = "both" if self.fill == "default" else self.fill
self.side = "top" if self.side == "default" else self.side
self.expand = False if self.expand == "default" else self.expand
@property
def value(self):
return getattr(self.source, self.source_attribute)
@value.setter
def value(self, value):
setattr(self.source, self.source_attribute, value)
def get_frame(self):
return self.frame
class ToolTip(object): #Adapted from https://stackoverflow.com/questions/20399243/display-message-when-hovering-over-something-with-mouse-cursor-in-python
def __init__(self, widget):
self.widget = widget
self.tipwindow = None
self.id = None
self.x = self.y = 0
def showtip(self, text):
"Display text in tooltip window"
self.text = text
if self.tipwindow or not self.text:
return
x, y, cx, cy = self.widget.bbox("insert")
x = x + self.widget.winfo_rootx() + 57
y = y + cy + self.widget.winfo_rooty() +27
self.tipwindow = tw = Toplevel(self.widget)
tw.wm_overrideredirect(1)
tw.wm_geometry("+%d+%d" % (x, y))
label = Label(tw, text=self.text, justify="left",
background="#ffffe0", relief="solid", borderwidth=1,
font=("tahoma", "8", "normal"))
label.pack(ipadx=1)
def hidetip(self):
tw = self.tipwindow
self.tipwindow = None
if tw:
tw.destroy()
def CreateToolTip(widget, text):
toolTip = ToolTip(widget)
def enter(event):
toolTip.showtip(text)
def leave(event):
toolTip.hidetip()
widget.bind('<Enter>', enter)
widget.bind('<Leave>', leave)
class OptionDir(Option):
def __init__(self, parent, section, name, data):
super().__init__(parent, section, name, data, special_valid_params=["width"])
# Setting defaults
self.width = 20 if self.width == "default" else self.width
self.label = self.name if self.label == "default" else self.label
self.value = "" if self.value == "default" else self.value
#Building GUI
self.container = self.get_frame()
self.container = LabelFrame(self.get_frame(), text=f"{self.label} - {self.desc}")
self.pack(self.container, fill="both", expand=True)
# self.label_tk = Label(self.container, text=self.label)
# self.pack(self.label_tk, side="left")
self.directory_entry = Entry(self.container, width=self.width)
self.directory_entry.bind('<KeyRelease>', self.handler)
self.directory_entry.insert(0, self.value)
self.pack(self.directory_entry, side="left", fill="both", expand=True)
self.browse_button = Button(self.container, text="browse", command=self.browse_dir)
self.pack(self.browse_button, side="right")
CreateToolTip(self.browse_button, "Browse for a directory.")
# self.desc_label = Label(self.container, text = self.desc, font=("", 8)) #TODO: Make a hover-over pop up
# CreateToolTip(self.desc_label, self.desc)
# self.pack(self.desc_label, side="left")
def handler(self, event):
logging.debug(f"Setting value to {self.directory_entry.get()}")
self.value = self.directory_entry.get()
def browse_dir(self):
initDir=self.value
if initDir=="":
initDir=os.getcwd()
if not os.path.isdir(initDir):
messagebox.showerror("Error", f'Specified directory not found. Value was:{"(Empty)" if initDir=="" else initDir}')
initDir=""
dir = filedialog.askdirectory(initialdir=initDir)
if not dir in ("", ()): #askdirectory can return an empty tuple(Escape pressed) or an empty string(Cancel pressed)
self.directory_entry.delete(0, "end")
self.directory_entry.insert(0, dir)
self.handler(None)
class OptionBool(Option):
def __init__(self, parent, section, name, data):
super().__init__(parent ,section, name, data)
#Setting defaults
self.value = "no" if self.value == "default" else self.value
self.label = self.name if self.label == "default" else self.label
#Building GUI
self.bool = BooleanVar(value = self.value)
self.check_button = Checkbutton(self.get_frame(), text=self.label, command=self.handler, variable=self.bool)
self.pack(self.check_button, side="left")
self.desc_label = Label(self.get_frame(), text = f": {self.desc}") #TODO: Make a pop up
self.pack(self.desc_label, side="left")
# CreateToolTip(self.check_button, self.desc)
def handler(self):
logging.debug(f"Setting value to {self.bool.get()}.")
self.value = "yes" if self.bool.get() else "no"
class OptionString(OptionDir):
def __init__(self, parent, section, name, data):
super().__init__(parent, section, name, data)
self.container["text"] = self.container["text"]
self.browse_button.pack_forget()
class OptionEnvVar(OptionDir):
def __init__(self, parent, section, name, data):
super().__init__(parent, section, name, data)
self.container["text"] = "ENV: " + self.container["text"]
self.browse_button.pack_forget()
# self.value = "" if self.value == "default" else self.value
# self.label = self.name if self.label == "default" else self.label
# self.tk_label = Label(self.get_frame(), text=self.label)
# self.pack(self.tk_label, side="left", pady=10)
# self.directory_entry = Entry(self.get_frame())
# self.directory_entry.bind('<KeyRelease>', self.handler)
# self.directory_entry.insert(0, self.value)
# self.pack(self.directory_entry, fill="both", expand=True, side="left")
# def handler(self, event):
# logging.debug(f"Setting value to {self.directory_entry.get()}")
# self.value = self.directory_entry.get()
class OptionRadio(Option):
def __init__(self, parent, section, name: str, data: Data):
super().__init__(parent, section, name, data, special_valid_params=["options"], special_required_params=[])
self.options = [] if self.options == "default" else self.options
self.value = "" if self.value == "default" else self.value
self.box = LabelFrame(self.get_frame(), text=f"{self.name} - {self.desc}")
self.pack(self.box, side="left")
self.variable = StringVar(value=self.value)
for key, obj in self.options._dict_().items():
desc = obj.get("desc", "")
value = obj.get("value", key)
if len(desc) > 0:
desc = " - " + desc
self.pack(Radiobutton(self.box, text=f"{key}{desc}", variable = self.variable, value=value, command=lambda: self.handler()), anchor="w")
def handler(self):
if self.variable.get() == self.value:
self.variable.set("")
logging.debug(f"Setting value to {self.variable.get()}")
self.value = self.variable.get()
class Section(Component):
def __init__(self, parent, section, data:Data): #TODO: Figure out if I can pass in data instead of making it global
valid_params = ["options", "size"] #TODO: Use size or take it out of valid params
required_params = ["options"]
super().__init__(parent, section, getattr(getattr(data, "sections"), section), special_valid_params=valid_params, special_required_params=required_params)
self.scrollable = self.get_scrollable_frame(self.get_frame())
self.components = {}
if type(parent) == Notebook:
parent.add(self.get_frame(), text=section)
options = getattr(self.source, "options")._dict_()
for option in options: #TODO: Don't repeat this logic in get_configure_command
obj = getattr(getattr(self.source, "options"), option)
my_type = obj.type
if my_type == "dir":
self.components[option] = OptionDir(self.get_scrollable(), section, option, data)
elif my_type == "bool" or my_type == "flag":
self.components[option] = OptionBool(self.get_scrollable(), section, option, data)
elif my_type == "envvar":
self.components[option] = OptionEnvVar(self.get_scrollable(), section, option, data)
elif my_type == "radio":
self.components[option] = OptionRadio(self.get_scrollable(), section, option, data)
elif my_type == "string":
self.components[option] = OptionString(self.get_scrollable(), section, option, data)
else:
raise RuntimeError(f"Option type '{my_type}' in {option} is not implemented yet.")
# self.components[option].get_frame().pack(fill="both", expand=1, side="top")
self.pack(self.components[option].get_frame(), fill = self.components[option].fill, expand = self.components[option].expand)
def get_scrollable(self):
if self.scrollable:
return self.scrollable
else:
return self.get_frame()
def get_frame(self):
return self.frame
def get_required_height(self):
total = 0
for component in self.components.values():
total += component.get_frame().winfo_height()
return total
def update_scrollbar(self):
if self.get_required_height() < self.main_frame.winfo_height():
self.my_scrollbar.pack_forget()
self.scrollable = False
else:
self.my_scrollbar.pack(side="right", fill="y")
self.scrollable = True
def get_scrollable_frame(self, parent):
self.main_frame = Frame(parent)
self.main_frame.pack(fill="both", expand=True)
self.main_frame.bind("<Configure>", lambda e: self.update_scrollbar())
self.my_canvas = Canvas(self.main_frame)
self.my_canvas.pack(side="left", fill="both", expand=True)
self.my_scrollbar = ttk.Scrollbar(master=self.main_frame, orient="vertical", command=self.my_canvas.yview)
self.my_canvas.configure(yscrollcommand=self.my_scrollbar.set)
second_frame = Frame(self.my_canvas)
canvasFrame = self.my_canvas.create_window((0, 0), window=second_frame, anchor="nw")
self.setIsInCanvas(False)
second_frame.bind("<Configure>", lambda e: self.my_canvas.configure(scrollregion=self.my_canvas.bbox("all")))
self.my_canvas.bind('<Configure>', lambda e: self.my_canvas.itemconfig(canvasFrame, width=e.width))
self.my_canvas.bind('<Enter>', lambda e: self.setIsInCanvas(True))
self.my_canvas.bind('<Leave>', lambda e: self.setIsInCanvas(False))
return second_frame
def setIsInCanvas(self, bool):
self.isInCanvas = bool
def _scroll(self, dir):
if self.scrollable:
if self.isInCanvas:
if self.get_frame().winfo_ismapped():
speed = 1
self.my_canvas.yview_scroll(dir * speed, "units")
def scroll_up(self):
self._scroll(1)
def scroll_down(self):
self._scroll(-1)
class App(Component):
def __init__(self, my_json_or_filename, program="/home/cherpin/git/trick/configure", resource_folder = f'{os.path.dirname(os.path.realpath(__file__))}/resources'):
if type(my_json_or_filename) == str: #Handle a file name
self.open(my_json_or_filename)
self.filename = my_json_or_filename
elif type(my_json_or_filename == dict): #Handle a dictionary object
self.filename = None
self.data = Data(**my_json_or_filename)
self.my_json = my_json_or_filename
else:
raise RuntimeError(f"Invalid parameter my_json_or_file: {my_json_or_filename}.")
self._program = program
self.resource_folder = resource_folder
self.root = tkinter.Tk()
# self.root = ThemedTk() #TODO: Figure out how to run this without pip install.
# self.root.get_themes()
# self.root.set_theme("plastik")
set_widget_geometry(self.root) #TODO: Set geometry based on width of notebook
# self.root.geometry("+-1000+-1000")
super().__init__(self.root, "app", self.data, special_required_params=["sections"], special_valid_params=["sections", "name", "landing"])
self.name = "app" if self.name == "default" else self.name
self.root.title(self.name)
self.root.minsize(width=500, height=400)
# self.root.maxsize(width=800, height=800)
self.root.report_callback_exception = self.report_callback_exception
self.header = Frame(self.root)
self.header.pack(side = "top", fill="x")
self.footer = Frame(self.root)
self.footer.pack(side="bottom", fill="x")
self.options_title = "Options for script"
self.notebook_label_frame = LabelFrame(self.root, text=self.options_title)
self.notebook_label_frame.pack(expand=True, fill="both")
self.body = Frame(self.notebook_label_frame)
self.body.pack(expand=True, fill="both")
def switch_tab(dir):
total_number_of_tabs = len(self.showing["sections"])
if total_number_of_tabs > 0:
showing = list(self.showing["sections"])
next_id = showing.index(self.notebook_name) + dir
if total_number_of_tabs - 1 < next_id:
next_id = list(self.sections).index(showing[0])
elif next_id < 0:
next_id = list(self.sections).index(showing[total_number_of_tabs - 1])
else:
next_id = list(self.sections).index(showing[next_id])
self.notebook.select(next_id)
navigation_frame = Frame(self.body)
navigation_frame.pack(anchor="e")
tab_right_button = Button(navigation_frame, text="right", command=lambda: switch_tab(1)) #TODO: Make this a picture
tab_right_button.pack(side="right")
tab_left_button = Button(navigation_frame, text="left", command=lambda: switch_tab(-1)) #TODO: Make this a picture
tab_left_button.pack(side="right")
self.add_shortcuts()
self.build_menu(self.root)
self.build_search_bar(self.header)
self.build_current_script(self.footer)
self.notebook_frame = Frame(self.body)
self.build_notebook(self.body)
self.build_current_command() #We can only run this after we build a notebook
self._status = StringVar()
self.status_label = Label(self.footer, textvariable=self._status)
self.set_status()
self.status_label.pack(side="left")
@property
def program(self):
return self._program
@program.setter
def program(self, value):
self._program = value
self.update_status()
self.build_current_command()
def set_status(self, msg=None):
if msg is None:
msg = f"Config file: {self.filename}"
self._status.set("Status - " + msg)
def add_shortcuts(self):
self.root.bind(f"<Alt-h>", lambda e: self.show_help())
self.root.bind(f"<Alt-e>", lambda e: self.execute())
self.root.bind(f"<Alt-o>", lambda e: self.focus_options())
self.root.bind(f"<Alt-s>", lambda e: self.focus_search())
def focus_options(self):
self.notebook_label_frame.focus_set()
def focus_search(self):
self.search_entry.focus_set()
def conf(self, e):
self.body.update()
height = self.body.winfo_height()
width = self.body.winfo_width()
self.notebook.configure(height=height, width=width)
def build_notebook(self, parent):
self.notebook = ttk.Notebook(parent)
# self.body.bind("<Configure>", self.conf)
self.notebook.pack(fill="both", expand=True)
self.sections = {}
sections = getattr(self.source, "sections")._dict_()
for section in sections:
obj = getattr(getattr(self.source, "sections"), section)
if len(getattr(obj, "options")._dict_()) > 0: #Note: not adding section if empty
self.sections[section] = Section(self.notebook, section, self.source)
CreateToolTip(self.sections[section].get_frame(), section)
self.previous_section_length = 0
def call_func_on_obj(obj, func):
if obj:
getattr(obj, func)()
self.get_frame().bind_all('<Button-4>', lambda e: call_func_on_obj(self.sections.get(self.notebook_name), "scroll_down"))
self.get_frame().bind_all('<Button-5>', lambda e: call_func_on_obj(self.sections.get(self.notebook_name), "scroll_up"))
self.call_search()
@property
def notebook_name(self):
if len(self.showing["sections"]) > 0:
return self.notebook.tab(self.notebook.select(), "text")
def build_search_bar(self, parent):
#Search box
# SearchBox(self).get_frame().pack(anchor="e")
self.outer_search_box = LabelFrame(parent, text="Filter Options")
self.outer_search_box.pack(side="left", anchor="n", fill="x", expand=1)
self.img = PhotoImage(file=f'{self.resource_folder}/trick_small.gif')
Label(self.outer_search_box, image=self.img).pack(side="right")
self.search_box = Frame(self.outer_search_box)
self.search_box.rowconfigure(0, weight=1)
self.search_box.columnconfigure(0, weight=1)
self.search_label = Label(self.search_box, text = "Search for options:", underline=0)
# self.search_label.grid(row=0, column=0, sticky="ew")
self.search_label.pack(expand=True, fill="x")
self.search_entry = Entry(self.search_box)
self.search_entry.bind("<KeyRelease>", self.call_search)
CreateToolTip(self.search_entry, "Search for a specific option.")
# self.search_entry.grid(row=0, column=1, sticky="ew")
self.search_entry.pack(expand=True, fill="x")
self.pack(self.search_box, side="top", anchor="e", expand=True, fill="x")
self.only_checked = BooleanVar(False)
self.checked_toggle = Checkbutton(self.outer_search_box, variable=self.only_checked, text="Show only used options", command=self.call_search)
self.checked_toggle.pack(side="right", anchor="e", expand=True, fill="x")
#End Search box
def build_current_script(self, parent):
#Current Script
self.current_script = Frame(parent)
self.current_script.pack(side="top", anchor="n", fill="x", expand=True)
self.label_frame = LabelFrame(self.current_script, text="Current Script with Options", underline=21)
self.label_frame.pack(side="top", expand=True, fill="x")
# self.win = tk.Toplevel()
# self.win.title("General help for the configure script")
# self.win.geometry("800x500")
# output = run(self.program, "help")
# self.output = ScrolledText(self.win, state="normal", height=8, width=50)
# self.output.insert(1.0, output)
# self.output["state"] = "disabled"
# self.pack(self.output, fill="both", expand=True, anchor="w")
self.current_command = ScrolledText(self.label_frame, height=4, state="normal")
self.current_command.bind("<Key>", textEvent)
self.current_command.bind("<Enter>", lambda e: self.setIsInCurrentCommand(True))
self.current_command.bind("<Leave>", lambda e: self.setIsInCurrentCommand(False))
self.current_command.pack(side="top", anchor="w", fill="x", expand=True)
self.setIsInCurrentCommand(False)
self.root.bind("<KeyRelease>", self.build_current_command)
self.root.bind("<ButtonRelease-1>", self.build_current_command)
self.status_frame = Frame(self.label_frame)
self.status_frame.pack()
status, color = self.get_status()
self.label_status = Label(self.status_frame, text=f"Status: {status}", foreground=color)
self.label_status.pack()
self.button_frame = Frame(self.label_frame)
self.button_frame.pack()
self.help_button = Button(self.button_frame, text=f"Help for script", command=self.show_help, underline=0)
self.help_button.pack(side="left", anchor="w", expand=True, fill="both", padx=10)
self.done_button = Button(self.button_frame, text="Execute command with options (will remember settings)", command=self.execute, underline=0)
CreateToolTip(self.done_button, "Execute command with options")
self.done_button.pack(side="right", anchor="e", expand=True, fill="both", padx=5)
def setIsInCurrentCommand(self, value):
self.isInCurrentCommand = value
def update_status(self):
self.label_status["text"], self.label_status["foreground"] = self.get_status()
def get_status(self):
rvalue = ""
color = "black"
if os.access(self.program, os.X_OK):
rvalue += "Valid"
color = "green"
else:
rvalue += "Invalid"
color = "red"
return rvalue + " Executable File", color
def show_help(self): #TODO: This code is being repeated where we a ScrolledText widget
self.win = tk.Toplevel()
self.win.title("General help for the configure script")
set_widget_geometry(self.win)
output = run(self.program, "help")
self.output = ScrolledText(self.win, state="normal", height=8, width=50)
self.output.bind("<Key>", textEvent)
self.output.insert(1.0, output)
self.pack(self.output, fill="both", expand=True, anchor="w")
def build_current_command(self, e=None):
# self.current_command["state"] = "normal"
if not self.isInCurrentCommand:
text = get_configure_command(self.program, self.source._dict_(), include_vars=True)
self.current_command.delete(1.0, "end")
self.current_command.insert(1.0, text)
# self.current_command["state"] = "disabled"
# self.current_command["text"] = text
def build_menu(self, parent):
menubar = Menu(parent)
filemenu = Menu(menubar, tearoff=0)
filemenu.add_command(label="Select command", command=self.select_command)
filemenu.add_command(label="Save options", command=self.save)
filemenu.add_separator()
filemenu.add_command(label="Exit", command=parent.destroy) #TODO: This may not work for non root parents
menubar.add_cascade(label="File", menu=filemenu, underline=0)
parent.config(menu=menubar)
def select_command(self):
initDir = os.path.abspath(os.path.dirname(self.program))
file = filedialog.askopenfilename(initialdir=initDir)
if file not in ("", ()):
self.program = os.path.abspath(file)
def call_search(self, e=None):
current = self.search_entry.get()
self._search(current, self.sections)
msg = " (filtered)"
if current != "" or self.only_checked.get():
self.notebook_label_frame["text"] = self.options_title + msg
else:
self.notebook_label_frame["text"] = self.options_title
def _search(self, word, sections):
section_id = 0
self.current_section_length = 0
self.showing = { "sections" : {} }
for section in sections:
options = sections[section].components
count_hidden = 0
self.showing["sections"][section] = {}
self.showing["sections"][section]["options"] = {}
for option in options: #TODO: Allow for double grouping
if (word != '' and not App.is_match(word, option, options[option].desc)) or (self.only_checked.get() and options[option].value in ("no", "")):
options[option].get_frame().pack_forget()
count_hidden += 1
else:
options[option].get_frame().pack(fill = options[option].fill, )
self.showing["sections"][section]["options"][option] = self.my_json["sections"][section]["options"][option]
if count_hidden == len(sections[section].components):
self.notebook.hide(section_id)
del self.showing["sections"][section]
else:
if self.previous_section_length == 0:
self.notebook.select(0)
self.notebook.add(sections[section].get_frame())
self.current_section_length += 1
section_id += 1
self.previous_section_length = self.current_section_length
return self.showing
@staticmethod
def is_match(search, *args): #Pass in args to see if search is a match with any of the arguments
rvalue = False
for a in args:
if search.lower() in a.lower():
rvalue = True
return rvalue
def get_frame(self):
return self.root
def execute(self, source=None, autoRun=False, parent=None, answer=None):
self.set_status("Running script")
if source == None:
cmd = get_configure_command(self.program, self.source._dict_())
else:
cmd = get_configure_command(self.program, source._dict_())
# RunCommand(self, cmd, autoRun=autoRun)
if not answer:
answer = messagebox.askyesno(title="Confirmation", message=f"Would you like to configure trick with your chosen options?")
if answer:
output = run(cmd)
self.win = tk.Tk()
def quit():
self.win.destroy()
self.root.destroy()
self.win.title("Script's output")
set_widget_geometry(self.win)
self.output = ScrolledText(self.win, state="normal", height=8, width=50)
self.output.bind("<Key>", textEvent)
self.output.insert(1.0, output)
self.output.pack(fill="both", expand=True, anchor="w")
self.finish_button = Button(self.win, text="Finished", command=quit)
self.finish_button.pack(anchor="e")
# self.root.destroy() #TODO: Check for a successfull output.
self.save()
# self.set_status()
self.win.mainloop()
# self.save()
else:
self.set_status()
def save(self, filename=None):
if filename == None:
if self.filename == None:
raise RuntimeError(f"No file to save configuration to.")
else:
filename = self.filename
with open(filename, "w") as f:
f.write(json.dumps(self.source._dict_(), indent=4)) #TODO: What happens if there is an error on this line
try:
os.makedirs("archive")
except OSError as exception:
if exception.errno != errno.EEXIST:
raise
timestr = time.strftime("%Y%m%d-%H%M%S")
shutil.copyfile(filename, f"archive/{timestr}_{ntpath.basename(filename)}")
def open(self, filename):
with open(filename, "r") as f:
new_json = json.load(f)
self.data = Data(**new_json)
self.my_json = new_json
#Adapted from https://stackoverflow.com/questions/4770993/how-can-i-make-silent-exceptions-louder-in-tkinter
def report_callback_exception(self, exc, val, tb):
#Handles tkinter exceptions
err_msg = {
"No file to save configuration to." : "You cannot save you current options because Gsetup was run without a configuration file."
}
err = err_msg.get(str(val), f'Unknown Error:{val}')
logging.error(traceback.format_exception(exc, val, tb))
messagebox.showerror('Error Found', err)
def is_saved(self):
# return DeepDiff(self.original_dict, self.data._dict_())
return self.original_dict == self.data._dict_()
class RunCommand:
def __init__(self, parent, command, autoRun = False) -> None:
self.win = tk.Toplevel()
# sys.stderr = Stderr(self.win)
self.parent = parent
self.command = command
self.win.title("Running command")
self.title = Text(self.win, height=3)
self.title.insert(1.0, f"Click run to run the folling command:\n{command}")
self.pack(self.title, anchor="w", expand=False, fill="x")
self.run_button = Button(self.win, text="run", command=self.run)
self.pack(self.run_button, anchor="w")
self.output = ScrolledText(self.win, state="disabled", height=8, width=50)
self.pack(self.output, fill="both", expand=True, anchor="w")
self.quit_button_and_save = Button(self.win, text="Quit and Save", command=self.quit_and_save)
self.pack(self.quit_button_and_save, anchor="w")
self.quit_button = Button(self.win, text="Quit", command=self.quit)
self.pack(self.quit_button, anchor="w")
if autoRun:
self.run()
self.win.bind("<Alt-r>", lambda e: self.run())
self.win.bind("<Alt-q>", lambda e: self.quit())
self.win.bind("<Alt-s>", lambda e: self.quit())
def pack(self, tk, **kargs):
tk.pack(kargs)
def grid(self, tk, **kargs):
tk.grid(kargs)
def quit(self):
self.win.destroy()
def quit_and_save(self):
self.parent.save()
self.win.destroy()
def run(self):
stdout = run(self.command)
self.display(stdout)
def display(self, msg):
self.output.configure(state="normal")
self.output.insert("end", msg)
self.output.configure(state="disabled")
self.output.yview("end")
class SearchBox:
def __init__(self, parent:App) -> None:
self.parent = parent
self.top = Frame(self.parent.get_frame())
self.search_box = LabelFrame(self.top, text="Filter Options")
self.search_box.rowconfigure(0, weight=1)
self.search_box.columnconfigure(0, weight=1)
# self.done_button = Button(self.search_box, text="Continue", command=self.my_continue)
# CreateToolTip(self.done_button, "Continue to run and save screen.")
# self.done_button.grid(row=0,column=2, sticky="e")
self.search_entry = Entry(self.search_box)
self.search_entry.bind("<KeyRelease>", self.parent.call_search)
CreateToolTip(self.search_entry, "Search for a specific option.")
self.search_entry.grid(row=0, column=1, sticky="e")
self.search_label = Label(self.search_box, text = "Search for options:")
self.search_label.grid(row=0, column=0, sticky="e")
self.search_box.pack(side="top", anchor="e", expand=False, fill="x")
def get_frame(self):
return self.top
class CurrentBox:
def __init__(self, parent:App) -> None:
self.parent = parent
class ChooseConfigure:
def __init__(self, parent=None) -> None:
if parent is None:
self.root = Tk()
else:
self.root = parent
self.label = Label(text="Config file not found. Please click browse to find your config file or click continue to use the default.")
self.label.pack()
self.dir = ""
self.browse_button = Button(self.root, text="Browse", command=self.browse)
self.browse_button.pack()
self.continue_button = Button(self.root, text="Continue", command=self.continue_func)
self.continue_button.pack()
self.file = { #This is the default configuration
"sections" : {},
# "landing" : { "version" : 1.0}
}
def continue_func(self):
self.root.destroy()
def get_frame(self):
return self.root
def browse(self):
initDir=os.getcwd()
if not os.path.isdir(initDir):
messagebox.showerror("Error", f'Specified directory not found. Value was:{"(Empty)" if initDir=="" else initDir}')
initDir=""
file = filedialog.askopenfilename(initialdir=initDir) #TODO: Fix this logic
if not dir in ("", ()): #askdirectory can return an empty tuple(Escape pressed) or an empty string(Cancel pressed)
self.file = file
self.root.destroy()
def get_file(self):
return self.file
def execute(parent, source, program, autoRun=False, answer=None):
cmd = get_configure_command(program, source._dict_())
# RunCommand(self, cmd, autoRun=autoRun)
if not answer:
answer = messagebox.askyesno(title="Confirmation", message=f"Are you sure that you want to run the following command:\n{cmd}")
if answer:
output_txt = run(cmd)
win = tk.Tk()
def quit():
win.destroy()
if parent:
parent.destroy()
win.title("Script's output")
set_widget_geometry(win)
output = ScrolledText(win, state="normal", height=8, width=50)
output.bind("<Key>", textEvent)
output.insert(1.0, output_txt)
output.pack(fill="both", expand=True, anchor="w")
finish_button = Button(win, text="Finished", command=quit)
finish_button.pack(anchor="e")
# self.save()
win.mainloop()
class LandingPage(Component):
def __init__(self, parent=None, config_file="./config.json", initial_dir=os.getcwd(), resource_folder = f'{os.path.dirname(os.path.realpath(__file__))}/resources') -> None:
if parent:
self.root = parent
else:
self.root = Tk()
self.root.maxsize(width=531, height=292) #These numbers were found through trial and error
self.root.minsize(width=531, height=292) #These numbers were found through trial and error
set_widget_geometry(self.root, 531, 292)
if type(config_file) is str:
with open(config_file, "r") as f:
app_json = json.load(f)
elif type(config_file) is dict:
app_json = config_file
else:
raise RuntimeError(f"Config_file is {type(config_file)}. It must be either a string or a dict.")
self.data = Data(**(app_json.get("landing", {})))
super().__init__(parent, app_json.get("name", "landing"), self.data, special_valid_params=["version", "desc"], special_required_params=[]) #Note: there should be no required params for Landing because landing itself is not required
self.resource_folder = resource_folder
#Set default values
self.version = "x.x" if self.version == "default" else self.version
self.desc = "This setup guide will allow you to easily see all the options that are available to configure Trick with." if self.desc == "default" else self.desc
self.root.title(self.name)
self.open_advanced = False
self.to_close = True
self.header = Frame(self.root)
self.body = Frame(self.root)
self.footer = Frame(self.root)
self.header.pack()
self.body.pack(expand=True, fill="both")
self.footer.pack()
self.release_label = Label(self.header, text=f"Release {self.version}")
self.release_label.pack(anchor="w")
self.title_frame = Frame(self.header)
self.desc_label = Label(self.title_frame, text="Welcome to Trick.", font='Helvetica 15 bold')
self.desc_label.pack(side="left")
self.img = PhotoImage(file=f'{self.resource_folder}/trick_icon.gif')
Label(self.title_frame, image=self.img).pack(side="left")
self.title_frame.pack()
self.desc_label2 = Label(self.header, wraplength=500, text=self.desc)
self.desc_label2.pack(pady=10)
self.label = Label(self.body, text="Location:")
self.label.pack(anchor="w", padx=50)
self.folder_location = StringVar(value=initial_dir)
self.folder_entry = Entry(self.body, textvariable=self.folder_location)
self.folder_entry.pack(side="left", expand=True, fill="x", padx=50)
self.change_button = Button(self.body, text="Change", command=self.change_dir)
CreateToolTip(self.change_button, "Click here to choose Trick's home directory. Configure will run from within this directory.")
self.change_button.pack(side="left", pady=10, padx=10)
self.configure_fast_button = Button(self.footer, text="Configure with defaults", command=self.configure)
CreateToolTip(self.configure_fast_button, "Run configure with the default options.")
self.configure_fast_button.pack(side="left", padx=10, pady=10)
self.configure_button = Button(self.footer, text="Configure with advanced options", command=self.configure_with_options)
CreateToolTip(self.configure_button, "Choose advanced options to configure trick with.")
self.configure_button.pack(side="left", padx=10, pady=10)
self.close_button = Button(self.footer, text="Close", command=self.close)
self.close_button.pack(side="left", padx=10, pady=10)
def change_dir(self):
dir = filedialog.askdirectory(initialdir=self.folder_location.get())
if not dir in ("", ()):
self.folder_location.set(dir)
else:
logging.error("Invalid directory.")
def set_program(self):
currdir = os.path.abspath(os.getcwd())
try:
os.chdir(self.folder_location.get())
except:
messagebox.showerror(title="Invalid directory", message=f"{self.folder_location.get()} is not a valid directory")
return False
arr = glob.glob("configure")
if len(arr) > 0:
self.program = os.path.abspath(arr[0])
return True
else:
os.chdir(curdir)
messagebox.showerror(title="Wrong home directory", message=f"No configure file found in location: {self.folder_location.get()}. Please enter your trick home directory.")
return False
def configure(self):
if self.set_program():
self.open_advanced = False
self.to_close = False
self.close()
def close(self):
self.root.destroy()
def configure_with_options(self):
if self.set_program():
self.open_advanced = True
self.to_close = False
self.close()
def get_frame(self):
return self.root
def main():
logging.getLogger().setLevel(logging.DEBUG)
parser = argparse.ArgumentParser()
enable_load = False
default = "(default: %(default)s)"
parser.add_argument("-s", "--script-file", default="./configure", help=f"script to add args to {default}")
parser.add_argument("-c", "--config", default=f"{os.path.dirname(os.path.realpath(__file__))}/sample_config.json", help=f"json file with gui options and settings {default}")
parser.add_argument("-b", "--build", action="store_true", default=False, help=f"guess the parameter choices from the scripts help output {default}")
args = parser.parse_args()
resource_folder = f'{os.path.dirname(os.path.realpath(__file__))}/resources'
if args.build:
if enable_load:
from load import load, write_help
write_help(args.script_file)
load()
else:
logging.warning(f"Build functionality is not enabled. Not loading {args.script_file}.")
config_file = args.config
if not os.path.isfile(config_file):
c = ChooseConfigure()
c.get_frame().mainloop()
config_file = c.get_file()
if type(config_file) is str:
config_file = os.path.abspath(config_file) #Landing page will change cwd so we get abs path
if os.path.exists(args.script_file):
script_folder = os.path.dirname(os.path.abspath(args.script_file))
else:
script_folder = os.getcwd()
l = LandingPage(parent=None, config_file=config_file, initial_dir=script_folder, resource_folder=resource_folder)
l.get_frame().mainloop()
if not l.to_close:
if l.open_advanced:
a = App(config_file, l.program, resource_folder=resource_folder)
a.get_frame().mainloop()
else:
execute(None, Data(sections=Data()), l.program, autoRun=True, answer=True)
if __name__ == "__main__":
main()