From b6cfc46fe4f367ddf8dafa60bdcb9eef94ca290e Mon Sep 17 00:00:00 2001 From: BodgeMaster <> Date: Tue, 15 Feb 2022 20:03:45 +0100 Subject: [PATCH] separate parts out into individual files --- config.py | 71 +++++++++++++++ gui_helper.py | 132 ++++++++++++++++++++++++++++ main.py | 237 +++----------------------------------------------- util.py | 27 ++++++ 4 files changed, 240 insertions(+), 227 deletions(-) create mode 100644 config.py create mode 100644 gui_helper.py create mode 100644 util.py diff --git a/config.py b/config.py new file mode 100644 index 0000000..de69c86 --- /dev/null +++ b/config.py @@ -0,0 +1,71 @@ +import os, sys, json +import tkinter as tk +from tkinter import ttk +import gui_helper, util + +def load_configuration(default_config, file_path): + if os.path.isfile(file_path): + try: + config_file = open(file_path, "r") + global configuration + configuration = json.loads(config_file.read()) + config_file.close() + return configuration + except Exception: + util.error("An exception occurred while trying to load the configuration.", handle_gracefully=False) + return {} + else: + # config not found + dialog_interaction_handler = gui_helper.Window_Interaction_Handler() + + dialog = tk.Tk() + dialog.title("No configuration found") + ttk.Label(dialog, text="No configuration found!").pack() + buttons_frame = tk.Frame(dialog) + buttons_frame.pack() + ttk.Button(buttons_frame, text="Create", command=lambda: dialog_interaction_handler.interact("create", True, additional_action=dialog.destroy)).grid(column=0, row=0) + ttk.Button(buttons_frame, text="Quit", command=lambda: dialog_interaction_handler.interact("create", False, additional_action=dialog.destroy)).grid(column=1, row=0) + dialog.resizable(0,0) + dialog.mainloop() + + if dialog_interaction_handler.get_result("create"): + try: + config_file = open(file_path, "w") + config_file.write(json.dumps(default_config)) + config_file.close() + except: + util.warn("Failed to save initial config file.", is_exception=True) + dialog = tk.Tk() + dialog.title("Failed to save initial config file") + ttk.Label(dialog, text="Failed to save the initial config file.\n" + + "The IDE can still start up, but it is likely that all changes to the configuration will be lost where they would be saved otherwise.").pack() + ttk.Button(dialog, text="Continue", command=dialog.destroy).pack() + dialog.resizable(0,0) + dialog.mainloop() + return default_config + else: + util.error("No config present and user chose not to create one. Exiting.", is_exception=False, handle_gracefully=True) + # exit with success exit code anyway because this is not a program failure + sys.exit(util.EXIT_SUCCESS) + +def get_configuration_value(config, default_config, key): + if not key in config: + util.info("Requested configuration value for "+str(key)+" not in configuration. Loading from default configuration.") + try: + config[key] = default_config[key] + except KeyError: + util.error("Requested an invalid configuration key.") + return None + return config[key] + +def set_configuration_value(file_path, config, key, value, save_to_disk=True): + if not key in config: + util.info("Writing configuration for previously unknown key "+str(key)+".") + config[key]=value + if save_to_disk: + try: + config_file = open(file_path, "w") + config_file.write(json.dumps(config)) + config_file.close() + except: + util.error("Failed to save config file.") diff --git a/gui_helper.py b/gui_helper.py new file mode 100644 index 0000000..c0e9095 --- /dev/null +++ b/gui_helper.py @@ -0,0 +1,132 @@ +import tkinter as tk +import util + +# easy way to get data out of window events +class Window_Interaction_Handler: + # constructor + def __init__(self): + self.__window_interactions = {} + + # add a result for an interaction event, saves results in reverse order by default, can optionally call another function + def interact(self, name, result, reverse_order=True, additional_action=None, additional_action_parameters=()): + if name in self.__window_interactions: + if reverse_order: + self.__window_interactions[name] = [result] + self.__window_interactions[name] + else: + self.__window_interactions[name] = self.__window_interactions[name] + [result] + else: + self.__window_interactions[name] = [result] + + if not additional_action==None: + additional_action(*additional_action_parameters) + + # get first result for a given event from the list of results (newest (default) or oldest), removes the returned result from the list by default + def get_result(self, name, remove=True): + if name in self.__window_interactions and len(self.__window_interactions[name])>0: + if remove: + result = self.__window_interactions[name].pop(0) + if len(self.__window_interactions[name])==0: + del self.__window_interactions[name] + return result + else: + return self.__window_interactions[name][0] + else: + return None + + # get all results for a given event + def get_results(self, name, clear=False): + if name in self.__window_interactions and len(self.__window_interactions[name])>0: + results = self.__window_interactions[name] + if clear: + del self.__window_interactions[name] + return results + + # clear results for a given event + def clear(self, name): + if name in self.__window_interactions: + del self.__window_interactions[name] + + # destructor + def __del__(self): + if len(self.__window_interactions)>0: + util.warn("__window_interactions not empty upon destruction of Window_Interaction_Handler:\n"+str(self.__window_interactions)) + + +def not_implemented(): + util.warn("Not implemented!") + +# format: +# "":{} -> menu or submenu +# "":function -> menu entry +# "":None -> disabled menu entry +# None:None -> separator +# +# Entries with ... at the end are supposed to open dialogs whereas entries without dots are supposed to take effect immediately +menu_structure = { + "IDE": { + "Preferences...": not_implemented, + None: None, + "Quit": not_implemented + }, + "Project": { + "New": { + "No known project types": None + }, + "Open...": not_implemented, + "Close": { + "No open projects": None + }, + None: None, + "Preferences...": not_implemented, + "Search...": not_implemented, + "Build": not_implemented + }, + "File": { + "New...": not_implemented, + "Open...": not_implemented, + "Save": not_implemented, + "Close": not_implemented, + None: None, + "Rename...": not_implemented, + "Move...": not_implemented, + "View in File Explorer...": not_implemented + }, + "Edit": { + "Cut": not_implemented, + "Copy": not_implemented, + "Paste": not_implemented, + "Move code...": not_implemented, + None: None, + "Search and Replace...": not_implemented, + None: None, + "Format": not_implemented, + "Indent": not_implemented, + "Unindent": not_implemented, + "Toggle Comment": not_implemented + }, + "View": { + "Zoom in": not_implemented, + "Zoom out": not_implemented, + "Normal Size": not_implemented + }, + "Help": { + "Manual...": not_implemented, + "About IDE...": not_implemented, + } +} + +def build_menu(structure_dict, menu): + for entry in structure_dict: + if structure_dict[entry]==None: + if entry==None: + menu.add_separator() + else: + menu.add_command(label=entry) + menu.entryconfig(entry, state="disabled") + if isinstance(structure_dict[entry], dict): + submenu = tk.Menu(menu, tearoff=False) + build_menu(structure_dict[entry], submenu) + menu.add_cascade(label=entry, menu=submenu) + if callable(structure_dict[entry]): + menu.add_command(label=entry, command=structure_dict[entry]) + diff --git a/main.py b/main.py index 953b939..2e3f497 100644 --- a/main.py +++ b/main.py @@ -1,164 +1,23 @@ #!/usr/bin/python3 +import os import tkinter as tk from tkinter import ttk -import sys, os, json, traceback +import config, gui_helper ################################################################################ -# DEFINITIONS +# CONSTANTS ################################################################################ -EXIT_SUCCESS=0 -EXIT_ERROR=1 - default_configuration = { "window geometry": "640x480" } -# empty configuration by default because new values will be added while trying to load them -# this allows the IDE to load old configuration files with missing keys -configuration = {} -home_directory = os.path.expanduser("~") -config_file_path = os.path.join(home_directory, "some_ide_config.json") - -def info(message): - # print info to sys.stderr because it isn’t really output, just debug information - print("INFO: "+str(message), file=sys.stderr) - traceback.print_stack() - -def warn(message, is_exception=False): - print("WARNING: "+str(message), file=sys.stderr) - if is_exception: - traceback.print_exc() - else: - traceback.print_stack() - -def error(message, is_exception=True, handle_gracefully=True): - print("ERROR: "+str(message), file=sys.stderr) - if is_exception: - traceback.print_exc() - else: - traceback.print_stack() - if handle_gracefully: - pass - else: - sys.exit(EXIT_ERROR) - -def get_configuration_value(key): - if key in configuration: - pass - else: - info("Requested configuration value for "+str(key)+" not in configuration. Loading from default configuration.") - try: - configuration[key] = default_configuration[key] - except KeyError: - error("Requested an invalid configuration key.") - return None - return configuration[key] - -def set_configuration_value(key, value, save_to_disk=True): - if not key in configuration: - info("Writing configuration for previously unknown key "+str(key)+".") - configuration[key]=value - try: - config_file = open(config_file_path, "w") - config_file.write(json.dumps(configuration)) - config_file.close() - except: - error("Failed to save config file.") - -# easy way to get data out of window events -class Window_Interaction_Handler: - # constructor - def __init__(self): - self.__window_interactions = {} - - # add a result for an interaction event, saves results in reverse order by default, can optionally call another function - def interact(self, name, result, reverse_order=True, additional_action=None, additional_action_parameters=()): - if name in self.__window_interactions: - if reverse_order: - self.__window_interactions[name] = [result] + self.__window_interactions[name] - else: - self.__window_interactions[name] = self.__window_interactions[name] + [result] - else: - self.__window_interactions[name] = [result] - - if not additional_action==None: - additional_action(*additional_action_parameters) - - # get first result for a given event from the list of results (newest (default) or oldest), removes the returned result from the list by default - def get_result(self, name, remove=True): - if name in self.__window_interactions and len(self.__window_interactions[name])>0: - if remove: - result = self.__window_interactions[name].pop(0) - if len(self.__window_interactions[name])==0: - del self.__window_interactions[name] - return result - else: - return self.__window_interactions[name][0] - else: - return None - - # get all results for a given event - def get_results(self, name, clear=False): - if name in self.__window_interactions and len(self.__window_interactions[name])>0: - results = self.__window_interactions[name] - if clear: - del self.__window_interactions[name] - return results - - # clear results for a given event - def clear(self, name): - if name in self.__window_interactions: - del self.__window_interactions[name] - - # destructor - def __del__(self): - if len(self.__window_interactions)>0: - warn("__window_interactions not empty upon destruction of Window_Interaction_Handler:\n"+str(self.__window_interactions)) +configuration_file_path = os.path.join(os.path.expanduser("~"), "some_ide_config.json") ################################################################################ # PROGRAM STARTUP ################################################################################ -# read configuration -if os.path.isfile(config_file_path): - try: - config_file = open(config_file_path, "r") - configuration = json.loads(config_file.read()) - config_file.close() - except Exception: - error("An exception occurred while trying to load the configuration.", handle_gracefully=False) -else: - # config not found - dialog_interaction_handler = Window_Interaction_Handler() - - dialog = tk.Tk() - dialog.title("No configuration found") - ttk.Label(dialog, text="No configuration found!").pack() - buttons_frame = tk.Frame(dialog) - buttons_frame.pack() - ttk.Button(buttons_frame, text="Create", command=lambda: dialog_interaction_handler.interact("create", True, additional_action=dialog.destroy)).grid(column=0, row=0) - ttk.Button(buttons_frame, text="Quit", command=lambda: dialog_interaction_handler.interact("create", False, additional_action=dialog.destroy)).grid(column=1, row=0) - dialog.resizable(0,0) - dialog.mainloop() - - if dialog_interaction_handler.get_result("create"): - try: - config_file = open(config_file_path, "w") - config_file.write(json.dumps(default_configuration)) - config_file.close() - except: - warn("Failed to save initial config file.", is_exception=True) - dialog = tk.Tk() - dialog.title("Failed to save initial config file") - ttk.Label(dialog, text="Failed to save the initial config file.\n" + - "The IDE can still start up, but it is likely that all changes to the configuration will be lost where they would be saved otherwise.").pack() - ttk.Button(dialog, text="Continue", command=dialog.destroy).pack() - dialog.resizable(0,0) - dialog.mainloop() - else: - error("No config present and user chose not to create one. Exiting.", is_exception=False, handle_gracefully=True) - # exit with success exit code anyway because this is not a program failure - sys.exit(EXIT_SUCCESS) +configuration = config.load_configuration(default_configuration, configuration_file_path) ################################################################################ # PROGRAM MAIN WINDOW @@ -166,94 +25,18 @@ else: main_window = tk.Tk() main_window.title("IDE") -main_window.geometry(get_configuration_value("window geometry")) - -def not_implemented(): - warn("Not implemented!") - -# format: -# "":{} -> menu or submenu -# "":function -> menu entry -# "":None -> disabled menu entry -# None:None -> separator -# -# Entries with ... at the end are supposed to open dialogs whereas entries without dots are supposed to take effect immediately -menu_structure = { - "IDE": { - "Preferences...": not_implemented, - "Quit": not_implemented - }, - "Project": { - "New": { - "No known project types": None - }, - "Open...": not_implemented, - "Close": { - "No open projects": None - }, - None: None, - "Preferences...": not_implemented, - "Search...": not_implemented, - "Build": not_implemented - }, - "File": { - "New...": not_implemented, - "Open...": not_implemented, - "Save": not_implemented, - "Close": not_implemented, - None: None, - "Rename...": not_implemented, - "Move...": not_implemented, - "View in File Explorer...": not_implemented - }, - "Edit": { - "Cut": not_implemented, - "Copy": not_implemented, - "Paste": not_implemented, - None: None, - "Search and Replace...": not_implemented, - None: None, - "Format": not_implemented, - "Indent": not_implemented, - "Unindent": not_implemented, - "Toggle Comment": not_implemented - }, - "View": { - "Zoom in": not_implemented, - "Zoom out": not_implemented, - "Normal Size": not_implemented - }, - "Help": { - "Manual...": not_implemented, - "About IDE...": not_implemented, - } -} +main_window.geometry(config.get_configuration_value(configuration, default_configuration, "window geometry")) menubar = None -def build_menu(structure_dict, menu): - for entry in structure_dict: - if structure_dict[entry]==None: - if entry==None: - menu.add_separator() - else: - menu.add_command(label=entry) - menu.entryconfig(entry, state="disabled") - if isinstance(structure_dict[entry], dict): - submenu = tk.Menu(menu, tearoff=False) - build_menu(structure_dict[entry], submenu) - menu.add_cascade(label=entry, menu=submenu) - if callable(structure_dict[entry]): - menu.add_command(label=entry, command=structure_dict[entry]) - -def rebuild_menu(): +def rebuild_menu(structure_dict): menubar = tk.Menu(main_window) - build_menu(menu_structure, menubar) + gui_helper.build_menu(structure_dict, menubar) main_window.config(menu=menubar) -rebuild_menu() +rebuild_menu(gui_helper.menu_structure) def handle_exit(): - set_configuration_value("window geometry", main_window.geometry()) + config.set_configuration_value(configuration_file_path, configuration, "window geometry", main_window.geometry()) main_window.destroy() main_window.protocol("WM_DELETE_WINDOW", handle_exit) diff --git a/util.py b/util.py new file mode 100644 index 0000000..0961c76 --- /dev/null +++ b/util.py @@ -0,0 +1,27 @@ +import sys, traceback + +EXIT_SUCCESS=0 +EXIT_ERROR=1 + +def info(message): + # print info to sys.stderr because it isn’t really output, just debug information + print("INFO: "+str(message), file=sys.stderr) + traceback.print_stack() + +def warn(message, is_exception=False): + print("WARNING: "+str(message), file=sys.stderr) + if is_exception: + traceback.print_exc() + else: + traceback.print_stack() + +def error(message, is_exception=True, handle_gracefully=True): + print("ERROR: "+str(message), file=sys.stderr) + if is_exception: + traceback.print_exc() + else: + traceback.print_stack() + if handle_gracefully: + pass + else: + sys.exit(EXIT_ERROR)