From 271fa18679e9f7dc49e774c0b3e85adbf3755a84 Mon Sep 17 00:00:00 2001 From: Kevin Phoenix Date: Wed, 31 Jul 2024 14:41:59 -0700 Subject: [PATCH] Improve jobs view (#1303) * Improve jobs view * Fix missing rename --- angrmanagement/logic/jobmanager.py | 18 +- angrmanagement/ui/views/jobs_view.py | 13 +- angrmanagement/ui/widgets/qjobs.py | 318 ++++++++++----------------- 3 files changed, 133 insertions(+), 216 deletions(-) diff --git a/angrmanagement/logic/jobmanager.py b/angrmanagement/logic/jobmanager.py index fbfa2617a..6ae9d233d 100644 --- a/angrmanagement/logic/jobmanager.py +++ b/angrmanagement/logic/jobmanager.py @@ -146,11 +146,15 @@ def add_job(self, job: Job) -> None: self.callback_job_added(job) self.jobs_queue.put(job) - def cancel_job(self, job: Job) -> None: + def cancel_job(self, job: Job) -> bool: if job in self.jobs: self.jobs.remove(job) - if self.worker_thread is not None and self.worker_thread.current_job == job: - self.worker_thread.keyboard_interrupt() + job.cancelled = True + if self.worker_thread is not None and self.worker_thread.current_job == job: + self.worker_thread.keyboard_interrupt() + + return True + return False def interrupt_current_job(self) -> None: """Notify the current running job that the user requested an interrupt. The job may ignore it.""" @@ -201,7 +205,7 @@ def callback_job_added(self, job: Job) -> None: """ if self.workspace.view_manager.first_view_in_category("jobs") is not None: jobs_view = self.workspace.view_manager.first_view_in_category("jobs") - gui_thread_schedule_async(jobs_view.q_jobs.add_new_job, args=[job]) + gui_thread_schedule_async(jobs_view.qjobs.add_new_job, args=[job]) def callback_worker_progress(self, job: Job) -> None: """ @@ -210,7 +214,7 @@ def callback_worker_progress(self, job: Job) -> None: """ if self.workspace.view_manager.first_view_in_category("jobs") is not None: jobs_view = self.workspace.view_manager.first_view_in_category("jobs") - gui_thread_schedule_async(jobs_view.q_jobs.change_job_progress, args=[job]) + gui_thread_schedule_async(jobs_view.qjobs.change_job_progress, args=[job]) def callback_worker_new_job(self, job: Job) -> None: """ @@ -219,7 +223,7 @@ def callback_worker_new_job(self, job: Job) -> None: """ if self.workspace.view_manager.first_view_in_category("jobs") is not None: jobs_view = self.workspace.view_manager.first_view_in_category("jobs") - gui_thread_schedule_async(jobs_view.q_jobs.change_job_running, args=(job,)) + gui_thread_schedule_async(jobs_view.qjobs.change_job_running, args=(job,)) def callback_job_complete(self, job: Job): """ @@ -228,6 +232,6 @@ def callback_job_complete(self, job: Job): """ if self.workspace.view_manager.first_view_in_category("jobs") is not None: jobs_view = self.workspace.view_manager.first_view_in_category("jobs") - gui_thread_schedule_async(jobs_view.q_jobs.change_job_finish, args=[job]) + gui_thread_schedule_async(jobs_view.qjobs.change_job_finish, args=[job]) # Private methods diff --git a/angrmanagement/ui/views/jobs_view.py b/angrmanagement/ui/views/jobs_view.py index 90ab85f69..f91558102 100644 --- a/angrmanagement/ui/views/jobs_view.py +++ b/angrmanagement/ui/views/jobs_view.py @@ -15,25 +15,24 @@ class JobsView(InstanceView): - """ - This creates a view for the jobs view which creates a table to display - all the jobs being ran or in queue - """ + """JobsView displays all pending, running, and finished jobs in the project.""" + + qjobs: QJobs def __init__(self, workspace: Workspace, default_docking_position: str, instance: Instance) -> None: super().__init__("jobs", workspace, default_docking_position, instance) self.base_caption = "Jobs" # The QJobs widget is initialized in the view, most functions of jobs table is done through QJobs - self.q_jobs = QJobs(workspace) + self.qjobs = QJobs(workspace) vlayout = QVBoxLayout() - vlayout.addWidget(self.q_jobs) + vlayout.addWidget(self.qjobs) vlayout.setContentsMargins(0, 0, 0, 0) self.setLayout(vlayout) self.reload() def closeEvent(self, event) -> None: - self.q_jobs.close() + self.qjobs.close() super().closeEvent(event) def reload(self) -> None: diff --git a/angrmanagement/ui/widgets/qjobs.py b/angrmanagement/ui/widgets/qjobs.py index f7e3be203..a5499ef81 100644 --- a/angrmanagement/ui/widgets/qjobs.py +++ b/angrmanagement/ui/widgets/qjobs.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING import qtawesome as qta -from PySide6.QtCore import QSize, Qt +from PySide6.QtCore import Qt from PySide6.QtWidgets import ( QAbstractItemView, QHBoxLayout, @@ -20,283 +20,197 @@ if TYPE_CHECKING: from angrmanagement.data.jobs.job import Job + from angrmanagement.ui.workspace import Workspace -class CancelButton(QPushButton): - """ - This creates a cancel button for each job in the job views, using the job.cancel() - method or skipping it from the queue - """ +class QIconWidget(qta.IconWidget): + """QIconWidget is a widget that displays a qtawesome icon.""" - def __init__(self, table: QJobs, job: Job, workspace): - super().__init__("Cancel") - self.workspace = workspace + def __init__(self, icon: str, color: str): + super().__init__() + self.setIcon(qta.icon(icon, color=color)) + self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) - self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) - self.clicked.connect(lambda: self.onClick(table, job)) +class PendingWidget(QIconWidget): + """PendingWidget represents a pending job icon in the jobs view table.""" - # On cancel button click, the function will check whether to cancel or skip the job - def onClick(self, table, job): - # Job is cancelled/interrupted if running - if isinstance(job.status, RunningWidget) and job in self.workspace.job_manager.jobs: - self.workspace.job_manager.worker_thread.keyboard_interrupt() - job.cancelled = True - table.change_job_cancel(job) - # Job is skipped if it's pending - elif isinstance(job.status, PendingWidget): - job.cancelled = True - table.change_job_cancel(job) + def __init__(self): + super().__init__("fa.clock-o", "grey") -class CancelledWidget(QWidget): - """ - This creates a status widget to show a job's status that it is - cancelled - """ +class RunningWidget(QIconWidget): + """RunningWidget represents a running job icon in the jobs view table.""" def __init__(self): - super().__init__() - self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + super().__init__("fa5s.spinner", "grey") - # Constructing cancel icon widget - self.cancel_widget = qta.IconWidget() - cancel_icon = qta.icon("ei.remove-sign", color="red") - self.cancel_widget.setIconSize(QSize(45, 26)) - self.cancel_widget.setIcon(cancel_icon) - # Create a horizontal layout and add the labels - hbox = QHBoxLayout() - hbox.addWidget(self.cancel_widget) +class FinishedWidget(QIconWidget): + """FinishedWidget represents a finished job icon in the jobs view table.""" - # Set the layout for the widget - self.setLayout(hbox) + def __init__(self): + super().__init__("fa.check-circle", "green") -class FinishedWidget(QWidget): - """ - This creates a status widget to show a job's status that it is - Finished - """ +class CancelledWidget(QIconWidget): + """CancelledWidget represents a cancelled job icon in the jobs view table.""" def __init__(self): - super().__init__() - self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) - - # Constructing finish icon widget - self.finish_widget = qta.IconWidget() - finish_icon = qta.icon("fa.check-circle", color="green") - self.finish_widget.setIconSize(QSize(45, 30)) - self.finish_widget.setIcon(finish_icon) + super().__init__("ei.remove-sign", "red") - # Create a horizontal layout and add the labels - hbox = QHBoxLayout() - hbox.addWidget(self.finish_widget) - # Set the layout for the widget - self.setLayout(hbox) +class ProgressWidget(QProgressBar): + """ProgressWidget represents a progress bar for a job in the jobs view table.""" - -class PendingWidget(QWidget): - """ - This creates a status widget to show a job's status that it is - Pending - """ + label: QLabel def __init__(self): super().__init__() - self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) - - # Constructing pending icon widget - self.pending_widget = qta.IconWidget() - pending_icon = qta.icon("fa.clock-o", color="black") - self.pending_widget.setIconSize(QSize(45, 26)) - self.pending_widget.setIcon(pending_icon) - hbox = QHBoxLayout() - hbox.addWidget(self.pending_widget) + self.label = QLabel("0%") + self.label.setMinimumHeight(24) - # Set the layout for the widget - self.setLayout(hbox) + layout = QHBoxLayout() + layout.addWidget(self.label) + layout.setAlignment(Qt.AlignmentFlag.AlignRight) + layout.setContentsMargins(0, 0, 8, 0) + self.setLayout(layout) + self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + self.setRange(0, 100) + self.setValue(0) + self.setMinimumHeight(24) -class ProgressWidget(QWidget): - """ - This creates a progress widget to show a job's progress bar with progression - of its completion - """ + def setValue(self, value: int): + super().setValue(value) + self.label.setText(f"{value}%") - def __init__(self): - super().__init__() - self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) - self.setContentsMargins(0, 0, 0, 10) - - # Attributes of progress widget: the text for the percentage and the progress bar - self.text = QLabel("0%") - self.progressBar = QProgressBar() - # Properties for the text/percentage - self.text.setAlignment(Qt.AlignCenter) - self.text.setFixedSize(45, 26) - - # progress bar properties and behaviors - self.progressBar.setAlignment(Qt.AlignCenter) - self.progressBar.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) - self.progressBar.setMinimumHeight(20) # Adjust the minimum height as needed - self.progressBar.setValue(0) +class CancelButton(QPushButton): + """Represents a cancel button for a job in the jobs view table.""" - # Setting layout and adding both widgets to the entire widget - hbox = QHBoxLayout() - hbox.addWidget(self.text) - hbox.addWidget(self.progressBar) - self.setLayout(hbox) + workspace: Workspace + def __init__(self, table: QJobs, job: Job, workspace): + super().__init__("Cancel") + self.workspace = workspace -class RunningWidget(QWidget): - """ - This creates a status widget to show a job's status that it is - currently running/in progress - """ + self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) - def __init__(self): - super().__init__() + self.clicked.connect(lambda: self.onClick(table, job)) - # Create a label for the spinning icon indicating the job is running - self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) - self.spin_widget = qta.IconWidget() - animation = qta.Spin(self.spin_widget, autostart=True) - spin_icon = qta.icon("fa5s.spinner", color="grey", animation=animation) - self.spin_widget.setIconSize(QSize(45, 26)) - self.spin_widget.setIcon(spin_icon) + # On cancel button click, the function will check whether to cancel or skip the job + def onClick(self, table: QJobs, job: Job): + if self.workspace.job_manager.cancel_job(job): + table.change_job_cancel(job) - # Create a horizontal layout and add the labels - hbox = QHBoxLayout() - hbox.addWidget(self.spin_widget) - # Set the layout for the widget - self.setLayout(hbox) +class QJobs(QTableWidget): + """QJobs displays all the jobs and their status/progression.""" + workspace: Workspace + content_widget: QWidget + content_layout: QVBoxLayout -class QJobs(QTableWidget): - """ - This creates a table widget to display all the jobs and - their status/progression on running the binaries - """ + row_map: dict[Job, int] + status_map: dict[Job, QWidget] + progress_bar_map: dict[Job, ProgressWidget] + cancel_button_map: dict[Job, CancelButton] - def __init__(self, workspace, parent=None): + def __init__(self, workspace: Workspace, parent=None): super().__init__(0, 4, parent) # 0 rows and 4 columns self.workspace = workspace self.content_widget = QWidget() self.content_layout = QVBoxLayout(self.content_widget) + self.row_map = {} + self.status_map = {} + self.progress_bar_map = {} + self.cancel_button_map = {} self.setHorizontalHeaderLabels(["Status", "Name", "Progress", "Cancel"]) # Set size policy to expand - self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) - self.setSelectionBehavior(QAbstractItemView.SelectRows) + self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) # Column and Height Behaviors - self.horizontalHeader().setDefaultAlignment(Qt.AlignLeft) + self.horizontalHeader().setDefaultAlignment(Qt.AlignmentFlag.AlignLeft) self.horizontalHeader().setSortIndicatorShown(True) - self.horizontalHeader().setSectionResizeMode(QHeaderView.Interactive) - self.horizontalHeader().setStretchLastSection(True) - self.verticalHeader().setDefaultSectionSize(45) + self.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Interactive) + self.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch) + self.verticalHeader().setDefaultSectionSize(24) # Set default column widths, different for each column - # 120 - column_widths = [65, 200, 780, 30] + column_widths = [16, 200, 780, 65] for idx, width in enumerate(column_widths): self.setColumnWidth(idx, width) # Private Methods - def _add_table_row(self, job: Job, status): # Status: Any of the status widgets - """ - This method creates a row for a job in the jobs view table, - takes an argument for a job, and an argument for status(Pending by default) - """ + def _add_table_row(self, job: Job, status: QWidget): + """_add_table_row is a private method that adds a row to the jobs view table.""" - # Assigning the row to the job as an attribute - job.row = self.rowCount() - self.insertRow(job.row) + # Assign the row to the job in the row map + new_row = self.rowCount() + self.row_map[job] = new_row + self.insertRow(new_row) - # Assigning the status of the job as an attribute and - # setting the status widget in the table(column 1) - job.status = status - self.setCellWidget(job.row, 0, job.status) + # Assign the status of the job as an attribute and set the status widget in the table (column 1) + self.status_map[job] = status + self.setCellWidget(new_row, 0, status) - # Setting the name of the job as a widget in the table(column 2) + # Set the name of the job as a widget in the table (column 2) job_name = QTableWidgetItem(job.name) - self.setItem(job.row, 1, job_name) - job_name.setFlags(job_name.flags() & ~Qt.ItemIsEditable) # Make the item non-editable - - # Assigning the progress bar of the job as an - # attribute and setting the progress widget in the table(column 3) - progressBar = ProgressWidget() - job.progress_bar = progressBar - self.setCellWidget(job.row, 2, job.progress_bar) + self.setItem(new_row, 1, job_name) + job_name.setFlags(job_name.flags() & ~Qt.ItemFlag.ItemIsEditable) # Make the item non-editable - # Assigning the cancel button of the job as an attribute and - # setting the cancel button widget in the table(column 4) - job.cancel_button = CancelButton(self, job, self.workspace) + # Assign the progress bar of the job to the progress bar map and set the + # progress widget in the table (column 3) + progress_bar = ProgressWidget() + self.progress_bar_map[job] = progress_bar + self.setCellWidget(new_row, 2, progress_bar) - # Constructing a seperate container to adjust margins of button - button_QWidget = QWidget() - hbox = QHBoxLayout(button_QWidget) - hbox.setContentsMargins(5, 5, 5, 5) - hbox.addWidget(job.cancel_button) - - self.setCellWidget(job.row, 3, button_QWidget) + # Set the cancel button widget in the table (column 4) + cancel_button = CancelButton(self, job, self.workspace) + self.cancel_button_map[job] = cancel_button + self.setCellWidget(new_row, 3, cancel_button) # Public Methods def add_new_job(self, job: Job): - """ - This method adds a new job to the jobs view table, - it only takes an argument for a job to add a row for it in the table - """ + """Adds a new job to the jobs view table.""" + pending = PendingWidget() self._add_table_row(job, pending) - def change_job_progress(self, job: Job): - """ - This method changes the progress percentage and progress bar of a job, - only takes the argument for the job to set all the changes - """ + def change_job_running(self, job: Job): + """Changes the status of a job in the jobs view table to running.""" + + status = RunningWidget() + self.status_map[job] = status + self.setCellWidget(self.row_map[job], 0, status) - job.progress_bar.progressBar.setValue(int(job.progress_percentage)) - job.progress_bar.text.setText(str(int(job.progress_percentage)) + "%") + def change_job_progress(self, job: Job): + """Changes the progress of a job in the jobs view table.""" - self.setCellWidget(job.row, 2, job.progress_bar) + self.progress_bar_map[job].setValue(int(job.progress_percentage)) def change_job_cancel(self, job: Job): - """ - This method changes the status of a job in the jobs view table to cancelled, - only takes the job as an argument - """ + """Changes the status of a job in the jobs view table to cancelled.""" - job.status = CancelledWidget() - self.setCellWidget(job.row, 0, job.status) + status = CancelledWidget() + self.status_map[job] = status + self.setCellWidget(self.row_map[job], 0, status) + self.cancel_button_map[job].setDisabled(True) def change_job_finish(self, job: Job): - """ - This method changes the the status of a job in the jobs view table to - finish and sets progress to 100, only takes the job as an argument - """ - - job.status = FinishedWidget() - self.setCellWidget(job.row, 0, job.status) - - job.progress_bar.progressBar.setValue(100) - job.progress_bar.text.setText("100%") + """Changes the status of a job in the jobs view table to finished.""" - self.setCellWidget(job.row, 2, job.progress_bar) - - def change_job_running(self, job: Job): - """ - This method changes the status of a job in the jobs view table to running, - only takes the job as an argument - """ + status = FinishedWidget() + self.status_map[job] = status + self.setCellWidget(self.row_map[job], 0, status) - job.status = RunningWidget() - self.setCellWidget(job.row, 0, job.status) + progress_bar = self.progress_bar_map[job] + progress_bar.setValue(100) + self.cancel_button_map[job].setDisabled(True)