~kevang/mnemosyne-proj/grade-shortcuts-improvements

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
#
# review_wdgt.py <Peter.Bienstman@UGent.be>
#

from PyQt4 import QtCore, QtGui, QtWebKit

from mnemosyne.libmnemosyne.translator import _
from mnemosyne.pyqt_ui.ui_review_wdgt import Ui_ReviewWdgt
from mnemosyne.libmnemosyne.ui_components.review_widget import ReviewWidget


class QAOptimalSplit(object):

    """Algorithm to make sure the split between question and answer boxes is
    as optimal as possible.

    This code is not part of ReviewWidget, in order to be able to reuse this
    in Preview Cards.

    """

    used_for_reviewing = True

    def __init__(self):
        self.question.settings().setAttribute(\
            QtWebKit.QWebSettings.PluginsEnabled, True)
        self.answer.settings().setAttribute(\
            QtWebKit.QWebSettings.PluginsEnabled, True)
        # Add some dummy QWebViews that will be used to determine the actual
        # size of the rendered html. This information will then be used to
        # determine the optimal split between the question and the answer
        # pane.
        self.question_preview = QtWebKit.QWebView()
        self.question_preview.loadFinished.connect(\
            self.question_preview_load_finished)
        self.answer_preview = QtWebKit.QWebView()
        self.answer_preview.loadFinished.connect(\
            self.answer_preview_load_finished)
        # Calculate an offset to use in the stretching factor of the boxes,
        # e.g. question_box = question_label + question.
        self.stretch_offset = self.question_label.size().height()
        if self.question_box.spacing() != -1:
            self.stretch_offset += self.question_box.spacing()
        self.scrollbar_width = QtGui.QScrollBar().sizeHint().width()
        self.question_text = ""
        self.answer_text = ""
        self.required_question_size = self.question.size()
        self.required_answer_size = self.answer.size()
        self.is_answer_showing = False
        self.times_resized = 0

    def resizeEvent(self, event):
        # Update stretch factors when changing the size of the window.
        self.set_question(self.question_text)
        self.set_answer(self.answer_text)
        # To get this working the first time we start the program (in general,
        # the first one or two resize events, depending on whether or not we
        # changed the window size), we need to explicitly show the contents.
        # (Qt bug?). We don't do this on subsequent resizes to prevent flicker
        # and media replays.
        if self.times_resized < 2:
            self.reveal_question()
            if self.is_answer_showing:
                self.reveal_answer()
            self.times_resized += 1
        return QtGui.QWidget.resizeEvent(self, event)

    def question_preview_load_finished(self):
        self.required_question_size = \
            self.question_preview.page().currentFrame().contentsSize()
        self.update_stretch_factors()

    def answer_preview_load_finished(self):
        self.required_answer_size = \
            self.answer_preview.page().currentFrame().contentsSize()
        self.update_stretch_factors()

    def update_stretch_factors(self):
        total_height_available = self.question.height() + self.answer.height()
        # Correct the required heights of question and answer for the
        # presence of horizontal scrollbars.
        required_question_height = self.required_question_size.height()
        if self.required_question_size.width() > self.question.width():
            required_question_height += self.scrollbar_width
        required_answer_height = self.required_answer_size.height()
        if self.required_answer_size.width() > self.answer.width():
            required_answer_height += self.scrollbar_width
        total_height_available = self.question.height() + self.answer.height()
        # If both question and answer fit in their own boxes, there is no need
        # to deviate from a 50/50 split.
        if required_question_height < total_height_available / 2 and \
            required_answer_height < total_height_available / 2:
            question_stretch = 50
            answer_stretch = 50
        # Don't be clairvoyant about the answer size, unless we will need
        # a non 50/50 split to start with.
        # If we are only showing the question, we try to limit 'surprising',
        # 'clairvoyant' stretches if they are not needed.
        elif not self.is_answer_showing:
            if required_question_height < total_height_available / 2:
                # No need to be clairvoyant.
                question_stretch = 50
                answer_stretch = 50
            else:
                # Make enough room for the question.
                question_stretch = required_question_height
                if required_question_height + required_answer_height \
                    <= total_height_available:
                    # Already have the stretch set-up to accomodate the answer,
                    # which makes the UI more relaxed (no need to have a
                    # different non 50/50 split once the answer is shown).
                    answer_stretch = required_answer_height
                else:
                    # But if we don't have enough space to show both the
                    # question and the answer, make sure the question gets
                    # all the space it can get now.
                    answer_stretch = 50
        # We are showing both question and answer.
        else:
            answer_stretch = required_answer_height
            if required_question_height + required_answer_height \
                    <= total_height_available:
                # If we have enough space, stretch in proportion to height.
                question_stretch = required_question_height
            else:
                # If we are previewing cards, go for a 50/50 split.
                if not self.used_for_reviewing:
                    question_stretch = 50
                    answer_stretch = 50
                # If we don't have enough space to show both the question and
                # the answer, try to give the answer all the space it needs.
                else:
                    answer_stretch = required_answer_height
                    question_stretch = total_height_available - answer_stretch
                    if question_stretch < 50:
                        question_stretch = 50        
        self.vertical_layout.setStretchFactor(\
            self.question_box, question_stretch + self.stretch_offset)
        self.vertical_layout.setStretchFactor(\
            self.answer_box, answer_stretch + self.stretch_offset)

    def silence_media(self, text):
        # Silence media, but make sure the player widget still shows to get
        # correct information about the geometry.
        text = text.replace("var soundFiles = new Array(",
                            "var soundFiles = new Array('off',")
        text = text.replace("<audio src=\"", "<audio src=\"off")
        text = text.replace("<video src=\"", "<video src=\"off")
        return text

    def set_question(self, text):
        #self.main_widget().show_information(text.replace("<", "&lt;"))
        self.question_text = text
        self.question_preview.page().setPreferredContentsSize(\
            QtCore.QSize(self.question.size().width(), 1))
        self.question_preview.setHtml(self.silence_media(text))

    def set_answer(self, text):
        #self.main_widget().show_information(text.replace("<", "&lt;"))        
        self.answer_text = text
        self.answer_preview.page().setPreferredContentsSize(\
            QtCore.QSize(self.answer.size().width(), 1))
        self.answer_preview.setHtml(self.silence_media(text))

    def reveal_question(self):
        self.question.setHtml(self.question_text)

    def reveal_answer(self):
        self.is_answer_showing = True
        self.update_stretch_factors()
        self.answer.setHtml(self.answer_text)

    def clear_question(self):
        self.question.setHtml(self.empty())

    def clear_answer(self):
        self.is_answer_showing = False
        self.update_stretch_factors()
        self.answer.setHtml(self.empty())


class ReviewWdgt(QtGui.QWidget, QAOptimalSplit, Ui_ReviewWdgt, ReviewWidget):

    auto_focus_grades = True
    number_keys_show_answer = True

    key_to_grade_map = {QtCore.Qt.Key_QuoteLeft: 0, QtCore.Qt.Key_0: 0,
            QtCore.Qt.Key_1: 1, QtCore.Qt.Key_2: 2, QtCore.Qt.Key_3: 3,
            QtCore.Qt.Key_4: 4, QtCore.Qt.Key_5: 5}
     
    def __init__(self, component_manager):
        ReviewWidget.__init__(self, component_manager)
        parent = self.main_widget()
        QtGui.QWidget.__init__(self, parent)
        parent.setCentralWidget(self)
        self.setupUi(self)
        # TODO: move this to designer with update of PyQt.
        self.grade_buttons = QtGui.QButtonGroup()
        self.grade_buttons.addButton(self.grade_0_button, 0)
        self.grade_buttons.addButton(self.grade_1_button, 1)
        self.grade_buttons.addButton(self.grade_2_button, 2)
        self.grade_buttons.addButton(self.grade_3_button, 3)
        self.grade_buttons.addButton(self.grade_4_button, 4)
        self.grade_buttons.addButton(self.grade_5_button, 5)
        self.grade_buttons.buttonClicked[int].connect(self.grade_answer)
        # TODO: remove this once Qt bug is fixed.
        self.setTabOrder(self.grade_1_button, self.grade_2_button)
        self.setTabOrder(self.grade_2_button, self.grade_3_button)
        self.setTabOrder(self.grade_3_button, self.grade_4_button)
        self.setTabOrder(self.grade_4_button, self.grade_5_button)
        self.setTabOrder(self.grade_5_button, self.grade_0_button)
        self.setTabOrder(self.grade_0_button, self.grade_1_button)
        self.focus_widget = None
        self.sched = QtGui.QLabel("", parent.status_bar)
        self.notmem = QtGui.QLabel("", parent.status_bar)
        self.act = QtGui.QLabel("", parent.status_bar)
        parent.clear_status_bar()
        parent.add_to_status_bar(self.sched)
        parent.add_to_status_bar(self.notmem)
        parent.add_to_status_bar(self.act)
        parent.status_bar.setSizeGripEnabled(0)
        QAOptimalSplit.__init__(self)

    def changeEvent(self, event):
        if hasattr(self, "show_button"):
            show_button_text = self.show_button.text()
        if event.type() == QtCore.QEvent.LanguageChange:
            self.retranslateUi(self)
        # retranslateUI resets the show button text to 'Show answer',
        # so we need to work around this.
        if hasattr(self, "show_button"):
            self.show_button.setText(_(show_button_text))
        QtGui.QWidget.changeEvent(self, event)

    def keyPressEvent(self, event):
        if event.key() in self.key_to_grade_map:
            if not event.isAutoRepeat():
                if self.is_answer_showing:
                    self.grade_answer(self.key_to_grade_map[event.key()])
                elif self.review_controller().is_question_showing():
                    if self.number_keys_show_answer:
                        self.show_answer()
        elif not event.isAutoRepeat() and event.key() \
            == QtCore.Qt.Key_QuoteLeft and \
            not self.review_controller().is_question_showing():
            self.review_controller().grade_answer(0)
        elif event.key() == QtCore.Qt.Key_PageDown:
            self.scroll_down()
        elif event.key() == QtCore.Qt.Key_PageUp:
            self.scroll_up()
        elif event.key() == QtCore.Qt.Key_R and \
            event.modifiers() == QtCore.Qt.ControlModifier:
            self.review_controller().update_dialog(redraw_all=True) # Replay media.
        elif event.key() == QtCore.Qt.Key_C and \
            event.modifiers() == QtCore.Qt.ControlModifier:
            self.copy()
        # Work around Qt issue.
        elif event.key() in [QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete]:
            self.controller().delete_current_card()
        else:
            QtGui.QWidget.keyPressEvent(self, event)

    def empty(self):
        background = self.palette().color(QtGui.QPalette.Base).name()
        if self.review_controller().card:
            colour = self.config().card_type_property(\
            "background_colour", self.review_controller().card.card_type)
            if colour:
                background = ("%X" % colour)[2:] # Strip alpha.
        return """
        <html><head>
        <style type="text/css">
        table { height: 100%; }
        body  { background-color: """ + background + """;
                margin: 0;
                padding: 0;
                border: thin solid #8F8F8F; }
        </style></head>
        <body><table><tr><td>
        </td></tr></table></body></html>"""

    def scroll_down(self):
        if self.review_controller().is_question_showing() or \
           self.review_controller().card.fact_view.a_on_top_of_q:
            frame = self.question.page().mainFrame()
        else:
            frame = self.answer.page().mainFrame()
        x, y = frame.scrollPosition().x(), frame.scrollPosition().y()
        y += int(0.9*(frame.geometry().height()))
        #frame.scroll(x, y) # Seems buggy 20111121.
        frame.evaluateJavaScript("window.scrollTo(%d, %d);" % (x, y))

    def scroll_up(self):
        if self.review_controller().is_question_showing() or \
           self.review_controller().card.fact_view.a_on_top_of_q:
            frame = self.question.page().mainFrame()
        else:
            frame = self.answer.page().mainFrame()
        x, y = frame.scrollPosition().x(), frame.scrollPosition().y()
        y -= int(0.9*(frame.geometry().height()))
        #frame.scroll(x, y)  # Seems buggy 20111121.
        frame.evaluateJavaScript("window.scrollTo(%d, %d);" % (x, y))

    def copy(self):
        if self.review_controller().is_question_showing() or \
           self.review_controller().card.fact_view.a_on_top_of_q:
            webview = self.question
        else:
            webview = self.answer
        webview.pageAction(QtWebKit.QWebPage.Copy).trigger()

    def show_answer(self):     
        self.review_controller().show_answer()

    def grade_answer(self, grade):
        self.vertical_layout.setStretchFactor(self.question_box, 50)
        self.vertical_layout.setStretchFactor(self.answer_box, 50)
        self.review_controller().grade_answer(grade)

    def set_question_box_visible(self, is_visible):
        if is_visible:
            self.question.show()
            self.question_label.show()
        else:
            self.question.hide()
            self.question_label.hide()

    def set_answer_box_visible(self, is_visible):
        if is_visible:
            self.answer.show()
            self.answer_label.show()
        else:
            self.answer.hide()
            self.answer_label.hide()

    def set_question_label(self, text):
        self.question_label.setText(text)

    def restore_focus(self):
        # After clicking on the question or the answer, that widget grabs the
        # focus, so that the keyboard shortcuts no longer work. This functions
        # is used to set the focus back to the correct widget.
        if self.focus_widget:
            self.focus_widget.setDefault(True)
            self.focus_widget.setFocus()

    def update_show_button(self, text, is_default, is_enabled):
        self.show_button.setText(text)
        self.show_button.setEnabled(is_enabled)
        if is_default:
            self.show_button.setDefault(True)
            self.show_button.setFocus()
            self.focus_widget = self.show_button

    def set_grades_enabled(self, is_enabled):
        self.grades.setEnabled(is_enabled)

    def set_grade_enabled(self, grade, is_enabled):
        self.grade_buttons.button(grade).setEnabled(is_enabled)

    def set_default_grade(self, grade):
        if self.auto_focus_grades:
            # On Windows, we seem to need to clear the previous default
            # first.
            for grade_i in range(6):
                self.grade_buttons.button(grade_i).setDefault(False)
            self.grade_buttons.button(grade).setDefault(True)
            self.grade_buttons.button(grade).setFocus()
            self.focus_widget = self.grade_buttons.button(grade)

    def set_grades_title(self, text):
        self.grades.setTitle(text)

    def set_grade_text(self, grade, text):
        self.grade_buttons.button(grade).setText(text)

    def set_grade_tooltip(self, grade, text):
        self.grade_buttons.button(grade).setToolTip(text)

    def update_status_bar_counters(self):
        scheduled_count, non_memorised_count, active_count = \
            self.review_controller().counters()
        self.sched.setText(_("Scheduled: %d ") % scheduled_count)
        self.notmem.setText(_("Not memorised: %d ") % non_memorised_count)
        self.act.setText(_("Active: %d ") % active_count)

    def redraw_now(self):
        self.repaint()
        self.parent().repaint()