1 /*
2  * Copyright (C) 2014-2018 Christopho, Solarus - http://www.solarus-games.org
3  *
4  * Solarus Quest Editor 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 Quest Editor 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 "widgets/gui_tools.h"
18 #include "widgets/map_view.h"
19 #include "widgets/shader_editor.h"
20 #include "widgets/text_editor.h"
21 #include "editor_exception.h"
22 #include "editor_settings.h"
23 #include "map_model.h"
24 #include "quest.h"
25 #include "shader_model.h"
26 #include "sprite_model.h"
27 #include <QFileDialog>
28 #include <QInputDialog>
29 #include <QUndoStack>
30 
31 namespace SolarusEditor {
32 
33 namespace {
34 
35 /**
36  * @brief Parent class of all undoable commands of the shader editor.
37  */
38 class ShaderEditorCommand : public QUndoCommand {
39 
40 public:
41 
ShaderEditorCommand(ShaderEditor & editor,const QString & text)42   ShaderEditorCommand(ShaderEditor& editor, const QString& text) :
43     QUndoCommand(text),
44     editor(editor) {
45   }
46 
get_editor() const47   ShaderEditor& get_editor() const {
48     return editor;
49   }
50 
get_shader() const51   ShaderModel& get_shader() const {
52     return editor.get_shader();
53   }
54 
55 private:
56 
57   ShaderEditor& editor;
58 
59 };
60 
61 /**
62  * @brief Changing the scaling factor of a shader.
63  */
64 class SetScalingFactorCommand : public ShaderEditorCommand {
65 
66 public:
SetScalingFactorCommand(ShaderEditor & editor,double scaling_factor)67   SetScalingFactorCommand(ShaderEditor& editor, double scaling_factor) :
68     ShaderEditorCommand(editor, ShaderEditor::tr("Scaling factor")),
69     before(get_shader().get_scaling_factor()),
70     after(scaling_factor) {
71   }
72 
undo()73   void undo() override {
74     get_shader().set_scaling_factor(before);
75   }
76 
redo()77   void redo() override {
78     get_shader().set_scaling_factor(after);
79   }
80 
81 private:
82   double before, after;
83 };
84 
85 /**
86  * @brief Changing a source file in a shader program.
87  */
88 class SetGlslFileCommand : public ShaderEditorCommand {
89 
90 public:
SetGlslFileCommand(ShaderEditor & editor,WhichGlslEditor which,const QString & file_name)91   SetGlslFileCommand(ShaderEditor& editor, WhichGlslEditor which, const QString& file_name) :
92     ShaderEditorCommand(editor, ShaderEditor::tr("Shader file")),
93     which(which),
94     before(),
95     after(file_name) {
96 
97     switch (which) {
98     case WhichGlslEditor::VERTEX_EDITOR:
99       before = get_shader().get_vertex_file();
100       break;
101     case WhichGlslEditor::FRAGMENT_EDITOR:
102       before = get_shader().get_fragment_file();
103       break;
104     }
105   }
106 
undo()107   void undo() override {
108     switch (which) {
109     case WhichGlslEditor::VERTEX_EDITOR:
110       get_shader().set_vertex_file(before);
111       break;
112     case WhichGlslEditor::FRAGMENT_EDITOR:
113       get_shader().set_fragment_file(before);
114       break;
115     }
116   }
117 
redo()118   void redo() override {
119     switch (which) {
120     case WhichGlslEditor::VERTEX_EDITOR:
121       get_shader().set_vertex_file(after);
122       break;
123     case WhichGlslEditor::FRAGMENT_EDITOR:
124       get_shader().set_fragment_file(after);
125       break;
126     }
127   }
128 
129 private:
130   WhichGlslEditor which;
131   QString before, after;
132 };
133 
134 }  // Anonymous namespace.
135 
136 /**
137  * @brief Creates a shader editor.
138  * @param quest The quest containing the file.
139  * @param path Path of the shader data file to open.
140  * @param parent The parent object or nullptr.
141  * @throws EditorException If the file could not be opened.
142  */
ShaderEditor(Quest & quest,const QString & path,QWidget * parent)143 ShaderEditor::ShaderEditor(Quest& quest, const QString& path, QWidget* parent) :
144   Editor(quest, path, parent),
145   shader(nullptr),
146   quest(quest),
147   vertex_editor(nullptr),
148   fragment_editor(nullptr) {
149 
150   ui.setupUi(this);
151 
152   // Get the shader.
153   ResourceType resource_type;
154   QString shader_id;
155   quest.check_exists(path);
156   if (!quest.is_resource_element(path, resource_type, shader_id) ||
157       resource_type != ResourceType::SHADER) {
158     throw EditorException(tr("File '%1' is not a shader").arg(path));
159   }
160   this->shader_id = shader_id;
161 
162   // Editor properties.
163   set_title(tr("Shader %1").arg(get_file_name_without_extension()));
164   set_icon(QIcon(":/images/icon_resource_shader.png"));
165   set_close_confirm_message(
166         tr("Shader '%1' has been modified. Save changes?").arg(shader_id));
167   set_zoom_supported(true);
168   ui.shader_previewer->set_view_settings(get_view_settings());
169 
170   // Open the file.
171   shader = std::unique_ptr<ShaderModel>(new ShaderModel(quest, shader_id, this));
172   get_undo_stack().setClean();
173 
174   // Prepare the GUI.
175   ui.scaling_factor_check_box->setAttribute(Qt::WA_LayoutUsesWidgetRect);
176   ui.preview_mode_selector->setAttribute(Qt::WA_LayoutUsesWidgetRect);
177 
178   ui.preview_picture_page->layout()->setAlignment(ui.preview_picture_field_layout, Qt::AlignTop);
179   ui.preview_map_page->layout()->setAlignment(ui.preview_map_field, Qt::AlignTop);
180   ui.preview_map_field->set_resource_type(ResourceType::MAP);
181   ui.preview_map_field->set_quest(quest);
182   ui.preview_sprite_field->set_resource_type(ResourceType::SPRITE);
183   ui.preview_sprite_field->set_quest(quest);
184 
185   ui.vertex_file_check_box->setAttribute(Qt::WA_LayoutUsesWidgetRect);
186   ui.vertex_file_field->setAttribute(Qt::WA_LayoutUsesWidgetRect);
187   ui.vertex_shader_page->layout()->setAlignment(ui.vertex_file_check_box, Qt::AlignTop);
188   ui.fragment_file_check_box->setAttribute(Qt::WA_LayoutUsesWidgetRect);
189   ui.fragment_file_field->setAttribute(Qt::WA_LayoutUsesWidgetRect);
190   ui.fragment_shader_page->layout()->setAlignment(ui.fragment_file_check_box, Qt::AlignTop);
191 
192   const int side_width = 300;
193   ui.main_splitter->setSizes({ side_width, width() - side_width });
194   ui.main_splitter->setStretchFactor(0, 0);
195   ui.main_splitter->setStretchFactor(1, 1);
196   const int preview_height = 400;
197   ui.right_splitter->setSizes({ preview_height, height() - preview_height });
198   ui.right_splitter->setStretchFactor(0, 1);
199   ui.right_splitter->setStretchFactor(1, 1);
200 
201   ui.shader_previewer->set_model(shader.get());
202 
203   QString vertex_file = shader->get_vertex_file();
204   QString vertex_file_path = quest.get_shader_code_file_path(vertex_file);
205   if (!quest.exists(vertex_file_path) || !quest.is_shader_code_file(vertex_file_path)) {
206     vertex_file_path = QString();
207   }
208   vertex_editor = new TextEditor(
209         quest, vertex_file_path, this);
210   ui.vertex_editor_layout->addWidget(vertex_editor);
211   ui.vertex_editor_layout->removeItem(ui.vertex_editor_placeholder);
212 
213   QString fragment_file = shader->get_fragment_file();
214   QString fragment_file_path = quest.get_shader_code_file_path(fragment_file);
215   if (!quest.exists(fragment_file_path) || !quest.is_shader_code_file(fragment_file_path)) {
216     fragment_file_path = QString();
217   }
218   fragment_editor = new TextEditor(
219         quest, fragment_file_path, this);
220   ui.fragment_editor_layout->addWidget(fragment_editor);
221   ui.fragment_editor_layout->removeItem(ui.fragment_editor_placeholder);
222 
223   update();
224   reload_settings();
225 
226   // Make connections.
227   connect(&get_database(), &QuestDatabase::element_description_changed,
228           this, &ShaderEditor::update_description_to_gui);
229   connect(ui.description_field, &QLineEdit::editingFinished,
230           this, &ShaderEditor::set_description_from_gui);
231 
232   connect(shader.get(), &ShaderModel::scaling_factor_changed,
233           this, &ShaderEditor::update_scaling_factor_field);
234   connect(ui.scaling_factor_check_box, &QCheckBox::clicked,
235           this, &ShaderEditor::scaling_factor_check_box_changed);
236   connect(ui.scaling_factor_field, static_cast<void (QDoubleSpinBox::*)(double)>(&QDoubleSpinBox::valueChanged),
237           this, &ShaderEditor::scaling_factor_field_changed);
238 
239   connect(ui.preview_mode_selector, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
240           [this]() {
241     ui.shader_previewer->set_preview_mode(ui.preview_mode_selector->get_selected_value());
242   });
243 
244   connect(ui.preview_picture_radio, &QRadioButton::clicked,
245           this, &ShaderEditor::preview_radio_changed);
246   connect(ui.preview_picture_browse_button, &QToolButton::clicked,
247           this, &ShaderEditor::browse_preview_picture);
248   connect(ui.preview_map_radio, &QRadioButton::clicked,
249           this, &ShaderEditor::preview_radio_changed);
250   connect(ui.preview_map_field, static_cast<void (QComboBox::*)(int)>(&ResourceSelector::currentIndexChanged),
251           this, &ShaderEditor::update_preview_image);
252   connect(ui.preview_sprite_radio, &QRadioButton::clicked,
253           this, &ShaderEditor::preview_radio_changed);
254   connect(ui.preview_sprite_field, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
255           this, &ShaderEditor::preview_selected_sprite_changed);
256   connect(ui.preview_sprite_animation_field, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
257           this, &ShaderEditor::preview_sprite_animation_changed);
258   connect(ui.preview_sprite_direction_field, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged),
259           this, &ShaderEditor::update_preview_image);
260 
261   connect(ui.vertex_file_check_box, &QCheckBox::clicked, [this]() {
262     source_file_check_box_changed(WhichGlslEditor::VERTEX_EDITOR);
263   });
264   connect(ui.vertex_file_new_button, &QToolButton::clicked, [this]() {
265     new_source_file(WhichGlslEditor::VERTEX_EDITOR);
266   });
267   connect(ui.vertex_file_browse_button, &QToolButton::clicked, [this]() {
268     browse_source_file(WhichGlslEditor::VERTEX_EDITOR);
269   });
270   connect(ui.vertex_file_save_button, &QToolButton::clicked, [this]() {
271     save_source_file(WhichGlslEditor::VERTEX_EDITOR);
272   });
273   connect(shader.get(), &ShaderModel::vertex_file_changed, [this]() {
274     update_source_editor_tab(WhichGlslEditor::VERTEX_EDITOR);
275   });
276   connect(&vertex_editor->get_undo_stack(), &QUndoStack::cleanChanged, [this](bool clean) {
277     source_editor_modification_state_changed(WhichGlslEditor::VERTEX_EDITOR, clean);
278   });
279 
280   connect(ui.fragment_file_check_box, &QCheckBox::clicked, [this]() {
281     source_file_check_box_changed(WhichGlslEditor::FRAGMENT_EDITOR);
282   });
283   connect(ui.fragment_file_new_button, &QToolButton::clicked, [this]() {
284     new_source_file(WhichGlslEditor::FRAGMENT_EDITOR);
285   });
286   connect(ui.fragment_file_browse_button, &QToolButton::clicked, [this]() {
287     browse_source_file(WhichGlslEditor::FRAGMENT_EDITOR);
288   });
289   connect(ui.fragment_file_save_button, &QToolButton::clicked, [this]() {
290     save_source_file(WhichGlslEditor::FRAGMENT_EDITOR);
291   });
292   connect(shader.get(), &ShaderModel::fragment_file_changed, [this]() {
293     update_source_editor_tab(WhichGlslEditor::FRAGMENT_EDITOR);
294   });
295   connect(&fragment_editor->get_undo_stack(), &QUndoStack::cleanChanged, [this](bool clean) {
296     source_editor_modification_state_changed(WhichGlslEditor::FRAGMENT_EDITOR, clean);
297   });
298 
299   connect(ui.shader_previewer, &ShaderPreviewer::shader_compilation_started,
300           this, &ShaderEditor::clear_console);
301   connect(ui.shader_previewer, &ShaderPreviewer::shader_error,
302         [this](const QString& message) {
303     emit log_message_to_console("Error", message);
304   });
305   connect(ui.shader_previewer, &ShaderPreviewer::shader_warning,
306           [this](const QString& message) {
307     emit log_message_to_console("Warning", message);
308   });
309 }
310 
311 /**
312  * @brief Destructor.
313  */
~ShaderEditor()314 ShaderEditor::~ShaderEditor() {
315 }
316 
317 /**
318  * @brief Returns the shader model being edited.
319  * @return The shader model.
320  */
get_shader()321 ShaderModel& ShaderEditor::get_shader() {
322   return *shader;
323 }
324 
325 /**
326  * @copydoc Editor::save
327  */
save()328 void ShaderEditor::save() {
329 
330   shader->save();
331   save_source_file(WhichGlslEditor::VERTEX_EDITOR);
332   save_source_file(WhichGlslEditor::FRAGMENT_EDITOR);
333 }
334 
335 /**
336  * @copydoc Editor::has_unsaved_changes
337  */
has_unsaved_changes() const338 bool ShaderEditor::has_unsaved_changes() const {
339 
340   return Editor::has_unsaved_changes() ||
341       vertex_editor->has_unsaved_changes() ||
342       fragment_editor->has_unsaved_changes();
343 }
344 
345 /**
346  * @copydoc Editor::reload_settings
347  */
reload_settings()348 void ShaderEditor::reload_settings() {
349 
350   EditorSettings settings;
351 
352   QString file = settings.get_value_string(EditorSettings::shader_preview_picture_file);
353   ui.preview_picture_field->setText(file);
354   QString map_id = settings.get_value_string(EditorSettings::shader_preview_map_id);
355   ui.preview_map_field->set_selected_id(map_id);
356   QString sprite_id = settings.get_value_string(EditorSettings::shader_preview_sprite_id);
357   QString animation = settings.get_value_string(EditorSettings::shader_preview_sprite_animation);
358   int direction = settings.get_value_int(EditorSettings::shader_preview_sprite_direction);
359   ui.preview_sprite_field->set_selected_id(sprite_id);
360   preview_selected_sprite_changed();
361   ui.preview_sprite_animation_field->setCurrentText(animation);
362   preview_sprite_animation_changed();
363   ui.preview_sprite_direction_field->setValue(direction);
364 
365   QString preview_type = settings.get_value_string(EditorSettings::shader_preview_type);
366   if (preview_type.isEmpty() || preview_type == "picture") {
367     ui.preview_picture_radio->setChecked(true);
368     ui.preview_file_widget->setCurrentWidget(ui.preview_picture_page);
369   }
370   else if (preview_type == "map") {
371     ui.preview_map_radio->setChecked(true);
372     ui.preview_file_widget->setCurrentWidget(ui.preview_map_page);
373   }
374   else if (preview_type == "sprite") {
375     ui.preview_sprite_radio->setChecked(true);
376     ui.preview_file_widget->setCurrentWidget(ui.preview_sprite_page);
377   }
378   update_preview_image();
379 }
380 
381 /**
382  * @brief Updates everything in the GUI.
383  */
update()384 void ShaderEditor::update() {
385 
386   update_shader_id_field();
387   update_description_to_gui();
388   update_scaling_factor_field();
389   update_source_editor_tab(WhichGlslEditor::VERTEX_EDITOR);
390   update_source_editor_tab(WhichGlslEditor::FRAGMENT_EDITOR);
391 }
392 
393 /**
394  * @brief Updates the shader id displaying.
395  */
update_shader_id_field()396 void ShaderEditor::update_shader_id_field() {
397 
398   ui.shader_id_field->setText(shader_id);
399 }
400 
401 /**
402  * @brief Updates the content of the shader description text edit.
403  */
update_description_to_gui()404 void ShaderEditor::update_description_to_gui() {
405 
406   QString description = get_database().get_description(ResourceType::SHADER, shader_id);
407   if (ui.description_field->text() != description) {
408     ui.description_field->setText(description);
409   }
410 }
411 
412 /**
413  * @brief Modifies the shader description in the quest database with
414  * the new text entered by the user.
415  *
416  * If the new description is invalid, an error dialog is shown.
417  */
set_description_from_gui()418 void ShaderEditor::set_description_from_gui() {
419 
420   QString description = ui.description_field->text();
421   if (description == get_database().get_description(ResourceType::SHADER, shader_id)) {
422     return;
423   }
424 
425   if (description.isEmpty()) {
426     GuiTools::error_dialog(tr("Invalid description"));
427     update_description_to_gui();
428     return;
429   }
430 
431   const bool was_blocked = blockSignals(true);
432   try {
433     get_database().set_description(ResourceType::SHADER, shader_id, description);
434     get_database().save();
435   }
436   catch (const EditorException& ex) {
437     ex.print_message();
438   }
439   update_description_to_gui();
440   blockSignals(was_blocked);
441 }
442 
443 /**
444  * @brief Updates the scaling factor from data to the GUI.
445  */
update_scaling_factor_field()446 void ShaderEditor::update_scaling_factor_field() {
447 
448   double scaling_factor = get_shader().get_scaling_factor();
449   if (scaling_factor == 0.0) {
450     ui.scaling_factor_check_box->setChecked(false);
451     ui.scaling_factor_field->setEnabled(false);
452   }
453   else {
454     ui.scaling_factor_check_box->setChecked(true);
455     ui.scaling_factor_field->setEnabled(true);
456     ui.scaling_factor_field->setValue(scaling_factor);
457   }
458 }
459 
460 /**
461  * @brief Called when the user changes the scaling factor check box.
462  */
scaling_factor_check_box_changed()463 void ShaderEditor::scaling_factor_check_box_changed() {
464 
465   double old_scaling_factor = get_shader().get_scaling_factor();
466   double new_scaling_factor = 0.0;
467   if (ui.scaling_factor_check_box->isChecked()) {
468     new_scaling_factor = ui.scaling_factor_field->value();;
469   }
470 
471   if (new_scaling_factor != old_scaling_factor) {
472     try_command(new SetScalingFactorCommand(*this, new_scaling_factor));
473   }
474 }
475 
476 /**
477  * @brief Called when the user changes the scaling factor value.
478  */
scaling_factor_field_changed()479 void ShaderEditor::scaling_factor_field_changed() {
480 
481   double scaling_factor = ui.scaling_factor_field->value();
482   if (scaling_factor == get_shader().get_scaling_factor()) {
483     return;
484   }
485   try_command(new SetScalingFactorCommand(*this, scaling_factor));
486 }
487 
488 /**
489  * @brief Returns the specified GLSL code editor widget.
490  * @param which Which GLSL editor to return.
491  * @return The corresponding code editor.
492  */
get_glsl_editor(WhichGlslEditor which)493 TextEditor* ShaderEditor::get_glsl_editor(WhichGlslEditor which) {
494 
495   TextEditor* glsl_editor = nullptr;
496   switch (which) {
497   case WhichGlslEditor::VERTEX_EDITOR:
498     glsl_editor = vertex_editor;
499     break;
500   case WhichGlslEditor::FRAGMENT_EDITOR:
501     glsl_editor = fragment_editor;
502     break;
503   }
504 
505   return glsl_editor;
506 }
507 
508 /**
509  * @brief Updates a GLSL editor tab from the data.
510  * @param which The GLSL editor tab to update.
511  */
update_source_editor_tab(WhichGlslEditor which)512 void ShaderEditor::update_source_editor_tab(WhichGlslEditor which) {
513 
514   if (shader == nullptr) {
515     return;
516   }
517 
518   QString file_name;
519   QLineEdit* file_name_field = nullptr;
520   QCheckBox* check_box = nullptr;
521   QStackedWidget* stacked_widget = nullptr;
522   QToolButton* save_button = nullptr;
523   TextEditor* glsl_editor = get_glsl_editor(which);
524 
525   switch (which) {
526 
527   case WhichGlslEditor::VERTEX_EDITOR:
528     file_name = shader->get_vertex_file();
529     file_name_field = ui.vertex_file_field;
530     check_box = ui.vertex_file_check_box;
531     stacked_widget = ui.vertex_editor_stacked_widget;
532     save_button = ui.vertex_file_save_button;
533     break;
534 
535   case WhichGlslEditor::FRAGMENT_EDITOR:
536     file_name = shader->get_fragment_file();
537     file_name_field = ui.fragment_file_field;
538     check_box = ui.fragment_file_check_box;
539     stacked_widget = ui.fragment_editor_stacked_widget;
540     save_button = ui.fragment_file_save_button;
541     break;
542   }
543 
544   QString path = get_quest().get_shader_code_file_path(file_name);
545   if (!file_name.isEmpty() &&
546       quest.exists(path) &&
547       quest.is_shader_code_file(path)) {
548     file_name_field->setText(file_name);
549     stacked_widget->setCurrentIndex(1);  // Normal page.
550     get_glsl_editor(which)->set_file_path(path);
551     glsl_editor->setEnabled(true);
552     save_button->setEnabled(true);
553     check_box->setChecked(true);
554   }
555   else {
556     file_name_field->setText(QString());
557     stacked_widget->setCurrentIndex(0);  // Empty page.
558     glsl_editor->setEnabled(false);
559     save_button->setEnabled(false);
560     check_box->setChecked(false);
561   }
562 }
563 
564 /**
565  * @brief Called when the user clicks a source file check box.
566  * @param which The GLSL editor whose check box has changed.
567  */
source_file_check_box_changed(WhichGlslEditor which)568 void ShaderEditor::source_file_check_box_changed(WhichGlslEditor which) {
569 
570   QCheckBox* check_box = nullptr;
571   QString* last_file_name = nullptr;
572   QString current_file_name;
573   QStackedWidget* stacked_widget = nullptr;
574 
575   switch (which) {
576 
577   case WhichGlslEditor::VERTEX_EDITOR:
578     last_file_name = &last_vertex_file;
579     current_file_name = shader->get_vertex_file();
580     check_box = ui.vertex_file_check_box;
581     stacked_widget = ui.vertex_editor_stacked_widget;
582     break;
583 
584   case WhichGlslEditor::FRAGMENT_EDITOR:
585     last_file_name = &last_fragment_file;
586     current_file_name = shader->get_fragment_file();
587     check_box = ui.fragment_file_check_box;
588     stacked_widget = ui.fragment_editor_stacked_widget;
589     break;
590   }
591 
592   const bool checked = check_box->isChecked();
593   if (checked) {
594     if (current_file_name.isEmpty() &&
595        !last_file_name->isEmpty()) {
596       // Use the previous file name.
597       try_command(new SetGlslFileCommand(*this, which, *last_file_name));
598     }
599     else {
600       stacked_widget->setCurrentIndex(1);  // Normal page.
601     }
602   }
603   else {
604     if (!current_file_name.isEmpty()) {
605       // Remove the value but remember it.
606       *last_file_name = current_file_name;
607       try_command(new SetGlslFileCommand(*this, which, ""));
608     }
609     else {
610       stacked_widget->setCurrentIndex(0);  // Empty page.
611     }
612   }
613 }
614 
615 /**
616  * @brief Lets the user choose a GLSL source file to create.
617  * @param which The GLSL editor where to create a file.
618  */
new_source_file(WhichGlslEditor which)619 void ShaderEditor::new_source_file(WhichGlslEditor which) {
620 
621   if (shader == nullptr) {
622     return;
623   }
624 
625   try {
626     TextEditor* glsl_editor = get_glsl_editor(which);
627     if (!glsl_editor->confirm_before_closing()) {
628       return;
629     }
630 
631     QString suffix;
632     QString content;
633     switch (which) {
634     case WhichGlslEditor::VERTEX_EDITOR:
635       suffix = ".vert.glsl";
636       content = ShaderModel::get_default_vertex_source();
637       break;
638     case WhichGlslEditor::FRAGMENT_EDITOR:
639       suffix = ".frag.glsl";
640       content = ShaderModel::get_default_fragment_source();
641       break;
642     }
643 
644     bool ok = false;
645     QString file_name = QInputDialog::getText(
646           this,
647           tr("New GLSL file"),
648           tr("File name:"),
649           QLineEdit::Normal,
650           shader_id + suffix,
651           &ok);
652 
653     if (ok) {
654       // Automatically add .glsl extension if not present.
655       if (!file_name.contains(".")) {
656         file_name = file_name + ".lua";
657       }
658       Quest::check_valid_file_name(file_name);
659       const QString& shaders_path = get_quest().get_resource_path(ResourceType::SHADER);
660       QString file_path = shaders_path + '/' + file_name;
661 
662       get_quest().create_file_from_string(file_path, content);
663 
664       file_name = file_name.right(shaders_path.size() + 1);
665       try_command(new SetGlslFileCommand(*this, which, file_name));
666     }
667   }
668   catch (const EditorException& ex) {
669     ex.show_dialog();
670   }
671 }
672 
673 /**
674  * @brief Lets the user choose a GLSL code file to open.
675  * @param which The GLSL editor where to browse a file.
676  */
browse_source_file(WhichGlslEditor which)677 void ShaderEditor::browse_source_file(WhichGlslEditor which) {
678 
679   if (shader == nullptr) {
680     return;
681   }
682 
683   try {
684     if (get_glsl_editor(which)->confirm_before_closing()) {
685       const QString& directory = QFileInfo(get_file_path()).dir().path();
686       QString file_name = QFileDialog::getOpenFileName(
687           this,
688           tr("Open a GLSL file"),
689           directory,
690           tr("GLSL shader file (*.glsl)")
691       );
692 
693       if (!file_name.isEmpty()) {
694         const QString& shaders_path = get_quest().get_resource_path(ResourceType::SHADER);
695         if (!file_name.startsWith(shaders_path)) {
696           throw EditorException(tr("Shader GLSL files must be in the shaders directory"));
697         }
698 
699         file_name = file_name.right(file_name.size() - shaders_path.size() - 1);
700         if (!file_name.isEmpty()) {
701           try_command(new SetGlslFileCommand(*this, which, file_name));
702         }
703       }
704     }
705   }
706   catch (const EditorException& ex) {
707     ex.show_dialog();
708   }
709 }
710 
711 /**
712  * @brief Saves the fragment shader GLSL file.
713  * @param which The GLSL editor to save.
714  */
save_source_file(WhichGlslEditor which)715 void ShaderEditor::save_source_file(WhichGlslEditor which) {
716 
717   if (shader == nullptr) {
718     return;
719   }
720 
721   TextEditor* glsl_editor = get_glsl_editor(which);
722   glsl_editor->save();
723   glsl_editor->get_undo_stack().setClean();
724 }
725 
726 /**
727  * @brief Called when the is-modified state of a GLSL editor has changed.
728  * @param which The GLSL editor whose file has changed.
729  * @param clean @c true if the file is now clean, @c false if it is now
730  * modified.
731  */
source_editor_modification_state_changed(WhichGlslEditor which,bool clean)732 void ShaderEditor::source_editor_modification_state_changed(WhichGlslEditor which, bool clean) {
733 
734   QString title;
735   int tab_index = 0;
736   switch (which) {
737   case WhichGlslEditor::VERTEX_EDITOR:
738     title = "Vertex shader";
739     tab_index = 0;
740     break;
741   case WhichGlslEditor::FRAGMENT_EDITOR:
742     title = "Fragment shader";
743     tab_index = 1;
744     break;
745   }
746 
747   if (!clean) {
748     title += '*';
749   }
750   ui.shader_files_tab_widget->setTabText(tab_index, title);
751 
752   if (clean) {
753     // A GLSL file was just saved: tell the preview to update.
754     ui.shader_previewer->on_source_changed();
755   }
756 }
757 
758 /**
759  * @brief Shows the appropriate preview settings page depending on the radio
760  * button checked.
761  */
preview_radio_changed()762 void ShaderEditor::preview_radio_changed() {
763 
764   if (ui.preview_picture_radio->isChecked()) {
765     ui.preview_file_widget->setCurrentWidget(ui.preview_picture_page);
766   }
767   else if (ui.preview_map_radio->isChecked()) {
768     ui.preview_file_widget->setCurrentWidget(ui.preview_map_page);
769   }
770   else if (ui.preview_sprite_radio->isChecked()) {
771     ui.preview_file_widget->setCurrentWidget(ui.preview_sprite_page);
772     preview_selected_sprite_changed();
773   }
774 
775   update_preview_image();
776 }
777 
778 /**
779  * @brief Sets up the sprite animation and direction fields according
780  * to the selected sprite.
781  */
preview_selected_sprite_changed()782 void ShaderEditor::preview_selected_sprite_changed() {
783 
784   ui.preview_sprite_animation_field->clear();
785 
786   const QString& sprite_id = ui.preview_sprite_field->get_selected_id();
787   if (sprite_id.isEmpty() ||
788       !quest.get_database().exists(ResourceType::SPRITE, sprite_id) ||
789       !quest.exists(quest.get_sprite_path(sprite_id))) {
790     update_preview_image();
791     return;
792   }
793 
794   SpriteModel sprite(get_quest(), sprite_id);
795   ui.preview_sprite_animation_field->addItems(sprite.get_animation_names());
796   ui.preview_sprite_animation_field->setCurrentText(sprite.get_default_animation_name());
797 }
798 
799 /**
800  * @brief Called when the user selects a sprite animation.
801  */
preview_sprite_animation_changed()802 void ShaderEditor::preview_sprite_animation_changed() {
803 
804   const QString& sprite_id = ui.preview_sprite_field->get_selected_id();
805   if (!sprite_id.isEmpty() &&
806       quest.get_database().exists(ResourceType::SPRITE, sprite_id) &&
807       quest.exists(quest.get_sprite_path(sprite_id))) {
808     SpriteModel sprite(get_quest(), sprite_id);
809     const QString& animation = ui.preview_sprite_animation_field->currentText();
810     int num_directions = sprite.get_animation_num_directions({animation, 0});
811     if (num_directions > 0) {
812       ui.preview_sprite_direction_field->setMaximum(num_directions - 1);
813     }
814   }
815 }
816 
817 /**
818  * @brief Lets the user choose a PNG file for the preview.
819  */
browse_preview_picture()820 void ShaderEditor::browse_preview_picture() {
821 
822   if (shader == nullptr) {
823     return;
824   }
825 
826   try {
827     const QString& directory = get_quest().get_resource_path(ResourceType::SPRITE);
828     QString file_name = QFileDialog::getOpenFileName(
829         this,
830         tr("Open a PNG picture"),
831         directory,
832         tr("PNG file (*.png)")
833     );
834 
835     if (file_name.isEmpty()) {
836       return;
837     }
838 
839     ui.preview_picture_field->setText(file_name);
840     update_preview_image();
841   }
842   catch (const EditorException& ex) {
843     ex.show_dialog();
844   }
845 }
846 
847 /**
848  * @brief Updates the image to be displayed in the preview widget.
849  */
update_preview_image()850 void ShaderEditor::update_preview_image() {
851 
852   QImage image;
853   EditorSettings settings;
854 
855   const QString& picture_file_name = ui.preview_picture_field->text();
856   if (!picture_file_name.isEmpty()) {
857     settings.set_value(EditorSettings::shader_preview_picture_file, picture_file_name);
858   }
859   const QString& map_id = ui.preview_map_field->get_selected_id();
860   if (!map_id.isEmpty()) {
861     settings.set_value(EditorSettings::shader_preview_map_id, map_id);
862   }
863   const QString& sprite_id = ui.preview_sprite_field->get_selected_id();
864   if (!sprite_id.isEmpty()) {
865     settings.set_value(EditorSettings::shader_preview_sprite_id, sprite_id);
866   }
867 
868   if (ui.preview_picture_radio->isChecked()) {
869     if (!picture_file_name.isEmpty()) {
870       image = QImage(picture_file_name);
871     }
872     settings.set_value(EditorSettings::shader_preview_type, "picture");
873   }
874   else if (ui.preview_map_radio->isChecked()) {
875     if (!map_id.isEmpty()) {
876       MapModel map(get_quest(), map_id);
877       MapView view;
878       view.set_map(&map);
879       image = view.export_to_image();
880     }
881     settings.set_value(EditorSettings::shader_preview_type, "map");
882   }
883   else if (ui.preview_sprite_radio->isChecked()) {
884     if (!sprite_id.isEmpty() &&
885         quest.get_database().exists(ResourceType::SPRITE, sprite_id) &&
886         quest.exists(quest.get_sprite_path(sprite_id))) {
887       SpriteModel sprite(get_quest(), sprite_id);
888       QString animation = ui.preview_sprite_animation_field->currentText();
889       int direction = ui.preview_sprite_direction_field->value();
890       if (!sprite.animation_exists(animation)) {
891         animation = sprite.get_default_animation_name();
892       }
893       if (direction < 0 || direction >= sprite.get_animation_num_directions({animation, 0})) {
894         direction = 0;
895       }
896       QPixmap pixmap = sprite.get_direction_first_frame({ animation, direction });
897       image = pixmap.toImage();
898       settings.set_value(EditorSettings::shader_preview_sprite_animation, animation);
899       settings.set_value(EditorSettings::shader_preview_sprite_direction, direction);
900     }
901     settings.set_value(EditorSettings::shader_preview_type, "sprite");
902   }
903 
904   ui.shader_previewer->set_preview_image(image);
905 }
906 
907 }
908