1 /*
2 * Copyright (C) 2006-2019 Christopho, Solarus - http://www.solarus-games.org
3 *
4 * Solarus is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * Solarus is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License along
15 * with this program. If not, see <http://www.gnu.org/licenses/>.
16 */
17 #include "solarus/gui/about_dialog.h"
18 #include "solarus/gui/gui_tools.h"
19 #include "solarus/gui/main_window.h"
20 #include "solarus/gui/quests_view.h"
21 #include "solarus/gui/settings.h"
22 #include <QDesktopWidget>
23 #include <QFileDialog>
24 #include <QMessageBox>
25 #include <QMimeData>
26
27 namespace SolarusGui {
28
29 /**
30 * @brief Creates a main window.
31 * @param parent Parent object or nullptr.
32 */
MainWindow(QWidget * parent)33 MainWindow::MainWindow(QWidget* parent) :
34 QMainWindow(parent),
35 quest_runner() {
36
37 // Set up widgets.
38 ui.setupUi(this);
39
40 // Icon.
41 const QStringList& icon_sizes = { "16", "24", "32", "48", "64", "128", "256" };
42 QIcon icon;
43 for (const QString& size : icon_sizes) {
44 icon.addPixmap(":/images/icon/solarus_launcher_icon_" + size + ".png");
45 }
46 setWindowIcon(icon);
47
48 // Console.
49 ui.console->set_quest_runner(quest_runner);
50 ui.console->set_command_enabled(false);
51 const int console_height = 150;
52 ui.console_splitter->setSizes({ height() - console_height, console_height });
53
54 // Show recent quests.
55 Settings settings;
56 const QStringList& quest_paths = settings.value("quests_paths").toStringList();
57 for (const QString& path : quest_paths) {
58 ui.quests_view->add_quest(path);
59 }
60
61 // Select the last quest played.
62 QString last_quest = settings.value("last_quest").toString();
63 if (!last_quest.isEmpty()) {
64 ui.quests_view->select_quest(last_quest);
65 }
66
67 // Menus.
68 initialize_menus();
69
70 // Actions.
71 ui.action_add_quest->setShortcut(QKeySequence::Open);
72 ui.action_exit->setShortcut(QKeySequence::Quit);
73 ui.add_button->setDefaultAction(ui.action_add_quest);
74 ui.remove_button->setDefaultAction(ui.action_remove_quest);
75
76 setAcceptDrops(true);
77
78 // Make connections.
79 connect(ui.quests_view->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)),
80 this, SLOT(selected_quest_changed()));
81 connect(ui.play_button, SIGNAL(clicked()),
82 this, SLOT(on_action_play_quest_triggered()));
83 connect(ui.quests_view, SIGNAL(activated(QModelIndex)),
84 this, SLOT(on_action_play_quest_triggered()));
85 connect(&quest_runner, SIGNAL(running()),
86 this, SLOT(update_run_quest()));
87 connect(&quest_runner, SIGNAL(finished()),
88 this, SLOT(update_run_quest()));
89 connect(ui.console, SIGNAL(setting_changed_in_quest(QString, QVariant)),
90 this, SLOT(setting_changed_in_quest(QString, QVariant)));
91
92 selected_quest_changed();
93 update_run_quest();
94 }
95
96 /**
97 * @brief Sets an appropriate size and centers the window on the screen having
98 * the mouse.
99 */
initialize_geometry_on_screen()100 void MainWindow::initialize_geometry_on_screen() {
101
102 QDesktopWidget* desktop = QApplication::desktop();
103 QRect screen = desktop->screenGeometry(desktop->screenNumber(QCursor::pos()));
104
105 // Center the window on the screen where the mouse is currently.
106 int x = screen.width() / 2 - frameGeometry().width() / 2 + screen.left() - 2;
107 int y = screen.height() / 2 - frameGeometry().height() / 2 + screen.top() - 10;
108
109 move(qMax(0, x), qMax(0, y));
110 }
111
112 /**
113 * @brief Sets up the menus of the main window.
114 */
initialize_menus()115 void MainWindow::initialize_menus() {
116
117 // TODO implement the audio menu
118 delete ui.menu_audio;
119 ui.menu_audio = nullptr;
120
121 update_menus();
122 }
123
124 /**
125 * @brief Updates menus with the current settings.
126 */
update_menus()127 void MainWindow::update_menus() {
128
129 update_fullscreen_action();
130 update_force_software_action();
131 }
132
133 /**
134 * @brief Updates the fullscreen action with the current settings.
135 */
update_fullscreen_action()136 void MainWindow::update_fullscreen_action() {
137
138 Settings settings;
139
140 bool fullscreen = settings.value("quest_fullscreen", false).toBool();
141 ui.action_fullscreen->setChecked(fullscreen);
142 }
143
144 /**
145 * @brief Updates the force software action with the current settings.
146 */
update_force_software_action()147 void MainWindow::update_force_software_action() {
148 Settings settings;
149
150 bool force_software = settings.value("force_software_rendering", false).toBool();
151 ui.action_force_software->setChecked(force_software);
152 }
153
154 /**
155 * @brief Add and select a quest if it exists and is not already known.
156 * @param quest_path Path to the quest to try to add.
157 */
add_quest(QString quest_path)158 bool MainWindow::add_quest(QString quest_path) {
159
160 // Sanitize path: Quest is a folder with a data folder.
161 QString end0("data/quest.dat");
162 QString end1("data");
163 if (quest_path.endsWith(end0)) {
164 quest_path.chop(end0.size());
165 } else if (quest_path.endsWith(end1)) {
166 quest_path.chop(end1.size());
167 }
168
169 // Check if the quest is already in the list.
170 if (ui.quests_view->has_quest(quest_path)) {
171 ui.quests_view->select_quest(quest_path);
172 return false;
173 }
174
175 // Add to the quest list view.
176 if (!ui.quests_view->add_quest(quest_path)) {
177 return false;
178 }
179
180 // Remember the new quest list.
181 Settings settings;
182 settings.setValue("quests_paths", ui.quests_view->get_paths());
183
184 // Select the new quest.
185 ui.quests_view->select_quest(quest_path);
186
187 return true;
188 }
189
190 /**
191 * @brief Receives a window close event.
192 * @param event The event to handle.
193 */
closeEvent(QCloseEvent * event)194 void MainWindow::closeEvent(QCloseEvent* event) {
195
196 if (confirm_close()) {
197 event->accept();
198 }
199 else {
200 event->ignore();
201 }
202 }
203
204 /**
205 * @brief Receives a drag enter event.
206 * @param event The event to handle.
207 */
dragEnterEvent(QDragEnterEvent * event)208 void MainWindow::dragEnterEvent(QDragEnterEvent* event) {
209
210 if (event->mimeData()->hasUrls()) {
211 event->acceptProposedAction();
212 }
213 }
214
215 /**
216 * @brief Receives a drop event.
217 * @param event The event to handle.
218 */
dropEvent(QDropEvent * event)219 void MainWindow::dropEvent(QDropEvent* event) {
220
221 QMimeData const * mime = event->mimeData();
222 if (mime->hasUrls()) {
223 for (QUrl const & url : mime->urls()) {
224 // Local file actually can be a non-local file.
225 if (url.isLocalFile() && url.host().isEmpty()) {
226 if (add_quest(url.toLocalFile())) {
227 break;
228 }
229 }
230 }
231 }
232 }
233
234 /**
235 * @brief Function called when the user wants to exit the program.
236 *
237 * A confirmation dialog is shown if a quest is running.
238 *
239 * @return @c false to cancel the closing operation.
240 */
confirm_close()241 bool MainWindow::confirm_close() {
242
243 if (!quest_runner.is_started()) {
244 // No quest is running.
245 return true;
246 }
247
248 QMessageBox::StandardButton answer = QMessageBox::warning(
249 nullptr,
250 tr("A quest is playing"),
251 tr("A quest is playing. Do you really want to exit Solarus?"),
252 QMessageBox::Close | QMessageBox::Cancel
253 );
254
255 switch (answer) {
256
257 case QMessageBox::Close:
258 return true;
259
260 case QMessageBox::Cancel:
261 case QMessageBox::Escape:
262 return false;
263
264 default:
265 return false;
266 }
267
268 return true;
269 }
270
271 /**
272 * @brief Slot called when the user adds a quest to the quest list.
273 */
on_action_add_quest_triggered()274 void MainWindow::on_action_add_quest_triggered() {
275
276 QString quest_path = QFileDialog::getOpenFileName(
277 this,
278 tr("Select archive or quest.dat")
279 );
280
281 if (quest_path.isEmpty()) {
282 return;
283 }
284
285 if (!add_quest(quest_path)) {
286 GuiTools::error_dialog(tr("No quest was found in this directory"));
287 }
288 }
289
290 /**
291 * @brief Slot called when the user removes a quest to the quest list.
292 */
on_action_remove_quest_triggered()293 void MainWindow::on_action_remove_quest_triggered() {
294
295 int selected_index = ui.quests_view->get_selected_index();
296
297 if (selected_index == -1) {
298 return;
299 }
300
301 // Remove from the quest list view.
302 if (ui.quests_view->remove_quest(selected_index)) {
303
304 // Update the quest list.
305 Settings settings;
306 settings.setValue("quests_paths", ui.quests_view->get_paths());
307 }
308
309 // Select the next one.
310 int num_quests = ui.quests_view->get_num_quests();
311 selected_index = qMin(selected_index, num_quests - 1);
312 ui.quests_view->select_quest(selected_index);
313 }
314
315 /**
316 * @brief Slot called when the user triggers the "Exit" action.
317 */
on_action_exit_triggered()318 void MainWindow::on_action_exit_triggered() {
319
320 if (confirm_close()) {
321 QApplication::exit(0);
322 }
323 }
324
325 /**
326 * @brief Slot called when the user triggers the "Play quest" action.
327 */
on_action_play_quest_triggered()328 void MainWindow::on_action_play_quest_triggered() {
329
330 if (quest_runner.is_started()) {
331 return;
332 }
333
334 QString path = ui.quests_view->get_selected_path();
335 if (path.isEmpty()) {
336 return;
337 }
338
339 // Write system settings to the settings.dat file of the quest.
340 Settings settings;
341 settings.export_to_quest(path);
342
343 // Run the quest.
344 quest_runner.start(path);
345
346 update_run_quest();
347 }
348
349 /**
350 * @brief Slot called when the user triggers the "Stop quest" action.
351 */
on_action_stop_quest_triggered()352 void MainWindow::on_action_stop_quest_triggered() {
353
354 if (!quest_runner.is_started()) {
355 return;
356 }
357
358 quest_runner.stop();
359
360 update_run_quest();
361 }
362
363 /**
364 * @brief Slot called when the user triggers the "Fullscreen" action.
365 */
on_action_fullscreen_triggered()366 void MainWindow::on_action_fullscreen_triggered() {
367
368 bool fullscreen = ui.action_fullscreen->isChecked();
369
370 Settings settings;
371 bool previous = settings.value("quest_fullscreen", false).toBool();
372 if (fullscreen == previous) {
373 return;
374 }
375
376 settings.setValue("quest_fullscreen", fullscreen);
377
378 if (quest_runner.is_started()) {
379 // Change the setting in the current quest process.
380 QString command = QString("sol.video.set_fullscreen(%1)").arg(fullscreen ? "true" : "false");
381 ui.console->execute_command(command);
382 }
383 }
384
on_action_force_software_triggered()385 void MainWindow::on_action_force_software_triggered() {
386 bool force = ui.action_force_software->isChecked();
387
388 Settings settings;
389 settings.setValue("force_software_rendering", force);
390 }
391
392 /**
393 * @brief Slot called when the user triggers the "Zoom x1" action.
394 */
on_action_zoom_x1_triggered()395 void MainWindow::on_action_zoom_x1_triggered() {
396 set_zoom_requested(1);
397 }
398
399 /**
400 * @brief Slot called when the user triggers the "Zoom x2" action.
401 */
on_action_zoom_x2_triggered()402 void MainWindow::on_action_zoom_x2_triggered() {
403 set_zoom_requested(2);
404 }
405
406 /**
407 * @brief Slot called when the user triggers the "Zoom x3" action.
408 */
on_action_zoom_x3_triggered()409 void MainWindow::on_action_zoom_x3_triggered() {
410 set_zoom_requested(3);
411 }
412
413 /**
414 * @brief Slot called when the user triggers the "Zoom x4" action.
415 */
on_action_zoom_x4_triggered()416 void MainWindow::on_action_zoom_x4_triggered() {
417 set_zoom_requested(4);
418 }
419
420 /**
421 * @brief Slot called when the user triggers the "About" action.
422 */
on_action_about_triggered()423 void MainWindow::on_action_about_triggered() {
424 AboutDialog dialog(this);
425 dialog.exec();
426 }
427
428 /**
429 * @brief Slot called when the quest has just started or stopped.
430 */
update_run_quest()431 void MainWindow::update_run_quest() {
432
433 QString selected_path = ui.quests_view->get_selected_path();
434 bool has_current = !selected_path.isEmpty();
435 bool playing = quest_runner.is_started();
436
437 bool enable_play = has_current && !playing;
438 bool enable_stop = has_current && playing;
439 ui.action_play_quest->setEnabled(enable_play);
440 ui.play_button->setEnabled(enable_play);
441 ui.action_stop_quest->setEnabled(enable_stop);
442
443 ui.menu_zoom->setEnabled(playing);
444 }
445
446 /**
447 * @brief Slot called when the selection changes in the quest list.
448 */
selected_quest_changed()449 void MainWindow::selected_quest_changed() {
450
451 QString selected_path = ui.quests_view->get_selected_path();
452 bool has_current = !selected_path.isEmpty();
453
454 // Enable/disable buttons.
455 ui.action_remove_quest->setEnabled(has_current);
456 update_run_quest();
457
458 // Save the last selected quest (including if none).
459 Settings settings;
460 settings.setValue("last_quest", selected_path);
461
462 static const QPixmap default_pixmap = QPixmap(":/images/no_logo.png");
463
464 if (!has_current) {
465 ui.quest_info_panel->setEnabled(false);
466
467 ui.quest_title_value->clear();
468 ui.quest_title_value->setVisible(false);
469
470 ui.quest_author_label->setVisible(false);
471 ui.quest_author_value->setVisible(false);
472 ui.quest_author_value->clear();
473
474 ui.quest_version_label->setVisible(false);
475 ui.quest_version_value->setVisible(false);
476 ui.quest_version_value->clear();
477
478 ui.quest_long_description_value->setVisible(false);
479 ui.quest_long_description_value->clear();
480
481 ui.quest_box_label->setPixmap(default_pixmap);
482 }
483 else {
484 ui.quest_info_panel->setEnabled(true);
485
486 const Solarus::QuestProperties properties =
487 ui.quests_view->get_selected_quest_properties();
488
489 ui.quest_box_label->setPixmap(ui.quests_view->get_selected_logo());
490
491 QString title = QString::fromStdString(properties.get_title());
492 ui.quest_title_value->setVisible(true);
493 ui.quest_title_value->setText(title);
494
495 QString author = QString::fromStdString(properties.get_author());
496 if (author.isEmpty()) {
497 ui.quest_author_label->setVisible(false);
498 ui.quest_author_value->setVisible(false);
499 }
500 else {
501 ui.quest_author_label->setVisible(true);
502 ui.quest_author_value->setText(author);
503 ui.quest_author_value->setVisible(true);
504 }
505
506 QString quest_version =
507 QString::fromStdString(properties.get_quest_version());
508 if (quest_version.isEmpty()) {
509 ui.quest_version_label->setVisible(false);
510 ui.quest_version_value->setVisible(false);
511 }
512 else {
513 ui.quest_version_label->setVisible(true);
514 ui.quest_version_value->setText(quest_version);
515 ui.quest_version_value->setVisible(true);
516 }
517
518 QString long_description =
519 QString::fromStdString(properties.get_long_description());
520 ui.quest_long_description_value->setVisible(true);
521 ui.quest_long_description_value->setText(long_description);
522 }
523 }
524
525 /**
526 * @brief Slot called when a setting changes in the currently running quest.
527 *
528 * The GUI is updated to reflect and remember the new setting.
529 *
530 * @param key Name of the system setting that has just changed.
531 * @param value The new value.
532 */
setting_changed_in_quest(const QString & key,const QVariant & value)533 void MainWindow::setting_changed_in_quest(
534 const QString& key, const QVariant& value
535 ) {
536
537 Settings settings;
538 if (key == "quest_fullscreen") {
539 settings.setValue(key, value);
540 update_fullscreen_action();
541 }
542 }
543
544 /**
545 * @brief Slot called when the user wants to change the window zoom.
546 * @param zoom The new zoom factor.
547 */
set_zoom_requested(int zoom)548 void MainWindow::set_zoom_requested(int zoom) {
549
550 if (!quest_runner.is_started()) {
551 return;
552 }
553
554 QString command = QString("local w, h = sol.video.get_quest_size(); sol.video.set_window_size(w * %1, h * %2)").arg(zoom).arg(zoom);
555 ui.console->execute_command(command);
556 }
557
558 }
559