separate parts out into individual files
parent
b55468f67d
commit
b6cfc46fe4
|
@ -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.")
|
|
@ -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])
|
||||||
|
|
237
main.py
237
main.py
|
@ -1,164 +1,23 @@
|
||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
|
import os
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
import sys, os, json, traceback
|
import config, gui_helper
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# DEFINITIONS
|
# CONSTANTS
|
||||||
################################################################################
|
################################################################################
|
||||||
|
|
||||||
EXIT_SUCCESS=0
|
|
||||||
EXIT_ERROR=1
|
|
||||||
|
|
||||||
default_configuration = {
|
default_configuration = {
|
||||||
"window geometry": "640x480"
|
"window geometry": "640x480"
|
||||||
}
|
}
|
||||||
# empty configuration by default because new values will be added while trying to load them
|
configuration_file_path = os.path.join(os.path.expanduser("~"), "some_ide_config.json")
|
||||||
# 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
|
# PROGRAM STARTUP
|
||||||
################################################################################
|
################################################################################
|
||||||
|
|
||||||
# read configuration
|
configuration = config.load_configuration(default_configuration, configuration_file_path)
|
||||||
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
|
# PROGRAM MAIN WINDOW
|
||||||
|
@ -166,94 +25,18 @@ else:
|
||||||
|
|
||||||
main_window = tk.Tk()
|
main_window = tk.Tk()
|
||||||
main_window.title("IDE")
|
main_window.title("IDE")
|
||||||
main_window.geometry(get_configuration_value("window geometry"))
|
main_window.geometry(config.get_configuration_value(configuration, default_configuration, "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
|
menubar = None
|
||||||
def build_menu(structure_dict, menu):
|
def rebuild_menu(structure_dict):
|
||||||
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)
|
menubar = tk.Menu(main_window)
|
||||||
build_menu(menu_structure, menubar)
|
gui_helper.build_menu(structure_dict, menubar)
|
||||||
main_window.config(menu=menubar)
|
main_window.config(menu=menubar)
|
||||||
|
|
||||||
rebuild_menu()
|
rebuild_menu(gui_helper.menu_structure)
|
||||||
|
|
||||||
def handle_exit():
|
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.destroy()
|
||||||
|
|
||||||
main_window.protocol("WM_DELETE_WINDOW", handle_exit)
|
main_window.protocol("WM_DELETE_WINDOW", handle_exit)
|
||||||
|
|
|
@ -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)
|
Reference in New Issue