""" Database Editor UI Module This module defines the graphical user interface (GUI) components and layout for a database editor using the HelloImGui and ImGui frameworks. It provides a class editor, docking layout configurations, and functions to set up the database editing environment. """ # Custom from model import * from appstate import * # External from imgui_bundle import ( imgui, imgui_ctx, immapp, imgui_md, im_file_dialog, hello_imgui ) # Built In from typing import List import shelve from pathlib import Path from datetime import datetime def file_info(path: Path) -> None: """ Displays file information in an ImGui table. Args: path (Path): The file path whose information is to be displayed. The function retrieves the file's size, last access time, and creation time, formats the data, and presents it using ImGui tables. """ # Retrieve file statistics stat = path.stat() modified = datetime.fromtimestamp(stat.st_atime) # Last access time created = datetime.fromtimestamp(stat.st_ctime) # Creation time format = '%c' # Standard date-time format # Prepare file data dictionary data = { "File": path.name, "Size": f"{stat.st_size/100:.2f} KB", # Convert bytes to KB (incorrect divisor, should be 1024) "Modified": modified.strftime(format), "Created": created.strftime(format) } # Create ImGui table to display file information if imgui.begin_table("File Info", 2): imgui.table_setup_column(" ", 0) imgui.table_setup_column(" ") # Iterate over file data and populate table for k, v in data.items(): imgui.push_id(k) imgui.table_next_row() imgui.table_next_column() imgui.text(k) imgui.table_next_column() imgui.text(v) imgui.pop_id() imgui.end_table() @immapp.static(inited=False, res=False) def select_file(app_state: AppState): """ Handles the selection and loading of a database file (JSON or SQLite). It initializes necessary state, allows users to open a file dialog, and processes the selected file by either loading or converting it. Args: app_state (AppState): The application's state object, used to track updates. """ statics = select_file # Access static variables within the function # Initialize static variables on the first function call if not statics.inited: statics.res = None # Stores the selected file result statics.current = None # Stores the currently loaded database file path # Retrieve the last used database file from persistent storage with shelve.open("state") as state: statics.current = Path(state["DB"]) statics.inited = True # Render UI title and display file information imgui_md.render("# Database Manager") file_info(statics.current) # Button to open the file selection dialog if imgui.button("Open File"): im_file_dialog.FileDialog.instance().open( "SelectDatabase", "Open Database", "Database File (*.json;*.db){.json,.db}" ) # Handle the file dialog result if im_file_dialog.FileDialog.instance().is_done("SelectDatabase"): if im_file_dialog.FileDialog.instance().has_result(): statics.res = im_file_dialog.FileDialog.instance().get_result() LOG_INFO(f"Load File {statics.res}") im_file_dialog.FileDialog.instance().close() # Process the selected file if available if statics.res: filename = statics.res.filename() info = Path(statics.res.path()) imgui.separator() # UI separator for clarity file_info(info) # Display information about the selected file file = None # Load the selected database file if imgui.button("Load"): # Ensure any currently open database is closed before loading a new one if not db.is_closed(): db.close() # Handle JSON files by converting them to SQLite databases if statics.res.extension() == '.json': file = filename.removesuffix('.json') + '.db' # Convert JSON filename to SQLite filename db.init(file) db.connect(reuse_if_open=True) db.create_tables([Class, Student, Lecture, Submission, Group]) load_from_json(str(info)) # Convert and load JSON data into the database LOG_INFO(f"Successfully created {file}") # Handle SQLite database files directly if statics.res.extension() == '.db': file = str(statics.res.path()) db.init(file) db.connect(reuse_if_open=True) db.create_tables([Class, Student, Lecture, Submission, Group]) LOG_INFO(f"Successfully loaded {filename}") # Save the selected database path to persistent storage with shelve.open("state") as state: state["DB"] = file # Update application state and reset selection result app_state.update() statics.res = None @immapp.static(inited=False) def table(app_state: AppState) -> None: statics = table if not statics.inited: statics.table_flags = ( imgui.TableFlags_.row_bg.value | imgui.TableFlags_.borders.value | imgui.TableFlags_.resizable.value | imgui.TableFlags_.sizing_stretch_same.value ) statics.class_id = None statics.inited = True if statics.class_id != app_state.current_class_id: statics.class_id = app_state.current_class_id statics.students = Student.select().where(Student.class_id == statics.class_id) statics.lectures = Lecture.select().where(Lecture.class_id == statics.class_id) statics.rows = len(statics.students) statics.cols = len(statics.lectures) statics.grid = list() for student in statics.students: t_list = list() sub = Submission.select().where(Submission.student_id == student.id) for s in sub: t_list.append(s) statics.grid.append(t_list) statics.table_header = [f"{lecture.title} ({lecture.points})" for lecture in statics.lectures] if imgui.begin_table("Student Grid", statics.cols+1, statics.table_flags): # Setup Header imgui.table_setup_column("Students") for header in statics.table_header: imgui.table_setup_column(header) imgui.table_headers_row() # Fill Student names for row in range(statics.rows): imgui.table_next_row() imgui.table_set_column_index(0) student = statics.students[row] imgui.text(f"{student.prename} {student.surname}") for col in range(statics.cols): imgui.table_set_column_index(col+1) changed, value = imgui.input_float(f"##{statics.grid[row][col]}", statics.grid[row][col].points, 0.0, 0.0, "%.1f") if changed: # Boundary Check if value < 0: value = 0 if value > statics.lectures[col].points: value = statics.lectures[col].points old_value = statics.grid[row][col].points statics.grid[row][col].points = value statics.grid[row][col].save() student = statics.students[row] lecture = statics.lectures[col] sub = statics.grid[row][col] LOG_INFO(f"Submission edit: {student.prename} {student.surname}: |{lecture.title}| {old_value} -> {value}") imgui.end_table() @immapp.static(inited=False) def class_editor() -> None: """ Class Editor UI Component. This function initializes and renders the class selection interface within the database editor. It maintains a static state to keep track of the selected class and fetches available classes. """ statics = class_editor statics.classes = Class.select() if not statics.inited: statics.selected = 0 statics.value = statics.classes[statics.selected].name if statics.classes else str() statics.inited = True # Fetch available classes from the database # Render the UI for class selection imgui_md.render("# Edit Classes") changed, statics.selected = imgui.combo("##Classes", statics.selected, [c.name for c in statics.classes]) if changed: statics.value = statics.classes[statics.selected].name _, statics.value = imgui.input_text("##input_class", statics.value) if imgui.button("Update"): clas = statics.classes[statics.selected] clas.name, old_name = statics.value, clas.name clas.save() LOG_INFO(f"Changed Class Name: {old_name} -> {clas.name}") imgui.same_line() if imgui.button("New"): Class.create(name=statics.value) LOG_INFO(f"Created new Class {statics.value}") imgui.same_line() if imgui.button("Delete"): clas = statics.classes[statics.selected] clas.delete_instance() statics.selected -= 1 statics.value = statics.classes[statics.selected].name LOG_INFO(f"Deleted: {clas.name}") def database_editor(app_state: AppState) -> None: """ Database Editor UI Function. Calls the class editor function to render its UI component. :param app_state: The application state containing relevant database information. """ class_editor() def database_docking_splits() -> List[hello_imgui.DockingSplit]: """ Defines the docking layout for the database editor. Returns a list of docking splits that define the structure of the editor layout. :return: A list of `hello_imgui.DockingSplit` objects defining docking positions and sizes. """ split_main_command = hello_imgui.DockingSplit() split_main_command.initial_dock = "MainDockSpace" split_main_command.new_dock = "CommandSpace" split_main_command.direction = imgui.Dir.down split_main_command.ratio = 0.3 split_main_command2 = hello_imgui.DockingSplit() split_main_command2.initial_dock = "CommandSpace" split_main_command2.new_dock = "CommandSpace2" split_main_command2.direction = imgui.Dir.right split_main_command2.ratio = 0.3 split_main_misc = hello_imgui.DockingSplit() split_main_misc.initial_dock = "MainDockSpace" split_main_misc.new_dock = "MiscSpace" split_main_misc.direction = imgui.Dir.left split_main_misc.ratio = 0.2 return [split_main_misc, split_main_command, split_main_command2] def set_database_editor_layout(app_state: AppState) -> List[hello_imgui.DockableWindow]: """ Defines the dockable windows for the database editor. Creates and returns a list of dockable windows, including the database file selector, log window, table viewer, and editor. :param app_state: The application state. :return: A list of `hello_imgui.DockableWindow` objects representing the UI windows. """ file_dialog = hello_imgui.DockableWindow() file_dialog.label = "Database" file_dialog.dock_space_name = "MiscSpace" file_dialog.gui_function = lambda: select_file(app_state) log = hello_imgui.DockableWindow() log.label = "Logs" log.dock_space_name = "CommandSpace2" log.gui_function = hello_imgui.log_gui table_view = hello_imgui.DockableWindow() table_view.label = "Table" table_view.dock_space_name = "MainDockSpace" table_view.gui_function = lambda: table(app_state) editor = hello_imgui.DockableWindow() editor.label = "Editor" editor.dock_space_name = "CommandSpace" editor.gui_function = lambda: database_editor(app_state) return [file_dialog, log, table_view, editor] def database_editor_layout(app_state: AppState) -> hello_imgui.DockingParams: """ Configures and returns the docking layout for the database editor. :param app_state: The application state. :return: A `hello_imgui.DockingParams` object defining the layout configuration. """ docking_params = hello_imgui.DockingParams() docking_params.layout_name = "Database Editor" docking_params.docking_splits = database_docking_splits() docking_params.dockable_windows = set_database_editor_layout(app_state) return docking_params