From 9e5567a99e255cd878ab3c8836f651eefb9059aa Mon Sep 17 00:00:00 2001 From: Faraphel Date: Sat, 30 Dec 2023 21:50:48 +0100 Subject: [PATCH] simplified the engine navigation menu and signals --- assets/web/common.css | 22 --- assets/web/success.html | 18 -- source/survey/IntegerQuestion.py | 14 +- source/survey/MultipleChoiceQuestion.py | 110 +---------- source/survey/SingleChoiceQuestion.py | 114 +----------- source/survey/Text.py | 20 +- source/survey/TextQuestion.py | 14 +- source/survey/WebMission.py | 34 ++-- source/survey/base/BaseChoiceQuestion.py | 124 ++++++++++++ source/survey/base/BaseSurvey.py | 4 +- source/survey/base/__init__.py | 1 + source/widget/SurveyEngine.py | 84 +++------ source/widget/SurveyNavigation.py | 77 ++++++++ source/widget/__init__.py | 1 + surveys.json | 228 ++++++++++++++--------- 15 files changed, 429 insertions(+), 436 deletions(-) delete mode 100644 assets/web/common.css delete mode 100644 assets/web/success.html create mode 100644 source/survey/base/BaseChoiceQuestion.py create mode 100644 source/widget/SurveyNavigation.py diff --git a/assets/web/common.css b/assets/web/common.css deleted file mode 100644 index 39ff20f..0000000 --- a/assets/web/common.css +++ /dev/null @@ -1,22 +0,0 @@ -.notification { - /* center the div */ - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - - /* set the size to shrink to the text */ - width: fit-content; - - /* set the text */ - text-align: center; - font-size: 150%; - font-family: "Arial", sans-serif; - font-weight: bold; - - /* set the border */ - border: 4px solid black; - border-radius: 2vh; - - padding: 20px; -} diff --git a/assets/web/success.html b/assets/web/success.html deleted file mode 100644 index 4865807..0000000 --- a/assets/web/success.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - Succès - - -
-
-

- Vous avez réussi cette étape.
- Vous pouvez à présent passer à la suivante. -

-
-
- - \ No newline at end of file diff --git a/source/survey/IntegerQuestion.py b/source/survey/IntegerQuestion.py index fe2786d..c8c0ffb 100644 --- a/source/survey/IntegerQuestion.py +++ b/source/survey/IntegerQuestion.py @@ -4,7 +4,7 @@ from PyQt6.QtCore import Qt, pyqtSignal from PyQt6.QtGui import QFont from PyQt6.QtWidgets import QVBoxLayout, QLabel, QSpinBox -from source import translate +from source import translate, widget from source.survey.base import BaseSurvey @@ -46,6 +46,11 @@ class IntegerQuestion(BaseSurvey): self.entry_response.setValue(default) self._layout.addWidget(self.entry_response) + # navigation + self.navigation = widget.SurveyNavigation(signals=signals) + self._layout.addWidget(self.navigation) + self.navigation.show_forward() + @classmethod def from_dict(cls, data: dict[str, Any], signals: dict[str, pyqtSignal]) -> "IntegerQuestion": return cls( @@ -56,13 +61,6 @@ class IntegerQuestion(BaseSurvey): signals=signals, ) - # events - - def on_show(self) -> None: - # immediately mark the survey as successful - if "success" in self.signals: - self.signals["success"].emit() # NOQA: emit exist - # data collection def get_collected_data(self) -> dict: diff --git a/source/survey/MultipleChoiceQuestion.py b/source/survey/MultipleChoiceQuestion.py index c5bafa1..d47a462 100644 --- a/source/survey/MultipleChoiceQuestion.py +++ b/source/survey/MultipleChoiceQuestion.py @@ -1,107 +1,15 @@ -from typing import Any +from typing import Type -from PyQt6.QtCore import Qt, pyqtSignal -from PyQt6.QtGui import QFont -from PyQt6.QtWidgets import QFrame, QVBoxLayout, QLabel, QCheckBox, QLineEdit +from PyQt6.QtWidgets import QAbstractButton, QCheckBox -from source import translate -from source.survey.base import BaseSurvey +from source.survey.base import BaseChoiceQuestion -class MultipleChoiceQuestion(BaseSurvey): - def __init__( - self, - title: translate.Translatable, - choices: dict[Any, translate.Translatable], - details_choice_enabled: bool = None, - details_choice_id: str = None, - details_choice_text: translate.Translatable = None, - signals: dict[str, pyqtSignal] = None - ): - super().__init__() - - self.details_choice_enabled = details_choice_enabled if details_choice_enabled is not None else None - self.details_choice_id = details_choice_id if details_choice_id is not None else None - self.details_choice_text = details_choice_text if details_choice_text is not None else None - self.signals = signals if signals is not None else {} - - # set layout - self._layout = QVBoxLayout() - self.setLayout(self._layout) - - # question title - self.label_question = QLabel() - self._layout.addWidget(self.label_question) - self.label_question.setText(translate.translate(title)) - self.label_question.setAlignment(Qt.AlignmentFlag.AlignCenter) - - font_title = self.label_question.font() - font_title.setPointSize(24) - font_title.setWeight(QFont.Weight.Bold) - self.label_question.setFont(font_title) - - # responses - self.frame_responses = QFrame() - self._layout.addWidget(self.frame_responses) - - self._layout_responses = QVBoxLayout() - self.frame_responses.setLayout(self._layout_responses) - - self.button_responses: dict[str, QCheckBox] = {} - - for choice_id, choice_text in choices.items(): - # create a radio button for that choice - button = QCheckBox() - button.setText(translate.translate(choice_text)) - - # add the button to the frame - self._layout_responses.addWidget(button) - - # save the button - self.button_responses[choice_id] = button - - if self.details_choice_enabled: - self.button_response_other = QCheckBox() - self._layout_responses.addWidget(self.button_response_other) - self.button_response_other.setText(translate.translate(self.details_choice_text)) - self.button_response_other.toggled.connect(self._on_response_other_check) # NOQA: connect exist - - self.entry_response_other = QLineEdit() - self._layout_responses.addWidget(self.entry_response_other) - self.entry_response_other.setEnabled(False) +class MultipleChoiceQuestion(BaseChoiceQuestion): + @classmethod + def get_button_choice_class(cls) -> Type[QAbstractButton]: + return QCheckBox @classmethod - def from_dict(cls, data: dict[str, Any], signals: dict[str, pyqtSignal]) -> "MultipleChoiceQuestion": - return cls( - title=data["title"], - choices=data["choices"], - details_choice_enabled=data.get("details_choice_enabled"), - details_choice_id=data.get("details_choice_id"), - details_choice_text=data.get("details_choice_text"), - - signals=signals, - ) - - def on_show(self) -> None: - if "success" in self.signals: - # the user can skip a text whenever he wants to, directly signal a success - self.signals["success"].emit() # NOQA: emit exist - - def _on_response_other_check(self): - # refresh the other entry response status - self.entry_response_other.setEnabled(self.button_response_other.isChecked()) - - def get_collected_data(self) -> dict: - collected_data = { - "choice": [ - choice_id - for choice_id, button in self.button_responses.items() - if button.isChecked() - ] - } - - if self.details_choice_enabled: - collected_data["choice"].append(self.details_choice_id) - collected_data["other"] = self.entry_response_other.text() - - return collected_data + def are_buttons_exclusive(cls) -> bool: + return False diff --git a/source/survey/SingleChoiceQuestion.py b/source/survey/SingleChoiceQuestion.py index 5d4f877..bced31b 100644 --- a/source/survey/SingleChoiceQuestion.py +++ b/source/survey/SingleChoiceQuestion.py @@ -1,111 +1,15 @@ -from typing import Any +from typing import Type -from PyQt6.QtCore import Qt, pyqtSignal -from PyQt6.QtGui import QFont -from PyQt6.QtWidgets import QFrame, QVBoxLayout, QLabel, QRadioButton, QButtonGroup, QLineEdit, QAbstractButton +from PyQt6.QtWidgets import QAbstractButton, QRadioButton -from source import translate -from source.survey.base import BaseSurvey +from source.survey.base import BaseChoiceQuestion -class SingleChoiceQuestion(BaseSurvey): - def __init__( - self, - title: translate.Translatable, - details_choice_enabled: bool = None, - details_choice_id: str = None, - details_choice_text: translate.Translatable = None, - choices: dict[Any, translate.Translatable] = None, - signals: dict[str, pyqtSignal] = None - ): - super().__init__() - - self.details_choice_enabled = details_choice_enabled if details_choice_enabled is not None else None - self.details_choice_id = details_choice_id if details_choice_id is not None else None - self.details_choice_text = details_choice_text if details_choice_text is not None else None - - choices = choices if choices is not None else {} - signals = signals if signals is not None else {} - - # set layout - self._layout = QVBoxLayout() - self.setLayout(self._layout) - - # question title - self.label_question = QLabel() - self._layout.addWidget(self.label_question) - self.label_question.setText(translate.translate(title)) - self.label_question.setAlignment(Qt.AlignmentFlag.AlignCenter) - - font_title = self.label_question.font() - font_title.setPointSize(24) - font_title.setWeight(QFont.Weight.Bold) - self.label_question.setFont(font_title) - - # responses - self.frame_responses = QFrame() - self._layout.addWidget(self.frame_responses) - - self._layout_responses = QVBoxLayout() - self.frame_responses.setLayout(self._layout_responses) - - self.group_responses = QButtonGroup() - - if "success" in signals: - # checking any button allow the user to go to the next step - self.group_responses.buttonClicked.connect(signals["success"].emit) # NOQA: connect and emit exists - - self.button_responses_id: dict[QAbstractButton, str] = {} - - for choice_id, choice_text in choices.items(): - # create a radio button for that choice - button = QRadioButton() - button.setText(translate.translate(choice_text)) - - # add the button to the frame - self._layout_responses.addWidget(button) - - # add the button to the group - self.group_responses.addButton(button) - self.button_responses_id[button] = choice_id - - if self.details_choice_enabled: - self.button_response_other = QRadioButton() - self._layout_responses.addWidget(self.button_response_other) - self.button_responses_id[self.button_response_other] = self.details_choice_id - - self.button_response_other.setText(translate.translate(self.details_choice_text)) - self.group_responses.addButton(self.button_response_other) - self.button_response_other.toggled.connect(self._on_response_other_check) # NOQA: connect exist - - self.entry_response_other = QLineEdit() - self._layout_responses.addWidget(self.entry_response_other) - self.entry_response_other.setEnabled(False) +class SingleChoiceQuestion(BaseChoiceQuestion): + @classmethod + def get_button_choice_class(cls) -> Type[QAbstractButton]: + return QRadioButton @classmethod - def from_dict(cls, data: dict[str, Any], signals: dict[str, pyqtSignal]) -> "SingleChoiceQuestion": - return cls( - title=data["title"], - choices=data["choices"], - details_choice_enabled=data.get("details_choice_enabled"), - details_choice_id=data.get("details_choice_id"), - details_choice_text=data.get("details_choice_text"), - - signals=signals, - ) - - def get_collected_data(self) -> dict: - checked_button = self.group_responses.checkedButton() - - collected_data = { - "choice": self.button_responses_id[checked_button] if checked_button is not None else None, - } - - if self.details_choice_enabled: - collected_data["other"] = self.entry_response_other.text() - - return collected_data - - def _on_response_other_check(self): - # refresh the other entry response status - self.entry_response_other.setEnabled(self.button_response_other.isChecked()) + def are_buttons_exclusive(cls) -> bool: + return True diff --git a/source/survey/Text.py b/source/survey/Text.py index 003b5a3..7c2f909 100644 --- a/source/survey/Text.py +++ b/source/survey/Text.py @@ -4,7 +4,7 @@ from PyQt6.QtCore import Qt, pyqtSignal from PyQt6.QtGui import QFont from PyQt6.QtWidgets import QVBoxLayout, QLabel -from source import translate +from source import translate, widget from source.survey.base import BaseSurvey @@ -19,7 +19,6 @@ class Text(BaseSurvey): super().__init__() self.abandonable = abandonable if abandonable is not None else False - self.signals = signals if signals is not None else {} # set the layout self._layout = QVBoxLayout() @@ -43,6 +42,14 @@ class Text(BaseSurvey): self.label_description.setText(translate.translate(description)) self.label_description.setAlignment(Qt.AlignmentFlag.AlignCenter) + # navigation + self.navigation = widget.SurveyNavigation(signals=signals) + self._layout.addWidget(self.navigation) + + self.navigation.show_forward() # always show forward + if self.abandonable: + self.navigation.show_abandon() # if enabled, show abandon + @classmethod def from_dict(cls, data: dict[str, Any], signals: dict[str, pyqtSignal]) -> "Text": return cls( @@ -52,12 +59,3 @@ class Text(BaseSurvey): signals=signals ) - - def on_show(self) -> None: - if "success" in self.signals: - # the user can skip a text whenever he wants to, directly signal a success - self.signals["success"].emit() # NOQA: emit exist - - # if abandon is enabled, emit on the corresponding signal - if self.abandonable and "abandon" in self.signals: - self.signals["abandon"].emit() # NOQA: emit exist diff --git a/source/survey/TextQuestion.py b/source/survey/TextQuestion.py index 1d57d43..eed3d38 100644 --- a/source/survey/TextQuestion.py +++ b/source/survey/TextQuestion.py @@ -4,7 +4,7 @@ from PyQt6.QtCore import Qt, pyqtSignal from PyQt6.QtGui import QFont from PyQt6.QtWidgets import QVBoxLayout, QLabel, QTextEdit -from source import translate +from source import translate, widget from source.survey.base import BaseSurvey @@ -33,6 +33,11 @@ class TextQuestion(BaseSurvey): self.entry_response = QTextEdit() self._layout.addWidget(self.entry_response) + # navigation + self.navigation = widget.SurveyNavigation(signals=signals) + self._layout.addWidget(self.navigation) + self.navigation.show_forward() + @classmethod def from_dict(cls, data: dict[str, Any], signals: dict[str, pyqtSignal]) -> "TextQuestion": return cls( @@ -40,13 +45,6 @@ class TextQuestion(BaseSurvey): signals=signals, ) - # events - - def on_show(self) -> None: - # immediately mark the survey as successful - if "success" in self.signals: - self.signals["success"].emit() # NOQA: emit exist - # data collection def get_collected_data(self) -> dict: diff --git a/source/survey/WebMission.py b/source/survey/WebMission.py index 8122019..9908ef4 100644 --- a/source/survey/WebMission.py +++ b/source/survey/WebMission.py @@ -1,17 +1,12 @@ import time -from pathlib import Path from typing import Optional, Any from PyQt6.QtCore import Qt, QTimer, pyqtSignal, QUrl, QEvent, QObject, QPointF from PyQt6.QtGui import QFont, QMouseEvent, QResizeEvent, QKeyEvent from PyQt6.QtWidgets import QLabel, QVBoxLayout, QSizePolicy -from source import translate -from source import assets_path +from source import translate, widget from source.survey.base import BaseSurvey -from source.widget import Browser - -page_success_path: Path = assets_path / "web/success.html" class WebMission(BaseSurvey): @@ -52,7 +47,7 @@ class WebMission(BaseSurvey): self.label_title.setFont(font_title) # web page - self.browser = Browser() + self.browser = widget.Browser() self._layout.addWidget(self.browser) self.browser.web.focusProxy().installEventFilter(self) # capture the event in eventFilter self.browser.web.urlChanged.connect(self._on_url_changed) # NOQA: connect exist @@ -76,6 +71,10 @@ class WebMission(BaseSurvey): # setup the events self.browser.web.page().scrollPositionChanged.connect(self._on_scroll_position_changed) + # navigation + self.navigation = widget.SurveyNavigation(signals=signals) + self._layout.addWidget(self.navigation) + @classmethod def from_dict(cls, data: dict[str, Any], signals: dict[str, pyqtSignal]) -> "WebMission": return cls( @@ -170,6 +169,8 @@ class WebMission(BaseSurvey): return super().eventFilter(obj, event) def on_show(self) -> None: + # TODO: remove ? + # initialize the start time self.start_time = time.time() @@ -190,6 +191,8 @@ class WebMission(BaseSurvey): self.timer_skip.start() def on_hide(self) -> None: + # TODO: remove ? + # disable full screen mode self.window().showNormal() @@ -215,13 +218,17 @@ class WebMission(BaseSurvey): # mark the mission as finished self._finished = True - # change the content of the page to the success message - self.browser.web.load(QUrl.fromLocalFile(str(page_success_path.absolute()))) + def _on_url_changed(self): + # log the new url + self._save_event( + type="url", + url=self.browser.web.url().toString() + ) def _on_time_skip(self): # when the timer to allow skip have run out if "skip" in self.signals: - self.signals["skip"].emit() # NOQA: emit exist + self.navigation.show_skip() # condition @@ -258,10 +265,3 @@ class WebMission(BaseSurvey): return { "event": self._collected_events, } - - def _on_url_changed(self): - # log the new url - self._save_event( - type="url", - url=self.browser.web.url().toString() - ) diff --git a/source/survey/base/BaseChoiceQuestion.py b/source/survey/base/BaseChoiceQuestion.py new file mode 100644 index 0000000..d68ee7a --- /dev/null +++ b/source/survey/base/BaseChoiceQuestion.py @@ -0,0 +1,124 @@ +from abc import abstractmethod +from typing import Any, Type + +from PyQt6.QtCore import Qt, pyqtSignal +from PyQt6.QtGui import QFont +from PyQt6.QtWidgets import QFrame, QVBoxLayout, QLabel, QButtonGroup, QLineEdit, QAbstractButton + +from source import translate, widget +from source.survey.base import BaseSurvey + + +class BaseChoiceQuestion(BaseSurvey): + """ + Base for a question that contains multiple options + """ + + @classmethod + @abstractmethod + def get_button_choice_class(cls) -> Type[QAbstractButton]: + """ + The class for the button representing the choices + """ + + @classmethod + @abstractmethod + def are_buttons_exclusive(cls) -> bool: + """ + Are the buttons exclusive ? + """ + + def __init__( + self, + title: translate.Translatable, + choices: dict, + signals: dict[str, pyqtSignal] = None + ): + super().__init__() + + signals = signals if signals is not None else {} + + # set layout + self._layout = QVBoxLayout() + self.setLayout(self._layout) + + # question title + self.label_question = QLabel() + self._layout.addWidget(self.label_question) + self.label_question.setText(translate.translate(title)) + self.label_question.setAlignment(Qt.AlignmentFlag.AlignCenter) + + font_title = self.label_question.font() + font_title.setPointSize(24) + font_title.setWeight(QFont.Weight.Bold) + self.label_question.setFont(font_title) + + # prepare navigation + self.navigation = widget.SurveyNavigation(signals=signals) + + # responses + self.frame_responses = QFrame() + self._layout.addWidget(self.frame_responses) + + self._layout_responses = QVBoxLayout() + self.frame_responses.setLayout(self._layout_responses) + + self.group_responses = QButtonGroup() + self.group_responses.setExclusive(self.are_buttons_exclusive()) + + # checking any button allow the user to go to the next step + self.group_responses.buttonClicked.connect(self.navigation.show_forward) # NOQA: connect and emit exists + + self.buttons_responses: dict[QAbstractButton, dict] = {} + button_choice_class = self.get_button_choice_class() + + for choice_id, choice_data in choices.items(): + # create a radio button for that choice + button = button_choice_class() + button.setText(translate.translate(choice_data["text"])) + + # add the button to the frame + self._layout_responses.addWidget(button) + + # add the button to the group + self.group_responses.addButton(button) + + # if the choice should ask the user for details + entry = None + if choice_data.get("ask_details", False): + entry = QLineEdit() + self._layout_responses.addWidget(entry) + entry.setEnabled(False) + + # toggling the button should also toggle the entry + button.toggled.connect(entry.setEnabled) # NOQA: connect exist + + # save the button and some data + self.buttons_responses[choice_id] = { + "button": button, + "entry": entry + } + + # add the navigation + self._layout.addWidget(self.navigation) + + @classmethod + def from_dict(cls, data: dict[str, Any], signals: dict[str, pyqtSignal]) -> "BaseChoiceQuestion": + return cls( + title=data["title"], + choices=data["choices"], + signals=signals, + ) + + def get_collected_data(self) -> dict: + collected_data = { + "choices": { + choice_id: { + "checked": choice_data["button"].isEnabled(), + "details": entry.text() if (entry := choice_data["entry"]) is not None else None + } + for choice_id, choice_data in self.buttons_responses.items() + } + } + + return collected_data diff --git a/source/survey/base/BaseSurvey.py b/source/survey/base/BaseSurvey.py index d8afafe..80df924 100644 --- a/source/survey/base/BaseSurvey.py +++ b/source/survey/base/BaseSurvey.py @@ -2,10 +2,10 @@ from abc import abstractmethod from typing import Optional, Any from PyQt6.QtCore import pyqtSignal -from PyQt6.QtWidgets import QFrame +from PyQt6.QtWidgets import QWidget -class BaseSurvey(QFrame): +class BaseSurvey(QWidget): """ A type of survey survey that can be in the user interface """ diff --git a/source/survey/base/__init__.py b/source/survey/base/__init__.py index 89e6393..4da7717 100644 --- a/source/survey/base/__init__.py +++ b/source/survey/base/__init__.py @@ -1 +1,2 @@ from .BaseSurvey import BaseSurvey +from .BaseChoiceQuestion import BaseChoiceQuestion diff --git a/source/widget/SurveyEngine.py b/source/widget/SurveyEngine.py index 72be649..70eb3fd 100644 --- a/source/widget/SurveyEngine.py +++ b/source/widget/SurveyEngine.py @@ -9,8 +9,9 @@ from typing import Optional import nextcord import requests from PyQt6.QtCore import pyqtSignal -from PyQt6.QtWidgets import QFrame, QVBoxLayout, QHBoxLayout, QPushButton, QProgressBar, QWidget +from PyQt6.QtWidgets import QVBoxLayout, QProgressBar, QWidget +from source import translate from source.survey.base import BaseSurvey from source.survey import Empty, survey_get @@ -33,51 +34,16 @@ class SurveyEngine(QWidget): self.signal_success.connect(self._on_signal_success) # NOQA: connect exist # prepare the survey collected data - self.collected_datas: dict[str, dict] = {"time": time.time(), "surveys": {}} + self.collected_datas: dict[str, dict] = { + "time": time.time(), # get the time of the start of the survey + "language": translate.get_language(), # get the user language + "surveys": {} # prepare the individual surveys data + } self.discord_webhook_result_url = discord_webhook_result_url self.current_survey_index = 0 - # set the layout - self._layout = QVBoxLayout() - self.setLayout(self._layout) - - # prepare the frame for the survey elements - self.frame_survey: BaseSurvey = Empty() - self._layout.addWidget(self.frame_survey) - - # navigations actions - self.frame_navigation = QFrame() - self._layout.addWidget(self.frame_navigation) - - self._layout_navigation = QHBoxLayout() - self.frame_navigation.setLayout(self._layout_navigation) - - self._layout_navigation.addStretch(0) # add a stretch to put the buttons on the right - - self.button_abandon = QPushButton() - self._layout_navigation.addWidget(self.button_abandon) - self.button_abandon.setText(self.tr("ABANDON")) - self.button_abandon.setStyleSheet("QPushButton { color : red; }") - self.button_abandon.clicked.connect(self.quit) # NOQA: connect exist - - self.button_skip = QPushButton() - self._layout_navigation.addWidget(self.button_skip) - self.button_skip.setText(self.tr("SKIP")) - self.button_skip.clicked.connect(self.next_survey) # NOQA: connect exist - - self.button_forward = QPushButton() - self._layout_navigation.addWidget(self.button_forward) - self.button_forward.setText(self.tr("NEXT")) - self.button_forward.clicked.connect(self.next_survey) # NOQA: connect exist - - # progress bar - self.progress = QProgressBar() - self._layout.addWidget(self.progress) - self.progress.setStyleSheet("QProgressBar::chunk { background-color: #03A9FC; }") - self.progress.setTextVisible(False) - self.progress.setFixedHeight(8) - # load the survey screens + # TODO: create dynamically self.survey_screens = [ ( survey_id, @@ -93,8 +59,21 @@ class SurveyEngine(QWidget): for survey_id, survey_data in surveys_data.items() ] - # update the progress bar - self.progress.setMaximum(len(self.survey_screens)) + # set the layout + self._layout = QVBoxLayout() + self.setLayout(self._layout) + + # prepare the frame for the survey elements + self.frame_survey: BaseSurvey = Empty() + self._layout.addWidget(self.frame_survey) + + # progress bar + self.progress = QProgressBar() + self._layout.addWidget(self.progress) + self.progress.setStyleSheet("QProgressBar::chunk { background-color: #03A9FC; }") + self.progress.setTextVisible(False) + self.progress.setFixedHeight(8) + self.progress.setMaximum(len(surveys_data)) # finalize the initialisation self.update_survey() @@ -116,16 +95,16 @@ class SurveyEngine(QWidget): # events def _on_signal_abandon(self): - # on success, show the button to give up - self.button_abandon.show() + # on abandon, quit the survey + self.quit() def _on_signal_skip(self): - # on success, show the button to skip - self.button_skip.show() + # on skip, skip to the next survey + self.next_survey() def _on_signal_success(self): - # on success, show the button to go forward - self.button_forward.show() + # on success, go to the next survey + self.next_survey() def next_survey(self): # get the collected data from the survey @@ -145,11 +124,6 @@ class SurveyEngine(QWidget): self.finish_survey() def update_survey(self): - # disable the buttons - self.button_abandon.hide() - self.button_skip.hide() - self.button_forward.hide() - # mark the actual survey as the old one old_frame_survey = self.frame_survey # call the old survey event diff --git a/source/widget/SurveyNavigation.py b/source/widget/SurveyNavigation.py new file mode 100644 index 0000000..8ce4430 --- /dev/null +++ b/source/widget/SurveyNavigation.py @@ -0,0 +1,77 @@ +from PyQt6.QtCore import pyqtSignal +from PyQt6.QtWidgets import QWidget, QHBoxLayout, QPushButton + + +class SurveyNavigation(QWidget): + def __init__(self, signals: dict[str, pyqtSignal] = None): + super().__init__() + + self._layout = QHBoxLayout() + self.setLayout(self._layout) + + # force the element to be on the right with a stretch + self._layout.addStretch() + + # abandon button + + self._button_abandon = QPushButton() + self._layout.addWidget(self._button_abandon) + self._button_abandon.setText(self.tr("ABANDON")) + self._button_abandon.setStyleSheet("QPushButton { color : red; }") + + if signals is not None and "abandon" in signals: + self._button_abandon.clicked.connect(signals["abandon"].emit) # NOQA: connect and emit exist + + # skip button + + self._button_skip = QPushButton() + self._layout.addWidget(self._button_skip) + self._button_skip.setText(self.tr("SKIP")) + + if signals is not None and "skip" in signals: + self._button_skip.clicked.connect(signals["skip"].emit) # NOQA: connect and emit exist + + # forward button + + self._button_forward = QPushButton() + self._layout.addWidget(self._button_forward) + self._button_forward.setText(self.tr("NEXT")) + + if signals is not None and "success" in signals: + self._button_forward.clicked.connect(signals["success"].emit) # NOQA: connect and emit exist + + # get all buttons + self._buttons = [ + self._button_abandon, + self._button_skip, + self._button_forward + ] + + # hide every button per default + self.hide_all() + + def show_abandon(self): + self._button_abandon.setVisible(True) + + def hide_abandon(self): + self._button_abandon.setVisible(False) + + def show_skip(self): + self._button_skip.setVisible(True) + + def hide_skip(self): + self._button_skip.setVisible(False) + + def show_forward(self): + self._button_forward.setVisible(True) + + def hide_forward(self): + self._button_forward.setVisible(False) + + def show_all(self): + for button in self._buttons: + button.setVisible(True) + + def hide_all(self): + for button in self._buttons: + button.setVisible(False) diff --git a/source/widget/__init__.py b/source/widget/__init__.py index e906706..b96e266 100644 --- a/source/widget/__init__.py +++ b/source/widget/__init__.py @@ -1,3 +1,4 @@ from .Browser import Browser from .SurveyEngine import SurveyEngine from .SurveyWindow import SurveyWindow +from .SurveyNavigation import SurveyNavigation diff --git a/surveys.json b/surveys.json index b721d36..919ac48 100644 --- a/surveys.json +++ b/surveys.json @@ -51,29 +51,39 @@ }, "choices": { "always": { - "en": "Always", - "fr": "Tout le temps", - "sp": "Siempre" + "text": { + "en": "Always", + "fr": "Tout le temps", + "sp": "Siempre" + } }, "often": { - "en": "Often", - "fr": "Souvent", - "sp": "A menudo" + "text": { + "en": "Often", + "fr": "Souvent", + "sp": "A menudo" + } }, "sometime": { - "en": "Sometimes", - "fr": "De temps en temps", - "sp": "De vez en cuando" + "text": { + "en": "Sometimes", + "fr": "De temps en temps", + "sp": "De vez en cuando" + } }, "rarely": { - "en": "Rarely", - "fr": "Rarement", - "sp": "Raramente" + "text": { + "en": "Rarely", + "fr": "Rarement", + "sp": "Raramente" + } }, "never": { - "en": "Never", - "fr": "Jamais", - "sp": "Nunca" + "text": { + "en": "Never", + "fr": "Jamais", + "sp": "Nunca" + } } } }, @@ -87,37 +97,48 @@ }, "choices": { "epic": { - "en": "Epic Games Store", - "fr": "Epic Games Store", - "sp": "Epic Games Store" + "text": { + "en": "Epic Games Store", + "fr": "Epic Games Store", + "sp": "Epic Games Store" + } }, "gog": { - "en": "GOG", - "fr": "GOG", - "sp": "GOG" + "text": { + "en": "GOG", + "fr": "GOG", + "sp": "GOG" + } }, "origin": { - "en": "Origin", - "fr": "Origin", - "sp": "Origin" + "text": { + "en": "Origin", + "fr": "Origin", + "sp": "Origin" + } }, "uplay": { - "en": "Uplay", - "fr": "Uplay", - "sp": "Uplay" + "text": { + "en": "Uplay", + "fr": "Uplay", + "sp": "Uplay" + } }, "battle": { - "en": "Battle.net", - "fr": "Battle.net", - "sp": "Battle.net" + "text": { + "en": "Battle.net", + "fr": "Battle.net", + "sp": "Battle.net" + } + }, + "other": { + "text": { + "en": "Other", + "fr": "Autre", + "sp": "Otro" + }, + "ask_details": true } - }, - "details_choice_enabled": true, - "details_choice_id": "other", - "details_choice_text": { - "en": "Other", - "fr": "Autre", - "sp": "Otro" } }, @@ -130,17 +151,20 @@ }, "choices": { "no": { - "en": "No", - "fr": "Non", - "sp": "No" + "text": { + "en": "No", + "fr": "Non", + "sp": "No" + } + }, + "yes": { + "text": { + "en": "Yes", + "fr": "Oui", + "sp": "Sí" + }, + "ask_details": true } - }, - "details_choice_enabled": true, - "details_choice_id": "yes", - "details_choice_text": { - "en": "Yes", - "fr": "Oui", - "sp": "Sí" } }, @@ -289,54 +313,74 @@ }, "choices": { "mission-language": { - "en": "Change the language", - "fr": "Changer la langue", - "sp": "Cambiar el idioma" + "text": { + "en": "Change the language", + "fr": "Changer la langue", + "sp": "Cambiar el idioma" + } }, "mission-price": { - "en": "Filter games by their price", - "fr": "Filtrer les jeux par leur prix", - "sp": "Filtrar juegos por su precio" + "text": { + "en": "Filter games by their price", + "fr": "Filtrer les jeux par leur prix", + "sp": "Filtrar juegos por su precio" + } }, "mission-community-hub": { - "en": "Visit the community hub", - "fr": "Se rendre sur le hub de la communauté", - "sp": "Ir al hub de la comunidad" + "text": { + "en": "Visit the community hub", + "fr": "Se rendre sur le hub de la communauté", + "sp": "Ir al hub de la comunidad" + } }, "mission-game-page": { - "en": "Visit a game page", - "fr": "Se rendre sur la page d'un jeu", - "sp": "Visitar la página de un juego" + "text": { + "en": "Visit a game page", + "fr": "Se rendre sur la page d'un jeu", + "sp": "Visitar la página de un juego" + } }, "mission-game-dlc": { - "en": "Visit the downloadable content (DLC) page of a game", - "fr": "Se rendre sur la page du contenu additionnel (DLC) d'un jeu", - "sp": "Visitar la página del contenido adicional descargable (DLC) de un juego" + "text": { + "en": "Visit the downloadable content (DLC) page of a game", + "fr": "Se rendre sur la page du contenu additionnel (DLC) d'un jeu", + "sp": "Visitar la página del contenido adicional descargable (DLC) de un juego" + } }, "mission-actuality-new": { - "en": "Visit the \"Featured\" news page", - "fr": "Se rendre sur la page des actualités \"À la une\"", - "sp": "Visitar la página de noticias \"Destacadas\"" + "text": { + "en": "Visit the \"Featured\" news page", + "fr": "Se rendre sur la page des actualités \"À la une\"", + "sp": "Visitar la página de noticias \"Destacadas\"" + } }, "mission-profile": { - "en": "Visit a user's profile", - "fr": "Se rendre sur le profil d'un utilisateur", - "sp": "Visitar el perfil de un usuario" + "text": { + "en": "Visit a user's profile", + "fr": "Se rendre sur le profil d'un utilisateur", + "sp": "Visitar el perfil de un usuario" + } }, "mission-game-discussion": { - "en": "Visit the discussion page of a game", - "fr": "Se rendre sur la page de discussion d'un jeu", - "sp": "Visitar la página de discusión de un juego" + "text": { + "en": "Visit the discussion page of a game", + "fr": "Se rendre sur la page de discussion d'un jeu", + "sp": "Visitar la página de discusión de un juego" + } }, "mission-gift-card": { - "en": "Visit the gift cards page", - "fr": "Se rendre sur la page des cartes cadeaux", - "sp": "Ir a la página de tarjetas de regalo" + "text": { + "en": "Visit the gift cards page", + "fr": "Se rendre sur la page des cartes cadeaux", + "sp": "Ir a la página de tarjetas de regalo" + } }, "mission-workshop": { - "en": "Visit the modification (mods) page of a game", - "fr": "Se rendre sur la page de la modification (mods) d'un jeu", - "sp": "Visitar la página de modificación (mods) de un juego" + "text": { + "en": "Visit the modification (mods) page of a game", + "fr": "Se rendre sur la page de la modification (mods) d'un jeu", + "sp": "Visitar la página de modificación (mods) de un juego" + } } } }, @@ -350,19 +394,25 @@ }, "choices": { "yes": { - "en": "Yes", - "fr": "Oui", - "sp": "Sí" + "text": { + "en": "Yes", + "fr": "Oui", + "sp": "Sí" + } }, "mixed": { - "en": "Partially", - "fr": "Mitigé", - "sp": "Parcialmente" + "text": { + "en": "Partially", + "fr": "Mitigé", + "sp": "Parcialmente" + } }, "no": { - "en": "No", - "fr": "Non", - "sp": "No" + "text": { + "en": "No", + "fr": "Non", + "sp": "No" + } } } }, @@ -393,9 +443,9 @@ "sp": "Agradecimientos" }, "description": { - "en": "We greatly appreciate your contribution to our survey and your time.\n\nYour collected data is located in the \"resultat\" folder.", - "fr": "Nous vous remercions grandement pour votre contribution à notre questionnaire et pour votre temps.\n\nVos données collectées sont situées dans le dossier \"resultat\".", - "sp": "Agradecemos enormemente su contribución a nuestro cuestionario y su tiempo.\n\nSus datos recopilados se encuentran en la carpeta \"resultat\"." + "en": "We greatly appreciate your contribution to our survey and your time.\n\nYour collected data is located in the \"results\" folder.", + "fr": "Nous vous remercions grandement pour votre contribution à notre questionnaire et pour votre temps.\n\nVos données collectées sont situées dans le dossier \"results\".", + "sp": "Agradecemos enormemente su contribución a nuestro cuestionario y su tiempo.\n\nSus datos recopilados se encuentran en la carpeta \"results\"." } } }