From ce7edbb5a02a82bd39aa09178736ceb951801f12 Mon Sep 17 00:00:00 2001 From: DanielDu Date: Tue, 28 Nov 2023 23:35:49 +0800 Subject: [PATCH] add the information screen --- component/button.py | 67 ------- component/modal.py | 59 +++--- component/screen.py | 424 ++++++++++++++++++++++++++++++++------------ main.py | 52 ++---- mouseHandler.py | 20 --- 5 files changed, 351 insertions(+), 271 deletions(-) delete mode 100644 mouseHandler.py diff --git a/component/button.py b/component/button.py index a954a57..e69de29 100644 --- a/component/button.py +++ b/component/button.py @@ -1,67 +0,0 @@ -from component.text import TextElement -from psychopy.visual.rect import Rect -import psychopy.visual.line - - -class ButtonElement: - def __init__(self, win, text, pos, width, height, color, action=None): - self.button = Rect(win, width=width, height=height, fillColor=color, pos=pos) - self.text = TextElement(win=win,text=text,pos=pos) - - self.action = action - self.states = { - 'unpress': { - 'button_color': color, - 'text_color': 'white' - }, - 'press': { - 'button_color': 'orange', - 'text_color': 'black' - } - } - self.state = 'unpress' # Initial state is 'unpress' - - def draw(self): - self.button.fillColor = self.states[self.state]['button_color'] - self.text.set_color(self.states[self.state]['text_color']) - self.button.draw() - self.text.draw() - - def containMouse(self,mouse): - return self.button.contains(mouse) - - -class ButtonList: - def __init__(self, win, textList, pos, width, height, color, action=None,rows=0,columns=0,horizontal_spacing=0,vertical_spacing=0): - self.pos = pos - positions = [] - for row in range(rows): - for col in range(columns): - x = col * (width + horizontal_spacing) - (columns - 1) * (width + horizontal_spacing) / 2 - y = (rows - 1 - row) * (height + vertical_spacing) - (rows - 1) * (height + vertical_spacing) / 2 - positions.append((x, y)) - - self.button_elements = [] - for i, text in enumerate(textList): - button_pos = (positions[i][0] + self.pos[0], positions[i][1] + self.pos[1]) # Use positions[i] here - self.button_elements.append(ButtonElement(win, text, pos=button_pos, width=width, height=height, color=color, action=action)) - - def draw(self): - for element in self.button_elements: - element.draw() - - def containMouse(self, mouse): - return any(element.containMouse(mouse) for element in self.button_elements) - - - def action(self,mouse): - for element_id,element in enumerate(self.button_elements): - if element.containMouse(mouse): - return element.action(element_id) - - def updateState(self,buttonId): - for element_id,element in enumerate(self.button_elements): - element.state = 'unpress' - if element_id==buttonId: - element.state = 'press' - diff --git a/component/modal.py b/component/modal.py index 2a4ee6e..d36c357 100644 --- a/component/modal.py +++ b/component/modal.py @@ -1,32 +1,27 @@ -from component.text import TextElement -from component.button import ButtonElement -import psychopy.visual.line -from psychopy.visual.rect import Rect -from config.constant import color_dict - -class ConfirmationModal: - def __init__(self, win, text,confirm_action=None,cancel_action=None): - self.win = win - self.text = TextElement(win, text, pos=(0, 0), color=color_dict["white"],) - self.confirm_button = ButtonElement(win, "Confirm", pos=(-0.3, -0.5), width=0.3, height=0.2, color="green", action=confirm_action) - self.cancel_button = ButtonElement(win, "Cancel", pos=(0.3, -0.5), width=0.3, height=0.2, color="red", action=cancel_action) - self.background_rect = Rect(win, width=1.2, height=0.9, fillColor=(0, 0, 0, 1.0), pos=(0, -0.2)) - self.buttons = [self.confirm_button, self.cancel_button] - self.visible = False - - def draw(self): - if self.visible: - self.background_rect.draw() - self.text.draw() - for element in self.buttons: - element.draw() - - def containMouse(self, mouse): - if(self.visible)==False: - return False - return self.confirm_button.containMouse(mouse) or self.cancel_button.containMouse(mouse) - - def action(self, mouse): - for button_element in self.buttons: - if button_element.containMouse(mouse): - return button_element.action(mouse) \ No newline at end of file +from PyQt6.QtWidgets import QApplication, QWidget, QPushButton, QVBoxLayout, QDialog, QHBoxLayout, QLabel + +class ConfirmDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.initUI() + + def initUI(self): + self.setWindowTitle("Confirm") + + layout = QHBoxLayout() + + confirm_btn = QPushButton("Confirm", self) + confirm_btn.clicked.connect(self.confirm) + layout.addWidget(confirm_btn) + + cancel_btn = QPushButton("Cancel", self) + cancel_btn.clicked.connect(self.cancel) + layout.addWidget(cancel_btn) + + self.setLayout(layout) + + def confirm(self): + self.accept() # Closes the dialog and returns QDialog.Accepted + + def cancel(self): + self.reject() # Closes the dialog and returns QDialog.Rejected \ No newline at end of file diff --git a/component/screen.py b/component/screen.py index 8b81209..f73b81c 100644 --- a/component/screen.py +++ b/component/screen.py @@ -1,17 +1,17 @@ -from component.button import ButtonElement -from component.modal import ConfirmationModal +from component.modal import ConfirmDialog from component.model import Answer -from component.text import TextElement -from component.button import ButtonElement,ButtonList import os from pydub import AudioSegment from pydub.playback import play -from config.constant import color_dict from config.constant import pofomopo_consonants,similarity_list from config.dir import audio_dir from storage.localStorage import dataHandaler import pandas as pd import random +from PyQt6.QtWidgets import QWidget, QLabel, QLineEdit, QPushButton, QVBoxLayout,QDialog,QProgressBar,QGridLayout,QSizePolicy,QHBoxLayout,QSpacerItem +from PyQt6.QtCore import Qt + +import logging class BaseScreen: def __init__(self, win): @@ -29,20 +29,50 @@ def remove_element(self, element): def draw(self): for element in self.elements: element.draw() + -class EndScreen(BaseScreen): - def __init__(self, win,result): - super().__init__(win) +class EndScreen(QWidget,): + def __init__(self,screen_height,screen_width, result): + super().__init__() + + self.screen_height = screen_height + self.screen_width = screen_width + self.resize(screen_height,screen_width) + + self.result = result + self.initUI() self.saveResult(result) - self.add_element(TextElement(win, "Thank you for participating!", pos=(0, 0),color=color_dict["black"])) - self.add_element(ButtonElement(win, "Replay", pos=(0, -0.2), width=0.3, height=0.2, color="green", action=self.replay)) - self.add_element(ButtonElement(win, "Quit", pos=(0, -0.5), width=0.3, height=0.2, color="red", action=self.quit)) - def replay(self,mouse): - return StartScreen(self.win) + def initUI(self): + layout = QVBoxLayout(self) + + # Thank you message + thank_you_label = QLabel("Thank you for participating!", self) + layout.addWidget(thank_you_label) + + # Replay button + replay_btn = QPushButton("Replay", self) + replay_btn.clicked.connect(self.navigate_start_screen) + layout.addWidget(replay_btn) + + # Quit button + quit_btn = QPushButton("Quit", self) + quit_btn.clicked.connect(self.quit) + layout.addWidget(quit_btn) + + self.setLayout(layout) + + + def navigate_start_screen(self): + logging.info("Open the StartScreen") + self.next_screen = StartScreen(self.screen_height,self.screen_width) + self.next_screen.show() + self.close() + + def quit(self): + # Logic to quit the experiment + self.close() # Closes the EndScreen window - def quit(self,mouse): - return None # This will end the experiment def saveResult(self,history): user_df = dataHandaler.get_user() user_df = user_df.drop(user_df.index) @@ -53,83 +83,152 @@ def saveResult(self,history): dataHandaler.append_history_data(new_history_value) dataHandaler.append_user_data(user_df) +class StartScreen(QWidget): + def __init__(self,screen_height,screen_width): + super().__init__() + self.screen_height = screen_height + self.screen_width = screen_width + self.resize(screen_height,screen_width) + logging.info("Initializing StartScreen") + self.initUI() -# StartScreen -class StartScreen(BaseScreen): - def __init__(self, win): - super().__init__(win) - self.add_element(TextElement(win, "Quizz Experiment", pos=(0, 0),color=color_dict["black"])) - self.add_element(ButtonElement(win, "Start", pos=(0, -0.2), width=0.3, height=0.2, color="green", action=self.navigate_start_screen)) - self.add_element(ButtonElement(win, "Setting", pos=(0, -0.5), width=0.3, height=0.2, color="blue", action=self.navigate_setting_screen)) + def initUI(self): + logging.info("Setting up UI for StartScreen") + + layout = QVBoxLayout() - def navigate_setting_screen(self,mouse): - return SettingScreen(self.win) - - def navigate_start_screen(self,mouse): - return TestScreen(self.win) + # Add stretch to center the widgets vertically + layout.addStretch() + + # Title + title = QLabel("Quiz Experiment", self) + title.setAlignment(Qt.AlignmentFlag.AlignCenter) + title.setStyleSheet("font-size: 24px; font-weight: bold;") # Increase font size and make it bold + layout.addWidget(title) + + # Start button + start_btn = QPushButton("Start", self) + start_btn.clicked.connect(self.navigate_UserInformationScreen_screen) + start_btn.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + start_btn.setStyleSheet("background-color: green; color: white; font-size: 20px; padding: 10px;") + layout.addWidget(start_btn) + + # Settings button + settings_btn = QPushButton("Settings", self) + settings_btn.clicked.connect(self.navigate_setting_screen) + settings_btn.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + settings_btn.setStyleSheet("background-color: gray; color: white; font-size: 20px; padding: 10px;") + layout.addWidget(settings_btn) + + # Add another stretch to ensure buttons are centered + layout.addStretch() + + self.setLayout(layout) + self.setWindowTitle('Start Screen') + + def navigate_setting_screen(self): + logging.info(" Open the settings screen") + self.next_screen = SettingScreen(self.screen_height,self.screen_width) + self.next_screen.show() + self.close() + + def navigate_UserInformationScreen_screen(self): + logging.info("Open the user information screen") + self.user_info_screen = UserInformationScreen(self.screen_height,self.screen_width) + self.user_info_screen.show() + self.close() # TestScreen -class TestScreen(BaseScreen): - def __init__(self, win): - super().__init__(win) - self.answerList = self.loadQuestion(audio_dir) +class TestScreen(QWidget): + def __init__(self, screen_height, screen_width): + super().__init__() + self.screen_height = screen_height + self.screen_width = screen_width + self.resize(screen_width, screen_height) + self.setWindowTitle("Test Screen") + logging.info("Initializing TestScreen") + self.initUI() + self.answerList = self.loadQuestion(audio_dir) self.current_index = 0 - # List of BofoMo consonants - self.playSoundButton = ButtonElement(win, "Play Sound", pos=(-0.4, 0.5), width=0.4, height=0.2, color="blue", action=self.play_sound) - self.previousButton = ButtonElement(win, "Previous", pos=(0.55, -0.8), width=0.3, height=0.2, color="blue", action=self.previous_question) - self.nextButton = ButtonElement(win, "Next", pos=(0.85, -0.8), width=0.2, height=0.2, color="blue", action=self.next_question) - self.submitButton = ButtonElement(win, "Submit", pos=(0.7, -0.5), width=0.3, height=0.2, color="blue", action= self.submit_test) - self.progress = TextElement(win, "Test Number:{}/{}".format(self.current_index+1,len(self.answerList)), pos=(0.7, 0.9),color=color_dict["black"]) - self.bofomo_consonants_list = ButtonList( - win=win, - textList=pofomopo_consonants, - pos=(-0.32, -0.45), # Grid position - width=0.25, # Button width - height=0.2, # Button height - color="blue", # Button color - action=self.bofomo_consonant_action, # Action to perform when a button is clicked - rows=5, # Number of rows - columns=5, # Number of columns - horizontal_spacing=0.02, # Horizontal spacing between buttons - vertical_spacing=0.02 # Vertical spacing between buttons - ) - self.similarities_list = ButtonList( - win=win, - textList=similarity_list, - pos=(0.8, 0.2), # Grid position - width=0.1, # Button width - height=0.15, # Button height - color="blue", # Button color - action=self.similarity_action, # Action to perform when a button is clicked - rows=5, # Number of rows - columns=1, # Number of columns - horizontal_spacing=0.01, # Horizontal spacing between buttons - vertical_spacing=0.01 # Vertical spacing between buttons - ) - self.add_element(self.playSoundButton) - self.add_element(self.submitButton) - self.add_element(self.progress) - self.add_element(self.bofomo_consonants_list) - self.add_element(self.similarities_list) + self.updateProcess() self.update_button_states() + self.answerList = self.loadQuestion(audio_dir) + self.current_index = 0 + + def initUI(self): + logging.info("Setting up UI for TestScreen") - def next_question(self, mouse): + layout = QVBoxLayout(self) + + self.progress_label = QLabel("Test Number: 0/0") + layout.addWidget(self.progress_label) + + self.playSoundButton = QPushButton("Play Sound") + self.playSoundButton.clicked.connect(self.play_sound) + layout.addWidget(self.playSoundButton) + + self.progress_bar = QProgressBar(self) + + button_layout = QHBoxLayout() + + # Spacer to push buttons to the right + spacer = QSpacerItem(20, 20, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum) + button_layout.addItem(spacer) + + # Previous Button + self.previousButton = QPushButton("Previous") + self.previousButton.clicked.connect(self.previous_question) + button_layout.addWidget(self.previousButton) + + # Next Button + self.nextButton = QPushButton("Next") + self.nextButton.clicked.connect(self.next_question) + button_layout.addWidget(self.nextButton) + + # Submit Button + submitButton = QPushButton("Submit") + submitButton.clicked.connect(self.handling_submit_button) + button_layout.addWidget(submitButton) + + # Add the horizontal layout to the main layout + + layout.addLayout(button_layout) + self.setLayout(layout) + self.setWindowTitle('TestScreen') + + + # Create grid layout for bofomo consonants + self.bofomo_consonants_grid = QGridLayout() + for i, text in enumerate(pofomopo_consonants): + button = QPushButton(str(text), self) + button.clicked.connect(lambda checked, text=text: self.bofomo_consonant_action(text)) + row, col = divmod(i, 5) + self.bofomo_consonants_grid.addWidget(button, row, col) + layout.addLayout(self.bofomo_consonants_grid) + + # Create grid layout for similarities + self.similarities_grid = QGridLayout() + for i, text in enumerate(similarity_list): + button = QPushButton(str(text), self) + button.clicked.connect(lambda checked, text=text: self.similarity_action(text)) + row, col = divmod(i, 1) + self.similarities_grid.addWidget(button, row, col) + layout.addLayout(self.similarities_grid) + + def next_question(self): self.current_index += 1 self.update_button_states() self.updateProcess() - return self - def previous_question(self, mouse): + def previous_question(self): self.current_index -= 1 self.update_button_states() self.updateProcess() - return self - def play_sound(self, mouse): + def play_sound(self): current_question = self.answerList[self.current_index] play(current_question.get_question()) - return self def loadQuestion(self, audio_dir): @@ -145,67 +244,160 @@ def loadQuestion(self, audio_dir): return quetionList def update_button_states(self): - # Disable "Next" button when at the end - if self.current_index == len(self.answerList) - 1: - self.add_element(self.previousButton) - self.remove_element(self.nextButton) - elif self.current_index == 0: - self.add_element(self.nextButton) - self.remove_element(self.previousButton) - else: - self.add_element(self.nextButton) - self.add_element(self.previousButton) + + is_first_question = self.current_index == 0 + is_last_question = self.current_index == len(self.answerList) - 1 + + self.previousButton.setVisible(not is_first_question) + self.nextButton.setVisible(not is_last_question) current_question = self.answerList[self.current_index] - self.bofomo_consonants_list.updateState(current_question.get_answer()) - self.similarities_list.updateState(current_question.get_similarity()) - def submit_test(self, mouse): + def handling_submit_button(self): + self.submit_test() + self.navigate_end_screen() + + def submit_test(self): for question in self.answerList: question.set_id(question.get_id()+1) question.set_answer(pofomopo_consonants[question.get_answer()] if question.get_answer()!= -1 else -1) question.set_similarity(similarity_list[question.get_similarity()] if question.get_similarity()!=-1 else -1) - return EndScreen(self.win,self.answerList) + + + def navigate_end_screen(self): + logging.info("Open the EndScreen") + self.next_screen = EndScreen(self.screen_height,self.screen_width,self.answerList) + self.next_screen.show() + self.close() def updateProcess(self): - self.progress.set_text("Test Number:{}/{}".format(self.current_index+1,len(self.answerList))) + # Update the progress label + self.progress_label.setText(f"Test Number: {self.current_index + 1}/{len(self.answerList)}") + + # Update the progress bar + if len(self.answerList) > 0: + progress_value = int((self.current_index + 1) / len(self.answerList) * 100) + self.progress_bar.setValue(progress_value) + else: + self.progress_bar.setValue(0) + # self.progress.set_text("Test Number:{}/{}".format(self.current_index+1,len(self.answerList))) # Action for BofoMo consonants def bofomo_consonant_action(self, button_index): current_question = self.answerList[self.current_index] current_question.set_answer(button_index) - self.bofomo_consonants_list.updateState(button_index) - return self + # self.bofomo_consonants_list.updateState(button_index) # Action for similarities def similarity_action(self, button_index): current_question = self.answerList[self.current_index] current_question.set_similarity(button_index) - self.similarities_list.updateState(button_index) - return self + # self.similarities_list.updateState(button_index) -# Create the SettingScreen class with the Reset button and modal logic -class SettingScreen(BaseScreen): - def __init__(self, win): - super().__init__(win) - self.add_element(TextElement(win, "Settings", pos=(0, 0), color=color_dict["black"])) - self.add_element(ButtonElement(win, "Reset", pos=(0, -0.2), width=0.3, height=0.2, color="red",action=self.show_confirmation_modal)) - self.add_element(ButtonElement(win, "Back", pos=(0.7, -0.8), width=0.3, height=0.2, color="grey",action=self.navigate_main_screen)) - self.confirmation_modal = ConfirmationModal(win, "Are you sure you want to reset?",confirm_action=self.confirm,cancel_action=self.cancel) - self.add_element(self.confirmation_modal) - - def show_confirmation_modal(self, mouse): - self.confirmation_modal.visible = True - return self +class SettingScreen(QWidget): + def __init__(self,screen_height,screen_width): + super().__init__() + logging.info("Initializing SettingScreen") + self.screen_height = screen_height + self.screen_width = screen_width + self.resize(screen_height,screen_width) + self.initUI() + + def initUI(self): + logging.info("Setting up UI for SettingScreen") + layout = QVBoxLayout(self) + + layout.addWidget(QLabel("Settings")) + + reset_btn = QPushButton("Reset", self) + reset_btn.clicked.connect(self.show_confirm_dialog) + layout.addWidget(reset_btn) + + back_btn = QPushButton("Back", self) + back_btn.clicked.connect(self.navigate_main_screen) + layout.addWidget(back_btn) + + self.setLayout(layout) + + def show_confirm_dialog(self): + dialog = ConfirmDialog(self) + result = dialog.exec() + + if result == QDialog.DialogCode.Accepted: + print("Save confirmed") + self.confirm() + else: + print("Save cancelled") + self.cancel() + + def confirm(self): + print("Reset confirmed") + + def cancel(self): + print("Reset cancelled") + + def navigate_main_screen(self): + logging.info("Open the user start screen") + self.next_screen = StartScreen(self.screen_height,self.screen_width) + self.next_screen.show() + self.close() + - def confirm(self, mouse): - self.confirmation_modal.visible = False - dataHandaler.resetHistory() - return self - - def cancel(self, mouse): - self.confirmation_modal.visible = False - return self - def navigate_main_screen(self,mouse): - return StartScreen(self.win) \ No newline at end of file +class UserInformationScreen(QWidget): + def __init__(self,screen_height,screen_width): + super().__init__() + self.screen_height = screen_height + self.screen_width = screen_width + self.resize(screen_height,screen_width) + logging.info("Initializing UserInformationScreen") + self.initUI() + + def initUI(self): + logging.info("Setting up UI for StartScreen") + layout = QVBoxLayout() + # Labels and fields + self.fields = { + "中文姓名": QLineEdit(self), + "英文姓名": QLineEdit(self), + "性別": QLineEdit(self), + "出生年": QLineEdit(self), + "教育程度": QLineEdit(self), + "母語": QLineEdit(self), + "跟家人常說語言": QLineEdit(self), + "跟朋友常說語言": QLineEdit(self) + } + + for label, edit in self.fields.items(): + layout.addWidget(QLabel(label)) + layout.addWidget(edit) + + # Save button + save_btn = QPushButton('Save', self) + save_btn.setStyleSheet("font-size: 18px; background-color: green; color: white; padding: 10px; margin-top: 15px;") + save_btn.clicked.connect(self.show_confirm_dialog) + layout.addWidget(save_btn) + + self.setLayout(layout) + self.setWindowTitle('User Information') + + def save_data(self): + user_info = {label: edit.text() for label, edit in self.fields.items()} + print("User Info Saved:", user_info) + + def navigate_test_screen(self): + logging.info("Open the TestScreen") + self.test_screen = TestScreen(self.screen_height,self.screen_width) + self.test_screen.show() + self.close() + + def show_confirm_dialog(self): + dialog = ConfirmDialog(self) + result = dialog.exec() + + if result == QDialog.DialogCode.Accepted: + print("Save confirmed") + self.save_data() + self.navigate_test_screen() + else: + print("Save cancelled") \ No newline at end of file diff --git a/main.py b/main.py index 69a1186..1730d0c 100644 --- a/main.py +++ b/main.py @@ -1,43 +1,23 @@ -from component.button import ButtonElement, ButtonList -from psychopy import visual, core, event from component.screen import StartScreen import logging -import psychopy.visual.line - -from mouseHandler import is_instance_of_class - +from PyQt6.QtWidgets import QApplication +import logging class Game: def __init__(self): - self.win = visual.Window([1200, 800], color="white") - self.current_screen = StartScreen(self.win) - self.mouse = event.Mouse() - + self.screen_height = 1200 + self.screen_width = 800 + pass def start(self): - while self.current_screen is not None: - self.current_screen.draw() - self.win.flip() - - # Check for mouse clicks - if self.mouse.getPressed()[0]: # 0 represents the left mouse button - for element in self.current_screen.elements: - if is_instance_of_class(element) and element.containMouse(self.mouse): - if element.action: - self.setScreen(element.action(self.mouse)) - core.wait(0.2) - print("screen is None") - self.win.close() - core.quit() - - def setScreen(self,screen): - self.current_screen = screen -try: - game = Game() - game.start() -except Exception as e: - logging.error("An error occurred: %s", str(e)) - - - - + app = QApplication([]) + start_screen = StartScreen(self.screen_height,self.screen_width) + start_screen.show() + app.exec() + +if __name__ == "__main__": + try: + game = Game() + game.start() + except Exception as e: + logging.error("An error occurred: %s", str(e)) \ No newline at end of file diff --git a/mouseHandler.py b/mouseHandler.py deleted file mode 100644 index 8a3b64b..0000000 --- a/mouseHandler.py +++ /dev/null @@ -1,20 +0,0 @@ -from component.button import ButtonElement, ButtonList -from component.modal import ConfirmationModal - -clickable_list:list = [ButtonList,ButtonElement, ConfirmationModal] - -def is_instance_of_class(variable, class_list=clickable_list): - """ - Check if a variable is an instance of any class in the class_list. - - Args: - variable: The variable to be checked. - class_list: A list of class names or classes. - - Returns: - True if the variable is an instance of any class in the class_list, False otherwise. - """ - for cls in class_list: - if isinstance(variable, cls): - return True - return False \ No newline at end of file