do not crash on malformed schedule files
[sispare-qt.git] / ui.cc
blob7c06ece110ca8c4eeb8799bffeedfff3211c3fde
1 /*
2 * Copyright (c) 2021, S. Gilles <sgilles@sgilles.net>
4 * Permission to use, copy, modify, and/or distribute this software
5 * for any purpose with or without fee is hereby granted, provided
6 * that the above copyright notice and this permission notice appear
7 * in all copies.
9 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
10 * WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
11 * WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
12 * AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
13 * CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
14 * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
15 * NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
16 * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
18 #include "ui.hh"
20 #include <QCloseEvent>
21 #include <QSettings>
22 #include <QSpacerItem>
23 #include <QtWidgets/QScrollBar>
26 * Thin wrapper around QMainWindow with just enough logic to save window
27 * settings.
29 class ThisMainWindow : public QMainWindow {
30 public:
31 ThisMainWindow(Session *the_session, QWidget *parent) :
32 QMainWindow(parent)
34 session = the_session;
35 settings_read();
38 void
39 closeEvent(QCloseEvent *event) override
41 settings_write();
42 session->update_cards_on_disk();
43 event->accept();
46 void
47 settings_read()
49 QSettings settings;
51 settings.beginGroup("MainWindow");
52 resize(settings.value("size", QSize(400, 300)).toSize());
53 move(settings.value("pos", QPoint(200, 200)).toPoint());
54 settings.endGroup();
57 void
58 settings_write() const
60 QSettings settings;
62 settings.beginGroup("MainWindow");
63 settings.setValue("size", size());
64 settings.setValue("pos", pos());
65 settings.endGroup();
68 private:
69 Session *session;
73 * Slightly modified QScrollArea to only allow vertical scrolling, and
74 * Mostly from https://forum.qt.io/topic/13374/solved-qscrollarea-vertical-scroll-only
75 * .
77 class QVerticalScrollArea : public QScrollArea {
78 public:
79 QVerticalScrollArea(QWidget *parent = 0) :
80 QScrollArea(parent)
82 setFrameStyle(QFrame::NoFrame);
83 setWidgetResizable(true);
84 setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
85 setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
88 bool
89 eventFilter(QObject *o, QEvent *e)
91 if (o &&
92 o == widget() &&
93 e->type() == QEvent::Resize) {
94 setMinimumWidth(widget()->minimumSizeHint().width() + verticalScrollBar()->width());
97 return QScrollArea::eventFilter(o, e);
102 UI::UI(Session *the_session, QWidget *parent)
104 session = the_session;
106 /* Set up QT stuff */
107 q_main_window = new ThisMainWindow(session, parent);
109 if (q_main_window->objectName().isEmpty()) {
110 q_main_window->setObjectName(QString::fromUtf8("sispare-qt"));
113 q_main_window->setWindowTitle(QString::fromUtf8("Sispare"));
115 /* Exiting */
116 action_exit = new QAction("E&xit", q_main_window);
117 action_exit->setObjectName(QString::fromUtf8("action_exit"));
118 QObject::connect(action_exit, &QAction::triggered, [this](){
119 q_main_window->close();
122 /* Layout */
125 +--------------------------------------------------+
126 | B1 | T3 |
127 |-------|------------------------------------------|
128 | B2 | |
129 |-------| T1 |
130 | B3 | |
131 |-------| |
132 |-------| |
133 | BF | |
134 |--------------------------------------------------|
135 | B< | T2 | B> |
136 +--------------------------------------------------+
138 T1: Display of the A/B side for current card
139 B1-3: Easy/Hard/Fail buttons
140 BF: Flip A/B
141 B</<: Forward/Back buttons
142 T2: Current progress indicator
143 T3: If we've already marked this card as something
145 central_widget = new QWidget(q_main_window);
146 central_widget->setObjectName(QString::fromUtf8("central_widget"));
148 /* Holds everything */
149 box_layout_0 = new QVBoxLayout();
150 box_layout_0->setObjectName(QString::fromUtf8("box_layout_0"));
151 box_layout_0->setSizeConstraint(QLayout::SetMinimumSize);
153 /* Holds T1, T3, B1-3, and BF */
154 box_layout_top = new QHBoxLayout();
155 box_layout_top->setObjectName(QString::fromUtf8("box_layout_top"));
156 box_layout_top->setSizeConstraint(QLayout::SetMinimumSize);
158 /* Holds B<, T2, B> */
159 box_layout_bottom = new QHBoxLayout();
160 box_layout_bottom->setObjectName(QString::fromUtf8("box_layout_bottom"));
162 /* Holds B1-3 and BF */
163 box_layout_top_left = new QVBoxLayout();
164 box_layout_top_left->setObjectName(QString::fromUtf8("box_layout_top_left"));
166 /* Holds T3 and T1 */
167 box_layout_top_main = new QVBoxLayout();
168 box_layout_top_main->setObjectName(QString::fromUtf8("box_layout_top_main"));
169 box_layout_top_main->setSizeConstraint(QLayout::SetMinimumSize);
171 /* T3 */
172 status_previously_seen = new QLabel();
173 status_previously_seen->setObjectName(QString::fromUtf8("status_previously_seen"));
174 status_previously_seen->setAlignment(Qt::AlignHCenter);
175 status_previously_seen->setText(QString::fromUtf8("(already marked as FAILED)"));
177 /* T1 */
178 scrollable_main_card = new QVerticalScrollArea();
179 scrollable_main_card->setObjectName(QString::fromUtf8("scrollable_main_card"));
180 scrollable_main_card->setAlignment(Qt::AlignHCenter);
181 main_card = new QLabel();
182 main_card->setObjectName(QString::fromUtf8("main_card"));
183 main_card->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::MinimumExpanding);
184 main_card->setWordWrap(true);
185 main_card->setTextInteractionFlags(Qt::TextSelectableByMouse);
186 main_card->setAlignment(Qt::AlignCenter);
188 /* B1-3 caption */
189 remembered = new QLabel();
190 remembered->setObjectName(QString::fromUtf8("remembered"));
191 remembered->setText(QString::fromUtf8("Remembered?"));
193 /* B1 */
194 button_easy = new QPushButton();
195 button_easy->setObjectName(QString::fromUtf8("button_easy"));
196 button_easy->setText(QString::fromUtf8("&Yes"));
197 QObject::connect(button_easy, &QPushButton::clicked, [this](){
198 session->mark_current_card_as(Review_status::Pass_easy);
199 sync_with_session();
202 /* B2 */
203 button_hard = new QPushButton();
204 button_hard->setObjectName(QString::fromUtf8("button_hard"));
205 button_hard->setText(QString::fromUtf8("&Hard"));
206 QObject::connect(button_hard, &QPushButton::clicked, [this](){
207 session->mark_current_card_as(Review_status::Pass_hard);
208 sync_with_session();
211 /* B3 */
212 button_fail = new QPushButton();
213 button_fail->setObjectName(QString::fromUtf8("button_fail"));
214 button_fail->setText(QString::fromUtf8("&No"));
215 QObject::connect(button_fail, &QPushButton::clicked, [this](){
216 session->mark_current_card_as(Review_status::Fail);
217 sync_with_session();
220 /* BF */
221 button_flip = new QPushButton();
222 button_flip->setObjectName(QString::fromUtf8("button_flip"));
223 button_flip->setText(QString::fromUtf8("Flip &Over"));
224 QObject::connect(button_flip, &QPushButton::clicked, [this](){
225 session->flip_current_card();
226 sync_with_session();
229 /* B< */
230 button_prev = new QPushButton();
231 button_prev->setObjectName(QString::fromUtf8("button_prev"));
232 button_prev->setText(QString::fromUtf8("Back"));
233 QObject::connect(button_prev, &QPushButton::clicked, [this](){
234 session->move_prev_card();
235 sync_with_session();
238 /* B> */
239 button_next = new QPushButton();
240 button_next->setObjectName(QString::fromUtf8("button_next"));
241 button_next->setText(QString::fromUtf8("Next"));
242 QObject::connect(button_next, &QPushButton::clicked, [this](){
243 session->move_next_card();
244 sync_with_session();
247 /* T2 */
248 status_progress = new QLabel();
249 status_progress->setObjectName(QString::fromUtf8("status_progress"));
251 /* Add everything to everything else */
252 box_layout_top_left->addSpacing(75);
253 box_layout_top_left->addWidget(remembered);
254 box_layout_top_left->addSpacing(25);
255 box_layout_top_left->addWidget(button_easy);
256 box_layout_top_left->addWidget(button_hard);
257 box_layout_top_left->addWidget(button_fail);
258 box_layout_top_left->addStretch(0);
259 box_layout_top_left->addWidget(button_flip);
260 box_layout_top_left->addStretch(0);
261 scrollable_main_card->setWidget(main_card);
262 box_layout_top_main->addWidget(status_previously_seen);
263 box_layout_top_main->addWidget(scrollable_main_card);
264 box_layout_bottom->addSpacing(100);
265 box_layout_bottom->addWidget(button_prev);
266 box_layout_bottom->addStretch(0);
267 box_layout_bottom->addWidget(status_progress);
268 box_layout_bottom->addStretch(0);
269 box_layout_bottom->addWidget(button_next);
270 box_layout_bottom->addSpacing(100);
271 box_layout_top->addLayout(box_layout_top_left);
272 box_layout_top->addSpacing(15);
273 box_layout_top->addLayout(box_layout_top_main);
274 box_layout_0->addLayout(box_layout_top);
275 box_layout_0->addLayout(box_layout_bottom);
276 central_widget->setLayout(box_layout_0);
277 q_main_window->setCentralWidget(central_widget);
279 /* Menu bar stuff */
280 menu_bar = new QMenuBar(q_main_window);
281 menu_bar->setObjectName(QString::fromUtf8("menu_bar"));
282 menu_bar->setGeometry(QRect(0, 0, 243, 21));
283 menu_bar_file = new QMenu(menu_bar);
284 menu_bar_file->setTitle(QString::fromUtf8("&File"));
285 menu_bar_file->setObjectName(QString::fromUtf8("menu_bar_file"));
286 q_main_window->setMenuBar(menu_bar);
287 menu_bar->addAction(menu_bar_file->menuAction());
288 menu_bar_file->addAction(action_exit);
290 /* Now the UI is all set up. Start the session by showing the first card */
291 sync_with_session();
294 UI::~UI()
296 /* Deletes all children as well */
297 delete q_main_window;
300 void
301 UI::show()
303 q_main_window->show();
307 * Deal with the pretty simple state exposed by Session. Enable/disable
308 * buttons and set text contents as appropriate.
310 void
311 UI::sync_with_session()
313 status_previously_seen->setText(QString::fromStdString(session->get_previously_seen_string()));
314 button_prev->setEnabled(session->have_prev_card());
315 button_next->setEnabled(session->have_next_card());
316 status_progress->setText(QString::fromStdString(session->get_progress_string()));
318 switch (session->get_current_action()) {
319 case Currently_doing::Viewing_A:
320 main_card->setText(QString::fromStdString(session->get_current_A_side().value_or("")));
321 main_card->setAlignment(Qt::AlignCenter);
322 main_card->setStyleSheet("#main_card { font-size: 80pt; }");
323 status_progress->setVisible(true);
324 button_easy->setEnabled(false);
325 button_hard->setEnabled(false);
326 button_fail->setEnabled(false);
327 button_flip->setEnabled(true);
328 break;
329 case Currently_doing::Viewing_B:
330 main_card->setText(QString::fromStdString(session->get_current_B_side().value_or("")));
331 main_card->setAlignment(Qt::AlignJustify);
332 main_card->setStyleSheet("#main_card { font-size: 14pt; }");
333 status_progress->setVisible(true);
334 button_easy->setEnabled(true);
335 button_hard->setEnabled(true);
336 button_fail->setEnabled(true);
337 button_flip->setEnabled(true);
338 break;
339 case Currently_doing::Finished:
340 main_card->setText(QString::fromStdString(session->get_statistics()));
341 main_card->setAlignment(Qt::AlignHCenter);
342 main_card->setStyleSheet("#main_card { }");
343 status_progress->setVisible(false);
344 button_easy->setEnabled(false);
345 button_hard->setEnabled(false);
346 button_fail->setEnabled(false);
347 button_flip->setEnabled(false);
348 break;
349 case Currently_doing::Trivial:
350 main_card->setText(QString::fromUtf8("No review necessary."));
351 main_card->setAlignment(Qt::AlignHCenter);
352 main_card->setStyleSheet("#main_card { }");
353 status_progress->setVisible(false);
354 button_easy->setEnabled(false);
355 button_hard->setEnabled(false);
356 button_fail->setEnabled(false);
357 button_flip->setEnabled(false);
358 break;