Compare commits

...

3 Commits

Author SHA1 Message Date
BodgeMaster b5124686bf make config object oriented 2022-02-15 20:39:07 +01:00
BodgeMaster b6cfc46fe4 separate parts out into individual files 2022-02-15 20:03:45 +01:00
BodgeMaster b55468f67d add spyder project and __pycache__ 2022-02-15 20:03:18 +01:00
5 changed files with 245 additions and 227 deletions

2
.gitignore vendored
View File

@ -1 +1,3 @@
*.swp
__pycache__
.spyproject

73
config.py Normal file
View File

@ -0,0 +1,73 @@
import os, sys, json
import tkinter as tk
from tkinter import ttk
import gui_helper, util
class Config:
def __init__(self, file_path, default_config):
self.__file_path = file_path
self.__default_config = default_config
self.__current_config = {}
if os.path.isfile(file_path):
try:
config_file = open(self.__file_path, "r")
self.__current_config = json.loads(config_file.read())
config_file.close()
except:
util.error("An exception occurred while trying to load the configuration.", handle_gracefully=False)
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"):
self.__current_config = default_config
try:
config_file = open(self.__file_path, "w")
config_file.write(json.dumps(self.__current_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 run, 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:
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(self, key):
if not key in self.__current_config:
util.info("Requested configuration value for "+str(key)+" not in configuration. Loading from default configuration.")
try:
self.__current_config[key] = self.__default_config[key]
except KeyError:
util.error("Requested an invalid configuration key.")
return None
return self.__current_config[key]
def set_configuration_value(self, key, value, save_to_disk=True):
if not key in self.__current_config:
util.info("Writing configuration for previously unknown key "+str(key)+".")
self.__current_config[key]=value
if save_to_disk:
try:
config_file = open(self.__file_path, "w")
config_file.write(json.dumps(self.__current_config))
config_file.close()
except:
util.error("Failed to save config file.")

132
gui_helper.py Normal file
View 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])

238
main.py
View File

@ -1,164 +1,24 @@
#!/usr/bin/python3
import os
import tkinter as tk
from tkinter import ttk
import sys, os, json, traceback
import gui_helper
from config import Config
################################################################################
# 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 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:
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(configuration_file_path, default_configuration)
################################################################################
# PROGRAM MAIN WINDOW
@ -166,94 +26,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(configuration.get_configuration_value("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())
configuration.set_configuration_value("window geometry", main_window.geometry())
main_window.destroy()
main_window.protocol("WM_DELETE_WINDOW", handle_exit)

27
util.py Normal file
View File

@ -0,0 +1,27 @@
import sys, traceback
EXIT_SUCCESS=0
EXIT_ERROR=1
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)