This repository has been archived on 2022-12-22. You can view files and clone it, but cannot push or open issues/pull-requests.
Concorde-IDE/main.py

262 lines
9.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#!/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 isnt 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:
warn("Failed to save config file.", is_exception=True)
dialog = tk.Tk()
dialog.title("Failed to save config file")
ttk.Label(dialog, text="Failed to save the configuration file.").pack()
ttk.Button(dialog, text="Continue", command=dialog.destroy).pack()
dialog.resizable(0,0)
dialog.mainloop()
# 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()
main_window.mainloop()