MIL-004: Step 3 - Flip the cards
Review record RC-026 (Go, autonomous mode, not independent). Task: MIL-004#1 Task: MIL-004#2 Task: MIL-004#3
This commit is contained in:
@@ -1,113 +1,181 @@
|
||||
"""View of the flash card screen.
|
||||
|
||||
@file flash_card_view.py
|
||||
@brief Builds the window of the wireframe: card canvas and two buttons.
|
||||
"""
|
||||
|
||||
import tkinter as tk
|
||||
|
||||
from flash_card.constants import (
|
||||
ASSETS_DIR,
|
||||
BACKGROUND_COLOR,
|
||||
CARD_CENTER_X,
|
||||
CARD_FRONT_IMAGE_NAME,
|
||||
CARD_HEIGHT,
|
||||
CARD_WIDTH,
|
||||
FRONT_TEXT_COLOR,
|
||||
LANGUAGE_FONT,
|
||||
LANGUAGE_TEXT_Y,
|
||||
RIGHT_IMAGE_NAME,
|
||||
WINDOW_PADDING,
|
||||
WINDOW_TITLE,
|
||||
WORD_FONT,
|
||||
WORD_TEXT_Y,
|
||||
WRONG_IMAGE_NAME,
|
||||
)
|
||||
from flash_card.presentation.features.flash_card.flash_card_view_model import (
|
||||
FlashCardViewModel,
|
||||
)
|
||||
|
||||
|
||||
class FlashCardView:
|
||||
"""The flash card window: a card and the wrong and right buttons."""
|
||||
|
||||
def __init__(self, root: tk.Tk, view_model: FlashCardViewModel) -> None:
|
||||
"""Build the window on a root.
|
||||
|
||||
@param root The Tk root window.
|
||||
@param view_model The data to show.
|
||||
"""
|
||||
## @brief The Tk root window.
|
||||
self._root = root
|
||||
## @brief The data source of the texts.
|
||||
self._view_model = view_model
|
||||
root.title(WINDOW_TITLE)
|
||||
root.config(padx=WINDOW_PADDING, pady=WINDOW_PADDING, bg=BACKGROUND_COLOR)
|
||||
|
||||
# Kept as attributes: Tkinter drops an image nothing refers to.
|
||||
## @brief Card front image, kept so Tk does not drop it.
|
||||
self._card_front_image = tk.PhotoImage(file=ASSETS_DIR / CARD_FRONT_IMAGE_NAME)
|
||||
## @brief Wrong button image, kept so Tk does not drop it.
|
||||
self._wrong_image = tk.PhotoImage(file=ASSETS_DIR / WRONG_IMAGE_NAME)
|
||||
## @brief Right button image, kept so Tk does not drop it.
|
||||
self._right_image = tk.PhotoImage(file=ASSETS_DIR / RIGHT_IMAGE_NAME)
|
||||
|
||||
## @brief Canvas that draws the card.
|
||||
self.canvas = tk.Canvas(
|
||||
root,
|
||||
width=CARD_WIDTH,
|
||||
height=CARD_HEIGHT,
|
||||
bg=BACKGROUND_COLOR,
|
||||
highlightthickness=0,
|
||||
)
|
||||
## @brief Canvas item id of the card image.
|
||||
self.image_id = self.canvas.create_image(
|
||||
CARD_CENTER_X, CARD_HEIGHT / 2, image=self._card_front_image
|
||||
)
|
||||
## @brief Canvas item id of the language name text.
|
||||
self.language_text_id = self.canvas.create_text(
|
||||
CARD_CENTER_X,
|
||||
LANGUAGE_TEXT_Y,
|
||||
text=view_model.language_name,
|
||||
font=LANGUAGE_FONT,
|
||||
fill=FRONT_TEXT_COLOR,
|
||||
)
|
||||
## @brief Canvas item id of the word text.
|
||||
self.word_text_id = self.canvas.create_text(
|
||||
CARD_CENTER_X,
|
||||
WORD_TEXT_Y,
|
||||
text=view_model.text,
|
||||
font=WORD_FONT,
|
||||
fill=FRONT_TEXT_COLOR,
|
||||
)
|
||||
self.canvas.grid(row=0, column=0, columnspan=2)
|
||||
|
||||
## @brief Button for a word the user does not know.
|
||||
self.wrong_button = tk.Button(
|
||||
root,
|
||||
image=self._wrong_image,
|
||||
highlightthickness=0,
|
||||
bd=0,
|
||||
command=self.next_card,
|
||||
)
|
||||
self.wrong_button.grid(row=1, column=0)
|
||||
## @brief Button for a word the user knows.
|
||||
self.right_button = tk.Button(
|
||||
root,
|
||||
image=self._right_image,
|
||||
highlightthickness=0,
|
||||
bd=0,
|
||||
command=self.next_card,
|
||||
)
|
||||
self.right_button.grid(row=1, column=1)
|
||||
|
||||
def next_card(self) -> None:
|
||||
"""Show the next card.
|
||||
|
||||
@return Nothing.
|
||||
"""
|
||||
self._view_model.next_card()
|
||||
self.canvas.itemconfig(
|
||||
self.language_text_id, text=self._view_model.language_name
|
||||
)
|
||||
self.canvas.itemconfig(self.word_text_id, text=self._view_model.text)
|
||||
"""View of the flash card screen.
|
||||
|
||||
@file flash_card_view.py
|
||||
@brief Builds the window of the wireframe: card canvas and two buttons.
|
||||
"""
|
||||
|
||||
import tkinter as tk
|
||||
|
||||
from flash_card.constants import (
|
||||
ASSETS_DIR,
|
||||
BACK_TEXT_COLOR,
|
||||
BACKGROUND_COLOR,
|
||||
CARD_BACK_IMAGE_NAME,
|
||||
CARD_CENTER_X,
|
||||
CARD_FRONT_IMAGE_NAME,
|
||||
CARD_HEIGHT,
|
||||
CARD_WIDTH,
|
||||
FLIP_DELAY_MS,
|
||||
FRONT_TEXT_COLOR,
|
||||
LANGUAGE_FONT,
|
||||
LANGUAGE_TEXT_Y,
|
||||
RIGHT_IMAGE_NAME,
|
||||
WINDOW_PADDING,
|
||||
WINDOW_TITLE,
|
||||
WORD_FONT,
|
||||
WORD_TEXT_Y,
|
||||
WRONG_IMAGE_NAME,
|
||||
)
|
||||
from flash_card.presentation.features.flash_card.flash_card_view_model import (
|
||||
FlashCardViewModel,
|
||||
Side,
|
||||
)
|
||||
|
||||
|
||||
class FlashCardView:
|
||||
"""The flash card window: a card and the wrong and right buttons."""
|
||||
|
||||
def __init__(self, root: tk.Tk, view_model: FlashCardViewModel) -> None:
|
||||
"""Build the window on a root.
|
||||
|
||||
@param root The Tk root window.
|
||||
@param view_model The data to show.
|
||||
"""
|
||||
## @brief The Tk root window.
|
||||
self._root = root
|
||||
## @brief The data source of the texts.
|
||||
self._view_model = view_model
|
||||
root.title(WINDOW_TITLE)
|
||||
root.config(padx=WINDOW_PADDING, pady=WINDOW_PADDING, bg=BACKGROUND_COLOR)
|
||||
|
||||
# Kept as attributes: Tkinter drops an image nothing refers to.
|
||||
## @brief Card front image, kept so Tk does not drop it.
|
||||
self._card_front_image = tk.PhotoImage(file=ASSETS_DIR / CARD_FRONT_IMAGE_NAME)
|
||||
## @brief Card back image, kept so Tk does not drop it.
|
||||
self._card_back_image = tk.PhotoImage(file=ASSETS_DIR / CARD_BACK_IMAGE_NAME)
|
||||
## @brief Wrong button image, kept so Tk does not drop it.
|
||||
self._wrong_image = tk.PhotoImage(file=ASSETS_DIR / WRONG_IMAGE_NAME)
|
||||
## @brief Right button image, kept so Tk does not drop it.
|
||||
self._right_image = tk.PhotoImage(file=ASSETS_DIR / RIGHT_IMAGE_NAME)
|
||||
|
||||
## @brief Canvas that draws the card.
|
||||
self.canvas = tk.Canvas(
|
||||
root,
|
||||
width=CARD_WIDTH,
|
||||
height=CARD_HEIGHT,
|
||||
bg=BACKGROUND_COLOR,
|
||||
highlightthickness=0,
|
||||
)
|
||||
## @brief Canvas item id of the card image.
|
||||
self.image_id = self.canvas.create_image(
|
||||
CARD_CENTER_X, CARD_HEIGHT / 2, image=self._card_front_image
|
||||
)
|
||||
## @brief Canvas item id of the language name text.
|
||||
self.language_text_id = self.canvas.create_text(
|
||||
CARD_CENTER_X,
|
||||
LANGUAGE_TEXT_Y,
|
||||
text=view_model.language_name,
|
||||
font=LANGUAGE_FONT,
|
||||
fill=FRONT_TEXT_COLOR,
|
||||
)
|
||||
## @brief Canvas item id of the word text.
|
||||
self.word_text_id = self.canvas.create_text(
|
||||
CARD_CENTER_X,
|
||||
WORD_TEXT_Y,
|
||||
text=view_model.text,
|
||||
font=WORD_FONT,
|
||||
fill=FRONT_TEXT_COLOR,
|
||||
)
|
||||
self.canvas.grid(row=0, column=0, columnspan=2)
|
||||
|
||||
## @brief Button for a word the user does not know.
|
||||
self.wrong_button = tk.Button(
|
||||
root,
|
||||
image=self._wrong_image,
|
||||
highlightthickness=0,
|
||||
bd=0,
|
||||
command=self.next_card,
|
||||
)
|
||||
self.wrong_button.grid(row=1, column=0)
|
||||
## @brief Button for a word the user knows.
|
||||
self.right_button = tk.Button(
|
||||
root,
|
||||
image=self._right_image,
|
||||
highlightthickness=0,
|
||||
bd=0,
|
||||
command=self.next_card,
|
||||
)
|
||||
self.right_button.grid(row=1, column=1)
|
||||
|
||||
## @brief Id of the pending flip timer, or None when none is pending.
|
||||
self.flip_timer: str | None = None
|
||||
self._start_flip_timer()
|
||||
|
||||
@property
|
||||
def card_front_image(self) -> tk.PhotoImage:
|
||||
"""Return the card front image.
|
||||
|
||||
@return The image shown while the front side is up.
|
||||
"""
|
||||
return self._card_front_image
|
||||
|
||||
@property
|
||||
def card_back_image(self) -> tk.PhotoImage:
|
||||
"""Return the card back image.
|
||||
|
||||
@return The image shown while the back side is up.
|
||||
"""
|
||||
return self._card_back_image
|
||||
|
||||
def next_card(self) -> None:
|
||||
"""Show the next card front and restart the flip timer.
|
||||
|
||||
@return Nothing.
|
||||
"""
|
||||
self._cancel_flip_timer()
|
||||
self._view_model.next_card()
|
||||
self._show_card()
|
||||
self._start_flip_timer()
|
||||
|
||||
def flip_card(self) -> None:
|
||||
"""Show the back of the current card.
|
||||
|
||||
@return Nothing.
|
||||
"""
|
||||
self._cancel_flip_timer()
|
||||
self._view_model.flip_card()
|
||||
self._show_card()
|
||||
|
||||
def _cancel_flip_timer(self) -> None:
|
||||
"""Cancel the pending flip timer, if any.
|
||||
|
||||
@return Nothing.
|
||||
"""
|
||||
if self.flip_timer is not None:
|
||||
self._root.after_cancel(self.flip_timer)
|
||||
self.flip_timer = None
|
||||
|
||||
def _start_flip_timer(self) -> None:
|
||||
"""Schedule the flip unless no card is left.
|
||||
|
||||
@return Nothing.
|
||||
"""
|
||||
if not self._view_model.is_finished:
|
||||
self.flip_timer = self._root.after(FLIP_DELAY_MS, self.flip_card)
|
||||
|
||||
def _show_card(self) -> None:
|
||||
"""Draw image and texts of the side the view model shows.
|
||||
|
||||
@return Nothing.
|
||||
"""
|
||||
if self._view_model.side is Side.BACK:
|
||||
image, color = self._card_back_image, BACK_TEXT_COLOR
|
||||
else:
|
||||
image, color = self._card_front_image, FRONT_TEXT_COLOR
|
||||
self.canvas.itemconfig(self.image_id, image=image)
|
||||
self.canvas.itemconfig(
|
||||
self.language_text_id, text=self._view_model.language_name, fill=color
|
||||
)
|
||||
self.canvas.itemconfig(
|
||||
self.word_text_id, text=self._view_model.text, fill=color
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
@brief Exposes the texts shown on the card; free of any Tkinter import.
|
||||
"""
|
||||
|
||||
import enum
|
||||
from collections.abc import Mapping
|
||||
|
||||
from flash_card.application.dto.card_dto import CardDto
|
||||
@@ -11,6 +12,15 @@ from flash_card.application.use_cases.study_session import StudySession
|
||||
from flash_card.constants import FINISHED_MESSAGE, LANGUAGE_NAMES
|
||||
|
||||
|
||||
class Side(enum.Enum):
|
||||
"""The side of a card that is shown."""
|
||||
|
||||
## @brief The front: the word to learn.
|
||||
FRONT = enum.auto()
|
||||
## @brief The back: the translation.
|
||||
BACK = enum.auto()
|
||||
|
||||
|
||||
class FlashCardViewModel:
|
||||
"""Data the flash card view shows."""
|
||||
|
||||
@@ -30,6 +40,8 @@ class FlashCardViewModel:
|
||||
self._language_names = language_names
|
||||
## @brief The card shown now, or None when none is left.
|
||||
self._card: CardDto | None = session.next_card()
|
||||
## @brief The side of the card shown.
|
||||
self._side = Side.FRONT
|
||||
|
||||
@property
|
||||
def is_finished(self) -> bool:
|
||||
@@ -39,31 +51,54 @@ class FlashCardViewModel:
|
||||
"""
|
||||
return self._card is None
|
||||
|
||||
@property
|
||||
def side(self) -> Side:
|
||||
"""Return the side of the card shown.
|
||||
|
||||
@return FRONT or BACK; FRONT when no card is left.
|
||||
"""
|
||||
return self._side
|
||||
|
||||
@property
|
||||
def language_name(self) -> str:
|
||||
"""Return the language name shown on the card.
|
||||
|
||||
@return The display name of the front language, upper case code when
|
||||
unknown, or an empty string when no card is left.
|
||||
@return The display name of the language of the side shown, upper case
|
||||
code when unknown, or an empty string when no card is left.
|
||||
"""
|
||||
if self._card is None:
|
||||
return ""
|
||||
code = self._card.front_language
|
||||
if self._side is Side.BACK:
|
||||
code = self._card.back_language
|
||||
else:
|
||||
code = self._card.front_language
|
||||
return self._language_names.get(code, code.upper())
|
||||
|
||||
@property
|
||||
def text(self) -> str:
|
||||
"""Return the word shown on the card.
|
||||
|
||||
@return The front text, or the finished message when no card is left.
|
||||
@return The text of the side shown, or the finished message when no
|
||||
card is left.
|
||||
"""
|
||||
if self._card is None:
|
||||
return FINISHED_MESSAGE
|
||||
if self._side is Side.BACK:
|
||||
return self._card.back_text
|
||||
return self._card.front_text
|
||||
|
||||
def next_card(self) -> None:
|
||||
"""Draw the next card.
|
||||
"""Draw the next card and show its front.
|
||||
|
||||
@return Nothing.
|
||||
"""
|
||||
self._card = self._session.next_card()
|
||||
self._side = Side.FRONT
|
||||
|
||||
def flip_card(self) -> None:
|
||||
"""Show the back of the card; does nothing when finished or flipped.
|
||||
|
||||
@return Nothing.
|
||||
"""
|
||||
if self._card is not None:
|
||||
self._side = Side.BACK
|
||||
|
||||
Reference in New Issue
Block a user