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