Module sbx.ui.study

Study interface

Expand source code
"""
Study interface
"""
import random
import sys

from prompt_toolkit.filters import Condition
from prompt_toolkit.key_binding.bindings.focus import (
    focus_next,
    focus_previous,
)
from prompt_toolkit.layout import HSplit, VSplit
from prompt_toolkit.widgets import Button, Label

from sbx.core.study import CardStack
from sbx.core.utility import print_error, simplify_path
from sbx.ui.controls import MarkdownArea
from sbx.ui.editor import EditorInterface

TITLE = "---- SBX - Flashcards ----"
BTN_3_CELL = 3
BTN_SHOW_CELL = 0
LABEL_CELL = 0
PATH_CELL = 1
MODE_BEFORE_ANSWER_VISIBLE = 424
MODE_SELF_EVAL = 124
MODE_DONE = 241


class StudyInterface(EditorInterface):
    """
    Study user interface that can work with a `CardStack`
    """

    def __init__(self, stack: CardStack):
        self._0_callback = self._continue_with_quality(0)
        self._1_callback = self._continue_with_quality(1)
        self._2_callback = self._continue_with_quality(2)
        self._3_callback = self._continue_with_quality(3)
        self._4_callback = self._continue_with_quality(4)
        self._5_callback = self._continue_with_quality(5)
        super().__init__(None)
        self._label_text_parts = [
            "SBX",
            "PATH",
            "Press F1 for help",
            "--STUDY--",
        ]
        self._original_stack = list(stack.iter())
        self._reset_stack()

    def _reset_stack(self):
        self._stack = self._original_stack[:]
        if not self._stack:
            print_error(
                "Nothing to study now, try again later. Or use -i option."
            )
            sys.exit(-1)
        random.shuffle(self._stack)
        self._swap_button_bar(self.generic_button_bar)
        self._current = self._stack.pop()
        self._show_front_only()
        self._mode = MODE_BEFORE_ANSWER_VISIBLE

    def _continue_with_quality(self, quality: int):
        def callback(_=None):
            self._mark_and_continue(quality)

        return callback

    def _mark_and_continue(self, quality: int):
        if self._mode != MODE_SELF_EVAL:
            return
        if not self._stack:
            # WHY? Focus on label otherwise message_box cannot
            #    find current item in focus stack :)
            self._swap_button_bar(self.empty_button_bar)
            self.message_box(
                TITLE, "You have completed all the cards for today."
            )
            self._mode = MODE_DONE
            self._mark_and_save(quality)
            return
        if not self._mark_and_save(quality):
            return
        self._swap_button_bar(self.generic_button_bar)
        self._current = self._stack.pop()
        self._show_front_only()
        self._mode = MODE_BEFORE_ANSWER_VISIBLE

    def _mark_and_save(self, quality):
        self._current.mark(quality)
        try:
            self._current.save()
            return True
        except (IOError, OSError):
            self.message_box(
                TITLE,
                "Failed to update flash card\n"
                "File = {!r}".format(self._current.path),
            )
            return False

    def _show(self, _=None):
        if self._mode != MODE_BEFORE_ANSWER_VISIBLE:
            return
        self.text_area_back.text = self._current.back
        self._swap_button_bar(self.quality_button_bar)
        self._mode = MODE_SELF_EVAL

    def _swap_button_bar(self, bar):
        root_child = list(self.root_container.children)
        root_child[0] = bar
        self.root_container.children = root_child
        self.layout.focus(self.text_area_scratch)
        self.get_current_app().invalidate()

    def _show_front_only(self):
        self._label_text_parts[PATH_CELL] = simplify_path(self._current.path)
        self.text_area_front.text = self._current.front
        self.text_area_back.text = "... not visible ..."
        self.text_area_scratch.text = ""

    def _get_base_layout(self):
        self.text_area_front = MarkdownArea(readonly=True)
        self.text_area_back = MarkdownArea(readonly=True)
        self.text_area_scratch = MarkdownArea(readonly=False)
        self.text_area_front.text = "..."
        self.text_area_back.text = "..."
        self.label = Label(text=self._get_label_text, style="class:status")
        self.btn_0 = Button(text="0", handler=self._0_callback)
        self.btn_1 = Button(text="1", handler=self._1_callback)
        self.btn_2 = Button(text="2", handler=self._2_callback)
        self.btn_3 = Button(text="3", handler=self._3_callback)
        self.btn_4 = Button(text="4", handler=self._4_callback)
        self.btn_5 = Button(text="5", handler=self._5_callback)
        quality_buttons = [
            Label("How good were you?", style="#000000"),
            self.btn_0,
            self.btn_1,
            self.btn_2,
            self.btn_3,
            self.btn_4,
            self.btn_5,
        ]
        self.btn_show = Button(text="Show", handler=self._show)
        self.generic_button_bar = VSplit([self.btn_show], style="bg:#cccccc")
        self.quality_button_bar = VSplit(quality_buttons, style="bg:#cccccc")
        self.empty_button_bar = VSplit(
            [Label("All done!", style="#000000")], style="bg:#cccccc"
        )
        self.root_container = HSplit(
            [
                self.generic_button_bar,
                VSplit(
                    [
                        HSplit(
                            [
                                Label(
                                    text="[Flashcard Front]",
                                    style="class:status",
                                ),
                                self.text_area_front,
                                Label(
                                    text="[Scratch Pad]", style="class:status"
                                ),
                                self.text_area_scratch,
                            ]
                        ),
                        HSplit(
                            [
                                Label(
                                    text="[Flashcard Back]",
                                    style="class:status",
                                ),
                                self.text_area_back,
                            ]
                        ),
                    ]
                ),
                self.label,
            ],
            style="class:main-panel",
        )
        return self.root_container

    def _is_mark(self):
        return self._mode == MODE_SELF_EVAL

    def only_on_mark(self, key):
        def fnc(kbd, action):
            kbd.add(key.strip(), filter=Condition(self._is_mark))(action)

        return fnc

    def _get_stat(self):
        return self._current.human_readable_info

    def _current_text_area(self):
        if not self.layout.buffer_has_focus:
            return None
        buffer = self.layout.current_control
        if id(buffer) == id(self.text_area_scratch.control):
            return self.text_area_scratch
        return None

    def get_keybindings(self) -> dict:
        return {
            "exit": "c-e",
            "next": "c-right,c-down",
            "prev": "c-left,c-up",
            "save": "c-s",
            "help": "f1",
            "info": "c-d",
            "show": "c-r",
            "tab": "tab",
        }

    def get_actions(self) -> dict:
        return {
            "exit": self.exit_clicked,
            "next": focus_next,
            "prev": focus_previous,
            "help": self._help,
            "show": self._show,
            "info": self.display_info,
            "tab": self._indent,
        }

    def _help(self, _):
        message = """
        Main UI
        -----------
        Control+e      - Exit
        Control+Left   - Focus Previous
        Control+Up     - Focus Previous
        Control+Right  - Focus Next
        Control+Down   - Focus Next
        Control+d      - Display Card Stat/Meta Data

        Study Buttons
        -----------
        Control+r      - Reveal

        Voting (You need to manually navigate)
        -----------
        0              - I have no idea what this is?
        1              - I have a vague memory
        2              - I don't remember answer fully
        3              - I got the answer correct (about 80%)
        4              - I got the answer correct (100%)
        5              - I think I have memorised this, very easy.
        """
        self.message_box(TITLE, message)

Classes

class StudyInterface (stack: CardStack)

Study user interface that can work with a CardStack

Expand source code
class StudyInterface(EditorInterface):
    """
    Study user interface that can work with a `CardStack`
    """

    def __init__(self, stack: CardStack):
        self._0_callback = self._continue_with_quality(0)
        self._1_callback = self._continue_with_quality(1)
        self._2_callback = self._continue_with_quality(2)
        self._3_callback = self._continue_with_quality(3)
        self._4_callback = self._continue_with_quality(4)
        self._5_callback = self._continue_with_quality(5)
        super().__init__(None)
        self._label_text_parts = [
            "SBX",
            "PATH",
            "Press F1 for help",
            "--STUDY--",
        ]
        self._original_stack = list(stack.iter())
        self._reset_stack()

    def _reset_stack(self):
        self._stack = self._original_stack[:]
        if not self._stack:
            print_error(
                "Nothing to study now, try again later. Or use -i option."
            )
            sys.exit(-1)
        random.shuffle(self._stack)
        self._swap_button_bar(self.generic_button_bar)
        self._current = self._stack.pop()
        self._show_front_only()
        self._mode = MODE_BEFORE_ANSWER_VISIBLE

    def _continue_with_quality(self, quality: int):
        def callback(_=None):
            self._mark_and_continue(quality)

        return callback

    def _mark_and_continue(self, quality: int):
        if self._mode != MODE_SELF_EVAL:
            return
        if not self._stack:
            # WHY? Focus on label otherwise message_box cannot
            #    find current item in focus stack :)
            self._swap_button_bar(self.empty_button_bar)
            self.message_box(
                TITLE, "You have completed all the cards for today."
            )
            self._mode = MODE_DONE
            self._mark_and_save(quality)
            return
        if not self._mark_and_save(quality):
            return
        self._swap_button_bar(self.generic_button_bar)
        self._current = self._stack.pop()
        self._show_front_only()
        self._mode = MODE_BEFORE_ANSWER_VISIBLE

    def _mark_and_save(self, quality):
        self._current.mark(quality)
        try:
            self._current.save()
            return True
        except (IOError, OSError):
            self.message_box(
                TITLE,
                "Failed to update flash card\n"
                "File = {!r}".format(self._current.path),
            )
            return False

    def _show(self, _=None):
        if self._mode != MODE_BEFORE_ANSWER_VISIBLE:
            return
        self.text_area_back.text = self._current.back
        self._swap_button_bar(self.quality_button_bar)
        self._mode = MODE_SELF_EVAL

    def _swap_button_bar(self, bar):
        root_child = list(self.root_container.children)
        root_child[0] = bar
        self.root_container.children = root_child
        self.layout.focus(self.text_area_scratch)
        self.get_current_app().invalidate()

    def _show_front_only(self):
        self._label_text_parts[PATH_CELL] = simplify_path(self._current.path)
        self.text_area_front.text = self._current.front
        self.text_area_back.text = "... not visible ..."
        self.text_area_scratch.text = ""

    def _get_base_layout(self):
        self.text_area_front = MarkdownArea(readonly=True)
        self.text_area_back = MarkdownArea(readonly=True)
        self.text_area_scratch = MarkdownArea(readonly=False)
        self.text_area_front.text = "..."
        self.text_area_back.text = "..."
        self.label = Label(text=self._get_label_text, style="class:status")
        self.btn_0 = Button(text="0", handler=self._0_callback)
        self.btn_1 = Button(text="1", handler=self._1_callback)
        self.btn_2 = Button(text="2", handler=self._2_callback)
        self.btn_3 = Button(text="3", handler=self._3_callback)
        self.btn_4 = Button(text="4", handler=self._4_callback)
        self.btn_5 = Button(text="5", handler=self._5_callback)
        quality_buttons = [
            Label("How good were you?", style="#000000"),
            self.btn_0,
            self.btn_1,
            self.btn_2,
            self.btn_3,
            self.btn_4,
            self.btn_5,
        ]
        self.btn_show = Button(text="Show", handler=self._show)
        self.generic_button_bar = VSplit([self.btn_show], style="bg:#cccccc")
        self.quality_button_bar = VSplit(quality_buttons, style="bg:#cccccc")
        self.empty_button_bar = VSplit(
            [Label("All done!", style="#000000")], style="bg:#cccccc"
        )
        self.root_container = HSplit(
            [
                self.generic_button_bar,
                VSplit(
                    [
                        HSplit(
                            [
                                Label(
                                    text="[Flashcard Front]",
                                    style="class:status",
                                ),
                                self.text_area_front,
                                Label(
                                    text="[Scratch Pad]", style="class:status"
                                ),
                                self.text_area_scratch,
                            ]
                        ),
                        HSplit(
                            [
                                Label(
                                    text="[Flashcard Back]",
                                    style="class:status",
                                ),
                                self.text_area_back,
                            ]
                        ),
                    ]
                ),
                self.label,
            ],
            style="class:main-panel",
        )
        return self.root_container

    def _is_mark(self):
        return self._mode == MODE_SELF_EVAL

    def only_on_mark(self, key):
        def fnc(kbd, action):
            kbd.add(key.strip(), filter=Condition(self._is_mark))(action)

        return fnc

    def _get_stat(self):
        return self._current.human_readable_info

    def _current_text_area(self):
        if not self.layout.buffer_has_focus:
            return None
        buffer = self.layout.current_control
        if id(buffer) == id(self.text_area_scratch.control):
            return self.text_area_scratch
        return None

    def get_keybindings(self) -> dict:
        return {
            "exit": "c-e",
            "next": "c-right,c-down",
            "prev": "c-left,c-up",
            "save": "c-s",
            "help": "f1",
            "info": "c-d",
            "show": "c-r",
            "tab": "tab",
        }

    def get_actions(self) -> dict:
        return {
            "exit": self.exit_clicked,
            "next": focus_next,
            "prev": focus_previous,
            "help": self._help,
            "show": self._show,
            "info": self.display_info,
            "tab": self._indent,
        }

    def _help(self, _):
        message = """
        Main UI
        -----------
        Control+e      - Exit
        Control+Left   - Focus Previous
        Control+Up     - Focus Previous
        Control+Right  - Focus Next
        Control+Down   - Focus Next
        Control+d      - Display Card Stat/Meta Data

        Study Buttons
        -----------
        Control+r      - Reveal

        Voting (You need to manually navigate)
        -----------
        0              - I have no idea what this is?
        1              - I have a vague memory
        2              - I don't remember answer fully
        3              - I got the answer correct (about 80%)
        4              - I got the answer correct (100%)
        5              - I think I have memorised this, very easy.
        """
        self.message_box(TITLE, message)

Ancestors

Methods

def get_actions(self) ‑> dict
Expand source code
def get_actions(self) -> dict:
    return {
        "exit": self.exit_clicked,
        "next": focus_next,
        "prev": focus_previous,
        "help": self._help,
        "show": self._show,
        "info": self.display_info,
        "tab": self._indent,
    }
def get_keybindings(self) ‑> dict
Expand source code
def get_keybindings(self) -> dict:
    return {
        "exit": "c-e",
        "next": "c-right,c-down",
        "prev": "c-left,c-up",
        "save": "c-s",
        "help": "f1",
        "info": "c-d",
        "show": "c-r",
        "tab": "tab",
    }
def only_on_mark(self, key)
Expand source code
def only_on_mark(self, key):
    def fnc(kbd, action):
        kbd.add(key.strip(), filter=Condition(self._is_mark))(action)

    return fnc

Inherited members