pygui revamped progress bar functionality into app task calls to simplify and commonize the functionality, handle and display task exceptions

This commit is contained in:
Blake Harnden 2020-05-03 10:41:36 -07:00
parent 835675480b
commit 0999fabb14
8 changed files with 82 additions and 87 deletions

View file

@ -297,7 +297,6 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
for service_exception in boot_exception.args: for service_exception in boot_exception.args:
exceptions.append(str(service_exception)) exceptions.append(str(service_exception))
return core_pb2.StartSessionResponse(result=False, exceptions=exceptions) return core_pb2.StartSessionResponse(result=False, exceptions=exceptions)
return core_pb2.StartSessionResponse(result=True) return core_pb2.StartSessionResponse(result=True)
def StopSession( def StopSession(

View file

@ -1,6 +1,8 @@
import math import math
import time
import tkinter as tk import tkinter as tk
from tkinter import font, ttk from tkinter import font, ttk
from tkinter.ttk import Progressbar
from core.gui import appconfig, themes from core.gui import appconfig, themes
from core.gui.coreclient import CoreClient from core.gui.coreclient import CoreClient
@ -9,6 +11,7 @@ from core.gui.images import ImageEnum, Images
from core.gui.menubar import Menubar from core.gui.menubar import Menubar
from core.gui.nodeutils import NodeUtils from core.gui.nodeutils import NodeUtils
from core.gui.statusbar import StatusBar from core.gui.statusbar import StatusBar
from core.gui.task import ProgressTask
from core.gui.toolbar import Toolbar from core.gui.toolbar import Toolbar
from core.gui.validation import InputValidation from core.gui.validation import InputValidation
@ -29,6 +32,8 @@ class Application(ttk.Frame):
self.canvas = None self.canvas = None
self.statusbar = None self.statusbar = None
self.validation = None self.validation = None
self.progress = None
self.time = None
# fonts # fonts
self.fonts_size = None self.fonts_size = None
@ -93,6 +98,7 @@ class Application(ttk.Frame):
self.right_frame.grid(row=0, column=1, sticky="nsew") self.right_frame.grid(row=0, column=1, sticky="nsew")
self.draw_canvas() self.draw_canvas()
self.draw_status() self.draw_status()
self.progress = Progressbar(self.right_frame, mode="indeterminate")
self.menubar = Menubar(self.master, self) self.menubar = Menubar(self.master, self)
def draw_canvas(self): def draw_canvas(self):
@ -117,6 +123,21 @@ class Application(ttk.Frame):
self.statusbar = StatusBar(self.right_frame, self) self.statusbar = StatusBar(self.right_frame, self)
self.statusbar.grid(sticky="ew") self.statusbar.grid(sticky="ew")
def progress_task(self, task: ProgressTask) -> None:
self.progress.grid(sticky="ew")
self.progress.start()
self.time = time.perf_counter()
task.app = self
task.start()
def progress_task_complete(self) -> None:
self.progress.stop()
self.progress.grid_forget()
total = time.perf_counter() - self.time
self.time = None
message = f"Task ran for {total:.3f} seconds"
self.statusbar.set_status(message)
def on_closing(self): def on_closing(self):
self.menubar.prompt_save_running_session(True) self.menubar.prompt_save_running_session(True)
@ -124,7 +145,6 @@ class Application(ttk.Frame):
appconfig.save(self.guiconfig) appconfig.save(self.guiconfig)
def joined_session_update(self): def joined_session_update(self):
self.statusbar.progress_bar.stop()
if self.core.is_runtime(): if self.core.is_runtime():
self.toolbar.set_runtime() self.toolbar.set_runtime()
else: else:

View file

@ -9,7 +9,7 @@ from core.api.grpc import core_pb2
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
from core.gui.errors import show_grpc_error from core.gui.errors import show_grpc_error
from core.gui.images import ImageEnum, Images from core.gui.images import ImageEnum, Images
from core.gui.task import BackgroundTask from core.gui.task import ProgressTask
from core.gui.themes import PADX, PADY from core.gui.themes import PADX, PADY
if TYPE_CHECKING: if TYPE_CHECKING:
@ -183,12 +183,11 @@ class SessionsDialog(Dialog):
self.join_session(self.selected_session) self.join_session(self.selected_session)
def join_session(self, session_id: int) -> None: def join_session(self, session_id: int) -> None:
self.destroy()
if self.app.core.xml_file: if self.app.core.xml_file:
self.app.core.xml_file = None self.app.core.xml_file = None
self.app.statusbar.progress_bar.start(5) task = ProgressTask(self.app.core.join_session, args=(session_id,))
task = BackgroundTask(self.app, self.app.core.join_session, args=(session_id,)) self.app.progress_task(task)
task.start()
self.destroy()
def double_click_join(self, _event: tk.Event) -> None: def double_click_join(self, _event: tk.Event) -> None:
item = self.tree.selection() item = self.tree.selection()
@ -201,7 +200,6 @@ class SessionsDialog(Dialog):
if not self.selected_session: if not self.selected_session:
return return
logging.debug("delete session: %s", self.selected_session) logging.debug("delete session: %s", self.selected_session)
# self.app.core.delete_session(self.selected_id, self.top)
self.tree.delete(self.selected_id) self.tree.delete(self.selected_id)
self.app.core.delete_session(self.selected_session) self.app.core.delete_session(self.selected_session)
if self.selected_session == self.app.core.session_id: if self.selected_session == self.app.core.session_id:

View file

@ -43,7 +43,12 @@ class ErrorDialog(Dialog):
button.grid(sticky="ew") button.grid(sticky="ew")
def show_grpc_error(e: grpc.RpcError, master, app: "Application"): def show_exception(app: "Application", title: str, exception: Exception) -> None:
dialog = ErrorDialog(app, app, title, str(exception))
dialog.show()
def show_grpc_error(e: grpc.RpcError, master, app: "Application") -> None:
title = [x.capitalize() for x in e.code().name.lower().split("_")] title = [x.capitalize() for x in e.code().name.lower().split("_")]
title = " ".join(title) title = " ".join(title)
title = f"GRPC {title}" title = f"GRPC {title}"
@ -51,8 +56,6 @@ def show_grpc_error(e: grpc.RpcError, master, app: "Application"):
dialog.show() dialog.show()
def show_grpc_response_exceptions(class_name, exceptions, master, app: "Application"): def show_error(app: "Application", title: str, message: str) -> None:
title = f"Exceptions from {class_name}" dialog = ErrorDialog(app, app, title, message)
detail = "\n".join([str(x) for x in exceptions])
dialog = ErrorDialog(master, app, title, detail)
dialog.show() dialog.show()

View file

@ -22,7 +22,7 @@ from core.gui.dialogs.sessionoptions import SessionOptionsDialog
from core.gui.dialogs.sessions import SessionsDialog from core.gui.dialogs.sessions import SessionsDialog
from core.gui.dialogs.throughput import ThroughputDialog from core.gui.dialogs.throughput import ThroughputDialog
from core.gui.nodeutils import ICON_SIZE from core.gui.nodeutils import ICON_SIZE
from core.gui.task import BackgroundTask from core.gui.task import ProgressTask
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
@ -340,9 +340,8 @@ class Menubar(tk.Menu):
self.core.xml_file = filename self.core.xml_file = filename
self.core.xml_dir = str(os.path.dirname(filename)) self.core.xml_dir = str(os.path.dirname(filename))
self.prompt_save_running_session() self.prompt_save_running_session()
self.app.statusbar.progress_bar.start(5) task = ProgressTask(self.core.open_xml, args=(filename,))
task = BackgroundTask(self.app, self.core.open_xml, args=(filename,)) self.app.progress_task(task)
task.start()
def execute_python(self): def execute_python(self):
dialog = ExecutePythonDialog(self.app, self.app) dialog = ExecutePythonDialog(self.app, self.app)

View file

@ -18,7 +18,6 @@ class StatusBar(ttk.Frame):
self.app = app self.app = app
self.status = None self.status = None
self.statusvar = tk.StringVar() self.statusvar = tk.StringVar()
self.progress_bar = None
self.zoom = None self.zoom = None
self.cpu_usage = None self.cpu_usage = None
self.memory = None self.memory = None
@ -28,19 +27,14 @@ class StatusBar(ttk.Frame):
self.draw() self.draw()
def draw(self): def draw(self):
self.columnconfigure(0, weight=1) self.columnconfigure(0, weight=7)
self.columnconfigure(1, weight=5) self.columnconfigure(1, weight=1)
self.columnconfigure(2, weight=1) self.columnconfigure(2, weight=1)
self.columnconfigure(3, weight=1) self.columnconfigure(3, weight=1)
self.columnconfigure(4, weight=1)
frame = ttk.Frame(self, borderwidth=1, relief=tk.RIDGE) frame = ttk.Frame(self, borderwidth=1, relief=tk.RIDGE)
frame.grid(row=0, column=0, sticky="ew") frame.grid(row=0, column=0, sticky="ew")
frame.columnconfigure(0, weight=1) frame.columnconfigure(0, weight=1)
self.progress_bar = ttk.Progressbar(
frame, orient="horizontal", mode="indeterminate"
)
self.progress_bar.grid(sticky="ew")
self.status = ttk.Label( self.status = ttk.Label(
self, self,
@ -49,7 +43,7 @@ class StatusBar(ttk.Frame):
borderwidth=1, borderwidth=1,
relief=tk.RIDGE, relief=tk.RIDGE,
) )
self.status.grid(row=0, column=1, sticky="ew") self.status.grid(row=0, column=0, sticky="ew")
self.zoom = ttk.Label( self.zoom = ttk.Label(
self, self,
@ -58,17 +52,17 @@ class StatusBar(ttk.Frame):
borderwidth=1, borderwidth=1,
relief=tk.RIDGE, relief=tk.RIDGE,
) )
self.zoom.grid(row=0, column=2, sticky="ew") self.zoom.grid(row=0, column=1, sticky="ew")
self.cpu_usage = ttk.Label( self.cpu_usage = ttk.Label(
self, text="CPU TBD", anchor=tk.CENTER, borderwidth=1, relief=tk.RIDGE self, text="CPU TBD", anchor=tk.CENTER, borderwidth=1, relief=tk.RIDGE
) )
self.cpu_usage.grid(row=0, column=3, sticky="ew") self.cpu_usage.grid(row=0, column=2, sticky="ew")
self.alerts_button = ttk.Button( self.alerts_button = ttk.Button(
self, text="Alerts", command=self.click_alerts, style=Styles.green_alert self, text="Alerts", command=self.click_alerts, style=Styles.green_alert
) )
self.alerts_button.grid(row=0, column=4, sticky="ew") self.alerts_button.grid(row=0, column=3, sticky="ew")
def click_alerts(self): def click_alerts(self):
dialog = AlertsDialog(self.app, self.app) dialog = AlertsDialog(self.app, self.app)

View file

@ -1,46 +1,39 @@
import logging import logging
import threading import threading
from typing import Any, Callable from typing import Any, Callable, Tuple
from core.gui.errors import show_grpc_response_exceptions from core.gui.errors import show_exception
class BackgroundTask: class ProgressTask:
def __init__(self, master: Any, task: Callable, callback: Callable = None, args=()): def __init__(
self.master = master self, task: Callable, callback: Callable = None, args: Tuple[Any] = None
self.args = args ):
self.app = None
self.task = task self.task = task
self.callback = callback self.callback = callback
self.thread = None self.args = args
if self.args is None:
self.args = ()
def start(self): def start(self) -> None:
logging.info("starting task") thread = threading.Thread(target=self.run, daemon=True)
self.thread = threading.Thread(target=self.run, daemon=True) thread.start()
self.thread.start()
def run(self): def run(self) -> None:
result = self.task(*self.args) logging.info("running task")
logging.info("task completed") try:
# if start session fails, a response with Result: False and a list of values = self.task(*self.args)
# exceptions is returned if values is None:
if not getattr(result, "result", True): values = ()
if len(getattr(result, "exceptions", [])) > 0: elif values and not isinstance(values, tuple):
self.master.after( values = (values,)
0, if self.callback:
show_grpc_response_exceptions, logging.info("calling callback")
*( self.app.after(0, self.callback, *values)
result.__class__.__name__, except Exception as e:
result.exceptions, logging.exception("progress task exception")
self.master, args = (self.app, "Task Error", e)
self.master, self.app.after(0, show_exception, *args)
) finally:
) self.app.after(0, self.app.progress_task_complete)
if self.callback:
if result is None:
args = ()
elif isinstance(result, (list, tuple)):
args = result
else:
args = (result,)
logging.info("calling callback: %s", args)
self.master.after(0, self.callback, *args)

View file

@ -1,5 +1,4 @@
import logging import logging
import time
import tkinter as tk import tkinter as tk
from enum import Enum from enum import Enum
from functools import partial from functools import partial
@ -10,11 +9,12 @@ from core.api.grpc import core_pb2
from core.gui.dialogs.customnodes import CustomNodesDialog from core.gui.dialogs.customnodes import CustomNodesDialog
from core.gui.dialogs.marker import MarkerDialog from core.gui.dialogs.marker import MarkerDialog
from core.gui.dialogs.runtool import RunToolDialog from core.gui.dialogs.runtool import RunToolDialog
from core.gui.errors import show_error
from core.gui.graph.enums import GraphMode from core.gui.graph.enums import GraphMode
from core.gui.graph.shapeutils import ShapeType, is_marker from core.gui.graph.shapeutils import ShapeType, is_marker
from core.gui.images import ImageEnum, Images from core.gui.images import ImageEnum, Images
from core.gui.nodeutils import NodeDraw, NodeUtils from core.gui.nodeutils import NodeDraw, NodeUtils
from core.gui.task import BackgroundTask from core.gui.task import ProgressTask
from core.gui.themes import Styles from core.gui.themes import Styles
from core.gui.tooltip import Tooltip from core.gui.tooltip import Tooltip
@ -47,7 +47,6 @@ class Toolbar(ttk.Frame):
""" """
super().__init__(master, **kwargs) super().__init__(master, **kwargs)
self.app = app self.app = app
self.time = None
# design buttons # design buttons
self.play_button = None self.play_button = None
@ -263,22 +262,18 @@ class Toolbar(ttk.Frame):
server. server.
""" """
self.app.menubar.change_menubar_item_state(is_runtime=True) self.app.menubar.change_menubar_item_state(is_runtime=True)
self.app.statusbar.progress_bar.start(5)
self.app.canvas.mode = GraphMode.SELECT self.app.canvas.mode = GraphMode.SELECT
self.time = time.perf_counter() task = ProgressTask(self.app.core.start_session, self.start_callback)
task = BackgroundTask(self, self.app.core.start_session, self.start_callback) self.app.progress_task(task)
task.start()
def start_callback(self, response: core_pb2.StartSessionResponse): def start_callback(self, response: core_pb2.StartSessionResponse):
self.app.statusbar.progress_bar.stop()
total = time.perf_counter() - self.time
message = f"Start ran for {total:.3f} seconds"
self.app.statusbar.set_status(message)
self.time = None
if response.result: if response.result:
self.set_runtime() self.set_runtime()
self.app.core.set_metadata() self.app.core.set_metadata()
self.app.core.show_mobility_players() self.app.core.show_mobility_players()
else:
message = "\n".join(response.exceptions)
show_error(self.app, "Start Session Error", message)
def set_runtime(self): def set_runtime(self):
self.runtime_frame.tkraise() self.runtime_frame.tkraise()
@ -450,19 +445,13 @@ class Toolbar(ttk.Frame):
""" """
redraw buttons on the toolbar, send node and link messages to grpc server redraw buttons on the toolbar, send node and link messages to grpc server
""" """
logging.info("Click stop button") logging.info("clicked stop button")
self.app.menubar.change_menubar_item_state(is_runtime=False) self.app.menubar.change_menubar_item_state(is_runtime=False)
self.app.statusbar.progress_bar.start(5) task = ProgressTask(self.app.core.stop_session, self.stop_callback)
self.time = time.perf_counter() self.app.progress_task(task)
task = BackgroundTask(self, self.app.core.stop_session, self.stop_callback)
task.start()
def stop_callback(self, response: core_pb2.StopSessionResponse): def stop_callback(self, response: core_pb2.StopSessionResponse):
self.app.statusbar.progress_bar.stop()
self.set_design() self.set_design()
total = time.perf_counter() - self.time
message = f"Stopped in {total:.3f} seconds"
self.app.statusbar.set_status(message)
self.app.canvas.stopped_session() self.app.canvas.stopped_session()
def update_annotation( def update_annotation(