added Browser tracking and TextQuestion type
This commit is contained in:
parent
13246538fc
commit
a44e61b83e
12 changed files with 270 additions and 124 deletions
|
@ -1,6 +1,7 @@
|
|||
from typing import Any
|
||||
|
||||
from PyQt6.QtCore import Qt, pyqtSignal
|
||||
from PyQt6.QtGui import QFont
|
||||
from PyQt6.QtWidgets import QFrame, QVBoxLayout, QLabel, QRadioButton, QButtonGroup
|
||||
|
||||
from source.survey.base import BaseSurvey
|
||||
|
@ -20,6 +21,11 @@ class ChoiceQuestion(BaseSurvey):
|
|||
self.label_question.setText(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)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from typing import Any
|
||||
|
||||
from PyQt6.QtCore import pyqtSignal
|
||||
from survey.base import BaseSurvey
|
||||
from source.survey.base import BaseSurvey
|
||||
|
||||
|
||||
class Empty(BaseSurvey):
|
||||
|
|
|
@ -4,7 +4,7 @@ from PyQt6.QtCore import Qt, pyqtSignal
|
|||
from PyQt6.QtGui import QFont
|
||||
from PyQt6.QtWidgets import QVBoxLayout, QLabel
|
||||
|
||||
from survey.base import BaseSurvey
|
||||
from source.survey.base import BaseSurvey
|
||||
|
||||
|
||||
class Text(BaseSurvey):
|
||||
|
|
54
source/survey/TextQuestion.py
Normal file
54
source/survey/TextQuestion.py
Normal file
|
@ -0,0 +1,54 @@
|
|||
from typing import Any
|
||||
|
||||
from PyQt6.QtCore import Qt, pyqtSignal
|
||||
from PyQt6.QtGui import QFont
|
||||
from PyQt6.QtWidgets import QFrame, QVBoxLayout, QLabel, QRadioButton, QButtonGroup, QTextEdit
|
||||
|
||||
from source.survey.base import BaseSurvey
|
||||
|
||||
|
||||
class TextQuestion(BaseSurvey):
|
||||
def __init__(self, title: str, signals: dict[str, pyqtSignal]):
|
||||
super().__init__()
|
||||
|
||||
self.signals = signals
|
||||
|
||||
# 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(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)
|
||||
|
||||
# response
|
||||
self.entry_response = QTextEdit()
|
||||
self._layout.addWidget(self.entry_response)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any], signals: dict[str, pyqtSignal]) -> "TextQuestion":
|
||||
return cls(
|
||||
title=data["title"],
|
||||
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:
|
||||
return {
|
||||
"choice": self.entry_response.toPlainText()
|
||||
}
|
|
@ -1,12 +1,12 @@
|
|||
import time
|
||||
from typing import Optional, Any
|
||||
|
||||
from PyQt6.QtCore import Qt, QTimer, pyqtSignal, QUrl
|
||||
from PyQt6.QtGui import QFont
|
||||
from PyQt6.QtCore import Qt, QTimer, pyqtSignal, QUrl, QEvent, QObject
|
||||
from PyQt6.QtGui import QFont, QMouseEvent, QResizeEvent
|
||||
from PyQt6.QtWidgets import QLabel, QVBoxLayout, QSizePolicy
|
||||
|
||||
from survey.base import BaseSurvey
|
||||
from source.widget import DecoratedWebEngineView
|
||||
from source.survey.base import BaseSurvey
|
||||
from source.widget import Browser
|
||||
|
||||
|
||||
class WebMission(BaseSurvey):
|
||||
|
@ -15,15 +15,15 @@ class WebMission(BaseSurvey):
|
|||
|
||||
self.check_condition = check_condition
|
||||
self.default_url = url
|
||||
self.signals = signals # TODO: default None ?
|
||||
self.signals = signals if signals is not None else {}
|
||||
|
||||
# set layout
|
||||
self._layout = QVBoxLayout()
|
||||
self.setLayout(self._layout)
|
||||
|
||||
# data collection
|
||||
self.initial_time = time.time()
|
||||
self.collect_urls: list[tuple[float, str]] = [] # list of urls that the user went by
|
||||
self.start_time = time.time()
|
||||
self._collected_events: list[dict[str, Any]] = []
|
||||
|
||||
# mission title
|
||||
self.label_title = QLabel()
|
||||
|
@ -37,11 +37,12 @@ class WebMission(BaseSurvey):
|
|||
self.label_title.setFont(font_title)
|
||||
|
||||
# web page
|
||||
self.web_view = DecoratedWebEngineView()
|
||||
self._layout.addWidget(self.web_view)
|
||||
self.web_view.urlChanged.connect(self._on_url_changed) # NOQA: connect exist
|
||||
self.browser = 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
|
||||
|
||||
self.web_view.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||||
self.browser.web.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||||
|
||||
# setup the timer for the check
|
||||
if self.check_condition is not None:
|
||||
|
@ -59,31 +60,92 @@ class WebMission(BaseSurvey):
|
|||
signals=signals
|
||||
)
|
||||
|
||||
# events
|
||||
|
||||
def eventFilter(self, obj: QObject, event: QEvent) -> bool:
|
||||
if obj is self.browser.web.focusProxy():
|
||||
# if the object is the content of the web engine widget
|
||||
match event.type():
|
||||
case QEvent.Type.MouseMove:
|
||||
# if this is a mouse movement
|
||||
event: QMouseEvent
|
||||
position = event.position()
|
||||
|
||||
self._save_event(
|
||||
type="mouse_move",
|
||||
position=(position.x(), position.y())
|
||||
)
|
||||
|
||||
case QEvent.Type.MouseButtonPress:
|
||||
# if this is a mouse click press
|
||||
event: QMouseEvent
|
||||
position = event.position()
|
||||
|
||||
self._save_event(
|
||||
type="mouse_press",
|
||||
position=(position.x(), position.y()),
|
||||
button=event.button(),
|
||||
)
|
||||
|
||||
case QEvent.Type.MouseButtonRelease:
|
||||
# if this is a mouse click release
|
||||
event: QMouseEvent
|
||||
position = event.position()
|
||||
|
||||
self._save_event(
|
||||
type="mouse_release",
|
||||
position=(position.x(), position.y()),
|
||||
button=event.button(),
|
||||
)
|
||||
|
||||
case QEvent.Type.MouseButtonDblClick:
|
||||
# if this is a mouse double click
|
||||
event: QMouseEvent
|
||||
position = event.position()
|
||||
|
||||
self._save_event(
|
||||
type="mouse_double_click",
|
||||
position=(position.x(), position.y()),
|
||||
)
|
||||
|
||||
case QEvent.Type.Resize:
|
||||
# if the window got resized
|
||||
event: QResizeEvent
|
||||
size = event.size()
|
||||
|
||||
self._save_event(
|
||||
type="resize",
|
||||
size=(size.width(), size.height()),
|
||||
)
|
||||
|
||||
return super().eventFilter(obj, event)
|
||||
|
||||
def on_show(self) -> None:
|
||||
self.web_view.setUrl(QUrl(self.default_url))
|
||||
# initialize the start time
|
||||
self.start_time = time.time()
|
||||
|
||||
# set the web view to the default url
|
||||
self.browser.web.setUrl(QUrl(self.default_url))
|
||||
|
||||
if self.check_condition is not None:
|
||||
# enable the timer
|
||||
self.timer_check.start()
|
||||
|
||||
else:
|
||||
# call directly the success signal
|
||||
if "success" in self.signals:
|
||||
self.signals["success"].emit() # NOQA: emit exist
|
||||
self._success() # call directly the success method
|
||||
|
||||
def on_hide(self) -> None:
|
||||
self.timer_check.stop()
|
||||
|
||||
# data collection
|
||||
def _success(self):
|
||||
# TODO: animation or notification to clearly mark mission as succeeded
|
||||
|
||||
def get_collected_data(self) -> dict:
|
||||
# TODO: more data to collect
|
||||
return {
|
||||
"collect_urls": self.collect_urls
|
||||
}
|
||||
# mark the success in the events
|
||||
self._save_event(type="check")
|
||||
|
||||
def _on_url_changed(self):
|
||||
self.collect_urls.append((time.time() - self.initial_time, self.web_view.url()))
|
||||
# emit on the success signal
|
||||
if "success" in self.signals:
|
||||
self.signals["success"].emit() # NOQA: emit exist
|
||||
|
||||
# condition
|
||||
|
||||
|
@ -93,8 +155,27 @@ class WebMission(BaseSurvey):
|
|||
"""
|
||||
|
||||
def check_callback(result: bool):
|
||||
if result and "success" in self.signals:
|
||||
self.signals["success"].emit() # NOQA: emit exist
|
||||
if result:
|
||||
self._success()
|
||||
|
||||
page = self.web_view.page()
|
||||
page = self.browser.web.page()
|
||||
page.runJavaScript(self.check_condition, resultCallback=check_callback)
|
||||
|
||||
# data collection
|
||||
|
||||
def _save_event(self, **data) -> None:
|
||||
# save the data of the event and add the current time
|
||||
data["time"] = round(time.time() - self.start_time, 3)
|
||||
self._collected_events.append(data)
|
||||
|
||||
def get_collected_data(self) -> dict:
|
||||
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()
|
||||
)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from .Empty import Empty
|
||||
from .Text import Text
|
||||
from .ChoiceQuestion import ChoiceQuestion
|
||||
from .TextQuestion import TextQuestion
|
||||
from .WebMission import WebMission
|
||||
|
||||
from .get import survey_get
|
||||
|
|
|
@ -2,14 +2,18 @@ from typing import Type
|
|||
|
||||
from PyQt6.QtCore import pyqtSignal
|
||||
|
||||
from . import Text, ChoiceQuestion, WebMission, Empty
|
||||
from . import Text, ChoiceQuestion, WebMission, Empty, TextQuestion
|
||||
from .base import BaseSurvey
|
||||
|
||||
|
||||
all_survey: dict[str, Type[BaseSurvey]] = {
|
||||
# base
|
||||
"empty": Empty,
|
||||
"text": Text,
|
||||
# questions
|
||||
"question-choice": ChoiceQuestion,
|
||||
"question-text": TextQuestion,
|
||||
# missions
|
||||
"mission-web": WebMission,
|
||||
}
|
||||
|
||||
|
|
88
source/widget/Browser.py
Normal file
88
source/widget/Browser.py
Normal file
|
@ -0,0 +1,88 @@
|
|||
from typing import Optional
|
||||
|
||||
from PyQt6.QtCore import QUrl, QObject, QEvent
|
||||
from PyQt6.QtGui import QMouseEvent
|
||||
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
||||
from PyQt6.QtWidgets import QVBoxLayout, QWidget, QProgressBar, QStyle, QToolBar
|
||||
|
||||
|
||||
class Browser(QWidget):
|
||||
"""
|
||||
A version of the QWebEngineView class with integrated progress bar and navigations bar.
|
||||
"""
|
||||
|
||||
def __init__(self, url: Optional[QUrl] = None):
|
||||
super().__init__()
|
||||
|
||||
# layout
|
||||
self._layout = QVBoxLayout()
|
||||
self.setLayout(self._layout)
|
||||
|
||||
# navigation bar
|
||||
self.navigation = QToolBar()
|
||||
self._layout.addWidget(self.navigation, 0)
|
||||
|
||||
style = self.style()
|
||||
|
||||
self._action_back = self.navigation.addAction(
|
||||
style.standardIcon(QStyle.StandardPixmap.SP_ArrowBack),
|
||||
"Back"
|
||||
)
|
||||
self._action_reload = self.navigation.addAction(
|
||||
style.standardIcon(QStyle.StandardPixmap.SP_BrowserReload),
|
||||
"Reload"
|
||||
)
|
||||
self._action_forward = self.navigation.addAction(
|
||||
style.standardIcon(QStyle.StandardPixmap.SP_ArrowForward),
|
||||
"Forward"
|
||||
)
|
||||
|
||||
# web widget
|
||||
self.web = QWebEngineView()
|
||||
self._layout.addWidget(self.web, 1)
|
||||
|
||||
if url is not None:
|
||||
self.web.load(QUrl(url))
|
||||
|
||||
# loading bar
|
||||
self.progress = QProgressBar()
|
||||
self._layout.addWidget(self.progress, 0)
|
||||
self.progress.setFixedHeight(6)
|
||||
self.progress.setTextVisible(False)
|
||||
self.progress.setMaximum(100)
|
||||
self.progress.hide()
|
||||
|
||||
# connect the signals
|
||||
self.web.loadStarted.connect(self._load_started) # NOQA: connect exist
|
||||
self.web.loadProgress.connect(self._load_progress) # NOQA: connect exist
|
||||
self.web.loadFinished.connect(self._load_finished) # NOQA: connect exist
|
||||
|
||||
self._action_back.triggered.connect(self.web.back) # NOQA: connect exist
|
||||
self._action_reload.triggered.connect(self.web.reload) # NOQA: connect exist
|
||||
self._action_forward.triggered.connect(self.web.forward) # NOQA: connect exist
|
||||
|
||||
# finalize the initialisation
|
||||
self.refresh_navigation_actions()
|
||||
|
||||
# graphical methods
|
||||
|
||||
def _load_started(self):
|
||||
# update the progress bar
|
||||
self.progress.setValue(0)
|
||||
self.progress.show()
|
||||
|
||||
def _load_progress(self, value: int):
|
||||
# update the progress bar
|
||||
self.progress.setValue(value)
|
||||
|
||||
def _load_finished(self):
|
||||
# update the progress bar
|
||||
self.progress.hide()
|
||||
# refresh the navigation buttons
|
||||
self.refresh_navigation_actions()
|
||||
|
||||
def refresh_navigation_actions(self):
|
||||
history = self.web.history()
|
||||
# enable the navigation button depending on the history
|
||||
self._action_back.setEnabled(history.canGoBack())
|
||||
self._action_forward.setEnabled(history.canGoForward())
|
|
@ -1,93 +0,0 @@
|
|||
from typing import Optional
|
||||
|
||||
from PyQt6.QtCore import QUrl
|
||||
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
||||
from PyQt6.QtWidgets import QVBoxLayout, QWidget, QProgressBar, QFrame, QHBoxLayout, QPushButton, QStyle
|
||||
|
||||
|
||||
class DecoratedWebEngineView(QWidget):
|
||||
"""
|
||||
A version of the QWebEngineView class with integrated progress bar and navigations bar.
|
||||
"""
|
||||
|
||||
def __init__(self, url: Optional[QUrl] = None):
|
||||
super().__init__()
|
||||
|
||||
# layout
|
||||
self._layout = QVBoxLayout()
|
||||
self.setLayout(self._layout)
|
||||
|
||||
# navigation bar
|
||||
self.frame_navigation = QFrame()
|
||||
self._layout.addWidget(self.frame_navigation, 0)
|
||||
self._layout_navigation = QHBoxLayout()
|
||||
self.frame_navigation.setLayout(self._layout_navigation)
|
||||
|
||||
self.button_back = QPushButton()
|
||||
self._layout_navigation.addWidget(self.button_back)
|
||||
self.button_back.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_ArrowBack))
|
||||
|
||||
self.button_reload = QPushButton()
|
||||
self._layout_navigation.addWidget(self.button_reload)
|
||||
self.button_reload.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_BrowserReload))
|
||||
|
||||
self.button_forward = QPushButton()
|
||||
self._layout_navigation.addWidget(self.button_forward)
|
||||
self.button_forward.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_ArrowForward))
|
||||
|
||||
# force the navigation buttons to be on the left by adding a stretching element
|
||||
self._layout_navigation.addStretch(0)
|
||||
|
||||
# web widget
|
||||
self.web_view = QWebEngineView()
|
||||
self._layout.addWidget(self.web_view, 1)
|
||||
|
||||
if url is not None:
|
||||
self.web_view.load(QUrl(url))
|
||||
|
||||
# loading bar
|
||||
self.progress_bar = QProgressBar()
|
||||
self._layout.addWidget(self.progress_bar, 0)
|
||||
self.progress_bar.setFixedHeight(6)
|
||||
self.progress_bar.setTextVisible(False)
|
||||
self.progress_bar.setMaximum(100)
|
||||
self.progress_bar.hide()
|
||||
|
||||
# connect the signals
|
||||
self.web_view.loadStarted.connect(self._load_started) # NOQA: connect exist
|
||||
self.web_view.loadProgress.connect(self._load_progress) # NOQA: connect exist
|
||||
self.web_view.loadFinished.connect(self._load_finished) # NOQA: connect exist
|
||||
|
||||
self.button_back.clicked.connect(self.back) # NOQA: connect exist
|
||||
self.button_reload.clicked.connect(self.reload) # NOQA: connect exist
|
||||
self.button_forward.clicked.connect(self.forward) # NOQA: connect exist
|
||||
|
||||
# finalize the initialisation
|
||||
self.refresh_navigation_actions()
|
||||
|
||||
def __getattr__(self, name):
|
||||
# if the member is not found in the class, look in the web view directly
|
||||
return getattr(self.web_view, name)
|
||||
|
||||
# graphical methods
|
||||
|
||||
def _load_started(self):
|
||||
# update the progress bar
|
||||
self.progress_bar.setValue(0)
|
||||
self.progress_bar.show()
|
||||
|
||||
def _load_progress(self, value: int):
|
||||
# update the progress bar
|
||||
self.progress_bar.setValue(value)
|
||||
|
||||
def _load_finished(self):
|
||||
# update the progress bar
|
||||
self.progress_bar.hide()
|
||||
# refresh the navigation buttons
|
||||
self.refresh_navigation_actions()
|
||||
|
||||
def refresh_navigation_actions(self):
|
||||
history = self.web_view.history()
|
||||
# enable the navigation button depending on the history
|
||||
self.button_back.setEnabled(history.canGoBack())
|
||||
self.button_forward.setEnabled(history.canGoForward())
|
|
@ -58,7 +58,7 @@ class FrameSurvey(QFrame):
|
|||
|
||||
def load_file(self, survey_path: Path | str):
|
||||
# load the surveys screens
|
||||
with open(survey_path) as file:
|
||||
with open(survey_path, encoding="utf-8") as file:
|
||||
surveys_data = json.load(file)
|
||||
|
||||
self.survey_screens = [
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
from .DecoratedWebEngineView import DecoratedWebEngineView
|
||||
from .Browser import Browser
|
||||
from .FrameSurvey import FrameSurvey
|
||||
from .MyMainWindow import MyMainWindow
|
||||
|
|
|
@ -29,5 +29,10 @@
|
|||
"title": "Rendez-vous sur la boutique des points.",
|
||||
"url": "https://steampowered.com/",
|
||||
"check": "true"
|
||||
},
|
||||
|
||||
"question-experience": {
|
||||
"type": "question-text",
|
||||
"title": "Qu'avez vous pensé de l'interface de Steam ?"
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue