#!/usr/bin/python3 import tkinter as tk from tkinter import ttk import sys, os, json, traceback ################################################################################ # DEFINITIONS ################################################################################ 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)) ################################################################################ # 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) ################################################################################ # PROGRAM MAIN WINDOW ################################################################################ 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, } } 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(): menubar = tk.Menu(main_window) build_menu(menu_structure, menubar) main_window.config(menu=menubar) rebuild_menu() def handle_exit(): set_configuration_value("window geometry", main_window.geometry()) main_window.destroy() main_window.protocol("WM_DELETE_WINDOW", handle_exit) main_window.mainloop()