diff --git a/qfieldsync/gui/checker_feedback_table.py b/qfieldsync/gui/checker_feedback_table.py index 14944bd1..c5505f7c 100644 --- a/qfieldsync/gui/checker_feedback_table.py +++ b/qfieldsync/gui/checker_feedback_table.py @@ -20,31 +20,65 @@ ***************************************************************************/ """ -from libqfieldsync.project_checker import Feedback, ProjectCheckerFeedback -from qgis.core import QgsApplication -from qgis.PyQt.QtCore import Qt -from qgis.PyQt.QtWidgets import QLabel, QSizePolicy, QTableWidget, QTableWidgetItem +from typing import Optional + +from libqfieldsync.project_checker import ( + Feedback, + FeedbackTypeId, + ProjectCheckerFeedback, +) +from qgis.core import QgsApplication, QgsProject +from qgis.PyQt.QtCore import Qt, pyqtSignal +from qgis.PyQt.QtWidgets import ( + QHBoxLayout, + QHeaderView, + QLabel, + QPushButton, + QSizePolicy, + QTableWidget, + QTableWidgetItem, + QWidget, +) from qfieldsync.utils.qt_utils import make_icon class CheckerFeedbackTable(QTableWidget): + feedback_fixed = pyqtSignal() + def __init__(self, checker_feedback: ProjectCheckerFeedback, *args, **kwargs): super().__init__(*args, **kwargs) + # dictionary mapping FeedbackTypeId to fix action callbacks + self._fix_handlers = { + FeedbackTypeId.PROJECT_IS_DIRTY: self._fix_project_is_dirty, + } + self.setColumnCount(2) self.setHorizontalHeaderLabels(["", self.tr("Message")]) - self.horizontalHeader().setStretchLastSection(True) + self.horizontalHeader().setSectionResizeMode( + 0, QHeaderView.ResizeMode.ResizeToContents + ) + self.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) + self.setRowCount(0) self.setMinimumHeight(100) self.setSizePolicy( QSizePolicy.Policy.MinimumExpanding, QSizePolicy.Policy.MinimumExpanding ) + self.verticalHeader().hide() + self.horizontalHeader().sectionResized.connect(self.resizeRowsToContents) + self.setWordWrap(True) + + self.set_feedback(checker_feedback) + + def set_feedback(self, checker_feedback: ProjectCheckerFeedback) -> None: + self.setRowCount(0) + for layer_id in checker_feedback.feedbacks: for feedback in checker_feedback.feedbacks[layer_id]: row = self.rowCount() - self.insertRow(row) # first column @@ -72,6 +106,10 @@ def __init__(self, checker_feedback: ProjectCheckerFeedback, *args, **kwargs): self.setItem(row, 1, item) # we do not escape the values on purpose to support Markdown/HTML + cell_widget = QWidget() + cell_layout = QHBoxLayout(cell_widget) + cell_layout.setContentsMargins(0, 0, 8, 0) + label = QLabel("**{}**\n\n{}".format(source, feedback.message)) label.setWordWrap(True) label.setMargin(5) @@ -83,10 +121,38 @@ def __init__(self, checker_feedback: ProjectCheckerFeedback, *args, **kwargs): | Qt.TextInteractionFlag.LinksAccessibleByKeyboard ) label.setOpenExternalLinks(True) - self.setCellWidget(row, 1, label) + cell_layout.addWidget(label, stretch=1) + + fix_button = self._get_feedback_fix(feedback) + if fix_button: + cell_layout.addWidget( + fix_button, + alignment=Qt.AlignmentFlag.AlignRight + | Qt.AlignmentFlag.AlignVCenter, + ) + + self.setCellWidget(row, 1, cell_widget) - self.verticalHeader().hide() - self.resizeColumnsToContents() self.resizeRowsToContents() - self.horizontalHeader().sectionResized.connect(self.resizeRowsToContents) - self.setWordWrap(True) + + def _get_feedback_fix(self, feedback: Feedback) -> Optional[QPushButton]: + """Return a QPushButton configured with a fix callback.""" + fix_action = self._fix_handlers.get(feedback.type_id) + if not fix_action: + return None + + fix_button = QPushButton(self.tr("Fix!")) + + def on_fix_clicked( + _checked: bool, + action=fix_action, + target_feedback=feedback, + ) -> None: + action(target_feedback) + self.feedback_fixed.emit() + + fix_button.clicked.connect(on_fix_clicked) + return fix_button + + def _fix_project_is_dirty(self, _feedback: Feedback) -> None: + QgsProject.instance().write() diff --git a/qfieldsync/gui/cloud_transfer_dialog.py b/qfieldsync/gui/cloud_transfer_dialog.py index 6d4a4a25..eef925f8 100644 --- a/qfieldsync/gui/cloud_transfer_dialog.py +++ b/qfieldsync/gui/cloud_transfer_dialog.py @@ -265,14 +265,17 @@ def show_project_compatibility_page(self): if not self.is_project_compatible_page_prepared: feedback = None if self.cloud_project and self.cloud_project.is_current_qgis_project: - checker = ProjectChecker(QgsProject.instance()) - feedback = checker.check(ExportType.Cloud) + self.project_checker = ProjectChecker(QgsProject.instance()) + feedback = self.project_checker.check(ExportType.Cloud) if feedback and feedback.count > 0: # check whether the widget has already been added the guard from adding twice due to repeated showEvent signal has_errors = len(feedback.error_feedbacks) > 0 - feedback_table = CheckerFeedbackTable(feedback) - self.feedbackTableWrapperLayout.addWidget(feedback_table) + self.feedback_table = CheckerFeedbackTable(feedback) + self.feedback_table.feedback_fixed.connect( + self.refresh_project_compatibility_page + ) + self.feedbackTableWrapperLayout.addWidget(self.feedback_table) self.stackedWidget.setCurrentWidget(self.projectCompatibilityPage) self.buttonBox.button(QDialogButtonBox.StandardButton.Apply).setVisible( True @@ -288,6 +291,20 @@ def show_project_compatibility_page(self): self.is_project_compatible_page_prepared = True + def refresh_project_compatibility_page(self): + if not self.cloud_project or not self.cloud_project.is_current_qgis_project: + return + + feedback = self.project_checker.check(ExportType.Cloud) + if feedback.count == 0: + self.show_project_files_fetching_page() + else: + self.feedback_table.set_feedback(feedback) + has_errors = len(feedback.error_feedbacks) > 0 + self.buttonBox.button(QDialogButtonBox.StandardButton.Apply).setEnabled( + not has_errors + ) + def show_project_files_fetching_page(self): self.stackedWidget.setCurrentWidget(self.getProjectFilesPage) self.buttonBox.button(QDialogButtonBox.StandardButton.Apply).setVisible(False) diff --git a/qfieldsync/gui/package_dialog.py b/qfieldsync/gui/package_dialog.py index 16eb6a6a..af78f0b7 100644 --- a/qfieldsync/gui/package_dialog.py +++ b/qfieldsync/gui/package_dialog.py @@ -146,20 +146,37 @@ def setup_gui(self): self.dirsToCopyWidget.refresh_tree() feedback = None - if os.path.exists(self.project.fileName()): + if Path(self.project.fileName()).exists(): feedback = self.project_checker.check(ExportType.Cable) if feedback and feedback.count > 0: has_errors = len(feedback.error_feedbacks) > 0 - feedback_table = CheckerFeedbackTable(feedback) - self.feedbackTableWrapperLayout.addWidget(feedback_table) + self.feedback_table = CheckerFeedbackTable(feedback) + self.feedback_table.feedback_fixed.connect( + self.refresh_project_compatibility_page + ) + self.feedbackTableWrapperLayout.addWidget(self.feedback_table) self.stackedWidget.setCurrentWidget(self.projectCompatibilityPage) self.nextButton.setVisible(True) self.nextButton.setEnabled(not has_errors) else: self.show_package_page() + def refresh_project_compatibility_page(self): + # The project file might not exist on disk if the current session is an unsaved, newly created QGIS project + if not Path(self.project.fileName()).exists(): + return + + feedback = self.project_checker.check(ExportType.Cable) + if feedback.count == 0: + # All issues resolved -> advance directly to package page + self.show_package_page() + else: + self.feedback_table.set_feedback(feedback) + has_errors = len(feedback.error_feedbacks) > 0 + self.nextButton.setEnabled(not has_errors) + def get_export_filename_suggestion(self) -> str: """Get the suggested export filename""" export_dirname = self.qfield_preferences.value("exportDirectoryProject")