261 lines
		
	
	
		
			9.3 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			261 lines
		
	
	
		
			9.3 KiB
		
	
	
	
		
			Python
		
	
	
| #!/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()
 |