diff --git a/package/ui/controllers/main.py b/package/ui/controllers/main.py index 0c52d59e..d58fcc7a 100644 --- a/package/ui/controllers/main.py +++ b/package/ui/controllers/main.py @@ -2,6 +2,7 @@ from .file_selection import FileSelectionController from .text_output import TextOutputController +from .model_status import ModelStatusController class Controller: @@ -12,6 +13,7 @@ def __init__(self, model, view): # Initialize controllers self.file_selection_controller = FileSelectionController(self.model, self.view.frames["file_selection"]) self.text_output_controller = TextOutputController(self.model, self.view.frames["text_output"]) + self.model_status_controller = ModelStatusController(self.model, self.view.frames["text_output"].model_status) # Bind the run button to the model self.view.frames["run_abort"].run_button.config(command=self.run_model) diff --git a/package/ui/controllers/model_status.py b/package/ui/controllers/model_status.py new file mode 100644 index 00000000..e83dff41 --- /dev/null +++ b/package/ui/controllers/model_status.py @@ -0,0 +1,41 @@ +import threading +import time +from ..models.main import ModelStatus + + +class ModelStatusController: + """ Tracks if the model is running and updates the view to reflect the status. """ + def __init__(self, model, view): + """ + Constructor + @In, model, Model, the model + @In, view, View, the view + """ + self.model = model + self.view = view + self._model_has_run = False # Flag to indicate the model has already been run + self.check_model_status() + + def check_model_status(self): + """ + Check the status of the model and update the view + @In, None + @Out, None + """ + def _check_status(): + """ + Local helper function for checking the status of the model in a separate thread + @In, None + @Out, None + """ + current_status = self.model.status + while True: + if current_status != self.model.status: + current_status = self.model.status + self.view.set_status(self.model.status) + time.sleep(0.5) + + thread = threading.Thread(target=_check_status) + thread.daemon = True + thread.name = "ModelStatusChecker" + thread.start() diff --git a/package/ui/controllers/text_output.py b/package/ui/controllers/text_output.py index e826492a..3b936f2d 100644 --- a/package/ui/controllers/text_output.py +++ b/package/ui/controllers/text_output.py @@ -20,17 +20,19 @@ def __init__(self, model, view): def toggle_show_text(self): """ - Toggle the visibility of the output text widget + Toggle the visibility of the output text widget and resize the window to fit @In, None @Out, None """ if self.view.is_showing: # Hide output - self.view.text.grid_forget() + # self.view.text.grid_forget() + self.view.hide_text_output() self.view.show_hide_button.config(text='Show Output') else: # Show output - self.view.text.grid(row=1, column=0, sticky='nsew') + # self.view.text.grid(row=1, column=0, sticky='nsew') + self.view.show_text_output() self.view.show_hide_button.config(text='Hide Output') - self.view.is_showing = not self.view.is_showing + # self.view.is_showing = not self.view.is_showing class StdoutRedirector: diff --git a/package/ui/models/main.py b/package/ui/models/main.py index c68f6842..5b365309 100644 --- a/package/ui/models/main.py +++ b/package/ui/models/main.py @@ -1,7 +1,16 @@ import threading from typing import Callable +import time +from enum import Enum +class ModelStatus(Enum): + """ Enum for model status """ + NOT_STARTED = "Not yet run" + RUNNING = "Running" + FINISHED = "Finished" + ERROR = "Error" + class Model: """ Runs a function in a separate thread """ @@ -15,6 +24,7 @@ def __init__(self, func: Callable, **kwargs): self.func = func self.thread = None self.kwargs = kwargs + self.status = ModelStatus.NOT_STARTED def start(self): """ @@ -22,8 +32,23 @@ def start(self): @In, None @Out, None """ - self.thread = threading.Thread(target=self.func, kwargs=self.kwargs) + def func_wrapper(): + """ + Wrapper for the function to run and set the status to FINISHED when done + @In, None + @Out, None + """ + self.status = ModelStatus.RUNNING + try: + self.func(**self.kwargs) + except: + self.status = ModelStatus.ERROR + else: + self.status = ModelStatus.FINISHED + + self.thread = threading.Thread(target=func_wrapper) self.thread.daemon = True + self.thread.name = self.get_package_name() self.thread.start() def get_package_name(self): diff --git a/package/ui/views/file_selection.py b/package/ui/views/file_selection.py index 929637c9..3b6a7844 100644 --- a/package/ui/views/file_selection.py +++ b/package/ui/views/file_selection.py @@ -1,5 +1,4 @@ from typing import Optional, Callable -import os import tkinter as tk diff --git a/package/ui/views/model_status.py b/package/ui/views/model_status.py new file mode 100644 index 00000000..3f44abb5 --- /dev/null +++ b/package/ui/views/model_status.py @@ -0,0 +1,34 @@ +import tkinter as tk +from ..models.main import ModelStatus as ModelStatusEnum + + +class ModelStatus(tk.Frame): + """ A widget for displaying the status of the model. """ + def __init__(self, master: tk.Widget, **kwargs): + """ + Constructor + @In, master, tk.Widget, the parent widget + @In, kwargs, dict, keyword arguments + @Out, None + """ + super().__init__(master, **kwargs) + self.status = tk.StringVar() + self.status.set("Model not yet run") + self.status_label = tk.Label(self, textvariable=self.status, bg="white", anchor='w', padx=10, pady=3) + self.status_label.pack(side='left') + self.grid_columnconfigure(0, weight=1) + + def set_status(self, new_status: ModelStatusEnum): + """ + Set the status label + @In, new_status, ModelStatusEnum, the new status + @Out, None + """ + self.status.set(new_status.value) + # Change the color of the label based on the status + if new_status == ModelStatusEnum.FINISHED: + self.status_label.config(fg='green') + elif new_status == ModelStatusEnum.ERROR: + self.status_label.config(fg='red') + else: + self.status_label.config(fg='black') diff --git a/package/ui/views/text_output.py b/package/ui/views/text_output.py index 23977e37..2279e154 100644 --- a/package/ui/views/text_output.py +++ b/package/ui/views/text_output.py @@ -1,5 +1,6 @@ import tkinter as tk from tkinter.scrolledtext import ScrolledText +from .model_status import ModelStatus class TextOutput(tk.Frame): @@ -14,9 +15,37 @@ def __init__(self, master, **kwargs): super().__init__(master, **kwargs) self.show_hide_button = tk.Button(self, text='Hide Ouptut', pady=5, width=15) self.show_hide_button.grid(row=0, column=0, sticky='w') + self.model_status = ModelStatus(self) + self.model_status.grid(row=0, column=1, sticky='e') self.text = ScrolledText(self, state=tk.DISABLED) self.is_showing = True # To use with show/hide button - self.text.grid(row=1, column=0, sticky='nsew') + self.text.grid(row=1, column=0, sticky='nsew', columnspan=2) self.grid_rowconfigure(0, minsize=50) self.grid_rowconfigure(1, weight=1) self.grid_columnconfigure(0, weight=1) + + def show_text_output(self): + """ + Show the text output widget + @In, None + @Out, None + """ + self.text.grid(row=1, column=0, sticky='nsew', columnspan=2) + self.show_hide_button.config(text='Hide Output') + self.is_showing = True + # Set window to default size + self.master.update() + self.master.geometry("800x600") + + def hide_text_output(self): + """ + Hide the text output widget + @In, None + @Out, None + """ + self.text.grid_forget() + self.show_hide_button.config(text='Show Output') + self.is_showing = False + # Reduce window size + self.master.update() + self.master.geometry("350x175")