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/strings_editor.h"
19 #include "widgets/new_string_dialog.h"
20 #include "widgets/change_string_key_dialog.h"
21 #include "editor_exception.h"
22 #include "quest.h"
23 #include "strings_model.h"
24 #include <QUndoStack>
25 #include <QMessageBox>
26 #include <cmath>
27
28 namespace SolarusEditor {
29
30 namespace {
31
32 /**
33 * @brief Parent class of all undoable commands of the strings editor.
34 */
35 class StringsEditorCommand : public QUndoCommand {
36
37 public:
38
StringsEditorCommand(StringsEditor & editor,const QString & text)39 StringsEditorCommand(StringsEditor& editor, const QString& text) :
40 QUndoCommand(text),
41 editor(editor) {
42 }
43
get_editor() const44 StringsEditor& get_editor() const {
45 return editor;
46 }
47
get_model() const48 StringsModel& get_model() const {
49 return editor.get_model();
50 }
51
52 private:
53
54 StringsEditor& editor;
55
56 };
57
58 /**
59 * @brief Create a string.
60 */
61 class CreateStringCommand : public StringsEditorCommand {
62
63 public:
64
CreateStringCommand(StringsEditor & editor,const QString & key,const QString & value="")65 CreateStringCommand(
66 StringsEditor& editor, const QString& key, const QString& value = "") :
67 StringsEditorCommand(editor, StringsEditor::tr("Create string")),
68 key(key),
69 value(value) {
70 }
71
undo()72 virtual void undo() override {
73
74 get_model().delete_string(key);
75 }
76
redo()77 virtual void redo() override {
78
79 get_model().create_string(key, value);
80 get_model().set_selected_key(key);
81 }
82
83 private:
84
85 QString key;
86 QString value;
87 };
88
89 /**
90 * @brief Duplicate string(s).
91 */
92 class DuplicateStringsCommand : public StringsEditorCommand {
93
94 public:
95
DuplicateStringsCommand(StringsEditor & editor,const QString & prefix,const QString & new_prefix)96 DuplicateStringsCommand(
97 StringsEditor& editor, const QString& prefix, const QString& new_prefix) :
98 StringsEditorCommand(editor, StringsEditor::tr("Duplicate strings")),
99 prefix(prefix),
100 new_prefix(new_prefix) {
101 }
102
undo()103 virtual void undo() override {
104
105 get_model().delete_prefix(new_prefix);
106 }
107
redo()108 virtual void redo() override {
109
110 get_model().duplicate_strings(prefix, new_prefix);
111 get_model().set_selected_key(new_prefix);
112 }
113
114 private:
115
116 QString prefix;
117 QString new_prefix;
118 };
119
120 /**
121 * @brief Change string key.
122 */
123 class SetStringKeyCommand : public StringsEditorCommand {
124
125 public:
126
SetStringKeyCommand(StringsEditor & editor,const QString & key,const QString & new_key)127 SetStringKeyCommand(
128 StringsEditor& editor, const QString& key, const QString& new_key) :
129 StringsEditorCommand(editor, StringsEditor::tr("Change string key")),
130 old_key(key),
131 new_key(new_key) {
132 }
133
undo()134 virtual void undo() override {
135
136 get_model().set_string_key(new_key, old_key);
137 get_model().set_selected_key(old_key);
138 }
139
redo()140 virtual void redo() override {
141
142 get_model().set_string_key(old_key, new_key);
143 get_model().set_selected_key(new_key);
144 }
145
146 private:
147
148 QString old_key;
149 QString new_key;
150 };
151
152 /**
153 * @brief Change string key.
154 */
155 class SetKeyPrefixCommand : public StringsEditorCommand {
156
157 public:
158
SetKeyPrefixCommand(StringsEditor & editor,const QString & olf_prefix,const QString & new_prefix)159 SetKeyPrefixCommand(
160 StringsEditor& editor, const QString& olf_prefix,
161 const QString& new_prefix) :
162 StringsEditorCommand(editor, StringsEditor::tr("Change string key prefix")),
163 old_prefix(olf_prefix),
164 new_prefix(new_prefix) {
165 }
166
undo()167 virtual void undo() override {
168
169 for (const auto& pair : edited_keys) {
170 get_model().set_string_key(pair.second, pair.first);
171 }
172 if (!edited_keys.isEmpty()) {
173 get_model().set_selected_key(edited_keys.front().first);
174 }
175 }
176
redo()177 virtual void redo() override {
178
179 edited_keys = get_model().set_string_key_prefix(old_prefix, new_prefix);
180 if (!edited_keys.isEmpty()) {
181 get_model().set_selected_key(edited_keys.front().second);
182 }
183 }
184
185 private:
186
187 QString old_prefix;
188 QString new_prefix;
189 QList<QPair<QString, QString>> edited_keys;
190 };
191
192 /**
193 * @brief Delete a string.
194 */
195 class DeleteStringCommand : public StringsEditorCommand {
196
197 public:
198
DeleteStringCommand(StringsEditor & editor,const QString & key)199 DeleteStringCommand(StringsEditor& editor, const QString& key) :
200 StringsEditorCommand(editor, StringsEditor::tr("Delete string")),
201 key(key),
202 value(get_model().get_string(key)) {
203 }
204
undo()205 virtual void undo() override {
206
207 get_model().create_string(key, value);
208 get_model().set_selected_key(key);
209 }
210
redo()211 virtual void redo() override {
212
213 get_model().delete_string(key);
214 }
215
216 private:
217
218 QString key;
219 QString value;
220 };
221
222 /**
223 * @brief Delete several strings.
224 */
225 class DeleteStringsCommand : public StringsEditorCommand {
226
227 public:
228
DeleteStringsCommand(StringsEditor & editor,const QString & prefix)229 DeleteStringsCommand(StringsEditor& editor, const QString& prefix) :
230 StringsEditorCommand(editor, StringsEditor::tr("Delete strings")),
231 prefix(prefix) {
232 }
233
undo()234 virtual void undo() override {
235
236 for (const auto& pair : values) {
237 get_model().create_string(pair.first, pair.second);
238 }
239 if (!values.isEmpty()) {
240 get_model().set_selected_key(values.front().first);
241 }
242 }
243
redo()244 virtual void redo() override {
245
246 values = get_model().delete_prefix(prefix);
247 }
248
249 private:
250
251 QString prefix;
252 QList<QPair<QString, QString>> values;
253 };
254
255 /**
256 * @brief Change string value.
257 */
258 class SetStringValueCommand : public StringsEditorCommand {
259
260 public:
261
SetStringValueCommand(StringsEditor & editor,const QString & key,const QString & value)262 SetStringValueCommand(
263 StringsEditor& editor, const QString& key, const QString& value) :
264 StringsEditorCommand(editor, StringsEditor::tr("Change string value")),
265 key(key),
266 old_value(get_model().get_string(key)),
267 new_value(value) {
268 }
269
undo()270 virtual void undo() override {
271
272 get_model().set_string(key, old_value);
273 get_model().set_selected_key(key);
274 }
275
redo()276 virtual void redo() override {
277
278 get_model().set_string(key, new_value);
279 get_model().set_selected_key(key);
280 }
281
282 private:
283
284 QString key;
285 QString old_value;
286 QString new_value;
287 };
288
289 }
290
291 /**
292 * @brief Creates a strings editor.
293 * @param quest The quest containing the file.
294 * @param language_id Language id of the strings data file to open.
295 * @param parent The parent object or nullptr.
296 * @throws EditorException If the file could not be opened.
297 */
StringsEditor(Quest & quest,const QString & language_id,QWidget * parent)298 StringsEditor::StringsEditor(
299 Quest& quest, const QString& language_id, QWidget* parent) :
300 Editor(quest, quest.get_strings_path(language_id), parent),
301 language_id(language_id),
302 model(nullptr),
303 quest(quest) {
304
305 ui.setupUi(this);
306
307 // Open the file.
308 model = new StringsModel(quest, language_id, this);
309 get_undo_stack().setClean();
310
311 // Editor properties.
312 set_title(tr("Strings %1").arg(language_id));
313 set_icon(QIcon(":/images/icon_strings.png"));
314 set_close_confirm_message(
315 tr("Strings '%1' have been modified. Save changes?").arg(language_id));
316
317 // Prepare the gui.
318 ui.strings_tree_view->set_model(model);
319 ui.strings_tree_view->setColumnWidth(0, 300);
320 ui.strings_tree_view->setColumnHidden(2, true);
321
322 ui.translation_field->set_resource_type(ResourceType::LANGUAGE);
323 ui.translation_field->set_quest(quest);
324 ui.translation_field->remove_id(language_id);
325 ui.translation_field->add_special_value("", tr("<No language>"), 0);
326 ui.translation_field->set_selected_id("");
327
328 update();
329
330 // Make connections.
331 connect(&get_database(),
332 SIGNAL(element_description_changed(ResourceType, const QString&, const QString&)),
333 this, SLOT(update_description_to_gui()));
334 connect(ui.description_field, SIGNAL(editingFinished()),
335 this, SLOT(set_description_from_gui()));
336
337 connect(ui.create_button, SIGNAL(clicked()),
338 this, SLOT(create_string_requested()));
339 connect(ui.strings_tree_view, SIGNAL(create_string_requested()),
340 this, SLOT(create_string_requested()));
341
342 connect(ui.duplicate_button, SIGNAL(clicked()),
343 this, SLOT(duplicate_string_requested()));
344 connect(ui.strings_tree_view, SIGNAL(duplicate_string_requested()),
345 this, SLOT(duplicate_string_requested()));
346
347 connect(ui.set_key_button, SIGNAL(clicked()),
348 this, SLOT(change_string_key_requested()));
349 connect(ui.strings_tree_view, SIGNAL(set_string_key_requested()),
350 this, SLOT(change_string_key_requested()));
351
352 connect(ui.delete_button, SIGNAL(clicked()),
353 this, SLOT(delete_string_requested()));
354 connect(ui.strings_tree_view, SIGNAL(delete_string_requested()),
355 this, SLOT(delete_string_requested()));
356
357 connect(&model->get_selection_model(),
358 SIGNAL(selectionChanged(QItemSelection, QItemSelection)),
359 this, SLOT(update_selection()));
360
361 connect(model, SIGNAL(set_value_requested(QString,QString)),
362 this, SLOT(set_value_requested(QString,QString)));
363
364 connect(ui.translation_field, SIGNAL(activated(QString)),
365 this, SLOT(translation_selector_activated()));
366 connect(ui.translation_refresh_button, SIGNAL(clicked()),
367 this, SLOT(translation_refresh_requested()));
368 }
369
370 /**
371 * @brief Destructor.
372 */
~StringsEditor()373 StringsEditor::~StringsEditor() {
374 if (model != nullptr) {
375 delete model;
376 }
377 }
378
379 /**
380 * @brief Returns the strings model being edited.
381 * @return The strings model.
382 */
get_model()383 StringsModel& StringsEditor::get_model() {
384 return *model;
385 }
386
387 /**
388 * @copydoc Editor::save
389 */
save()390 void StringsEditor::save() {
391
392 model->save();
393 }
394
395 /**
396 * @brief Updates everything in the gui.
397 */
update()398 void StringsEditor::update() {
399
400 update_language_id_field();
401 update_description_to_gui();
402 update_selection();
403 }
404
405 /**
406 * @brief Updates the language id displaying.
407 */
update_language_id_field()408 void StringsEditor::update_language_id_field() {
409
410 ui.language_id_field->setText(language_id);
411 }
412
413 /**
414 * @brief Updates the content of the language description text edit.
415 */
update_description_to_gui()416 void StringsEditor::update_description_to_gui() {
417
418 QString description = get_database().get_description(
419 ResourceType::LANGUAGE, language_id);
420 if (ui.description_field->text() != description) {
421 ui.description_field->setText(description);
422 }
423 }
424
425 /**
426 * @brief Modifies the language description in the quest resource list with
427 * the new text entered by the user.
428 *
429 * If the new description is invalid, an error dialog is shown.
430 */
set_description_from_gui()431 void StringsEditor::set_description_from_gui() {
432
433 QString description = ui.description_field->text();
434 if (description == get_database().get_description(
435 ResourceType::LANGUAGE, language_id)) {
436 return;
437 }
438
439 if (description.isEmpty()) {
440 GuiTools::error_dialog(tr("Invalid description"));
441 update_description_to_gui();
442 return;
443 }
444
445 const bool was_blocked = blockSignals(true);
446 try {
447 get_database().set_description(
448 ResourceType::LANGUAGE, language_id, description);
449 get_database().save();
450 }
451 catch (const EditorException& ex) {
452 ex.print_message();
453 }
454 update_description_to_gui();
455 blockSignals(was_blocked);
456 }
457
458 /**
459 * @brief Updates the selection.
460 */
update_selection()461 void StringsEditor::update_selection() {
462
463 // Ensures that the selected item is visible in the tree view
464 QString key = model->get_selected_key();
465 if (!key.isEmpty()) {
466 ui.strings_tree_view->scrollTo(model->key_to_index(key));
467 }
468
469 // Update buttons
470 bool enable = !key.isEmpty() && model->prefix_exists(key);
471 ui.set_key_button->setEnabled(enable);
472 ui.duplicate_button->setEnabled(enable);
473 ui.delete_button->setEnabled(enable);
474 }
475
476 /**
477 * @brief Slot called when the user wants to create a string.
478 */
create_string_requested()479 void StringsEditor::create_string_requested() {
480
481 NewStringDialog dialog(model, model->get_selected_key(), "", this);
482
483 int result = dialog.exec();
484 if (result != QDialog::Accepted) {
485 return;
486 }
487
488 QString key = dialog.get_string_key();
489 QString value = dialog.get_string_value();
490 try_command(new CreateStringCommand(*this, key, value));
491 }
492
493 /**
494 * @brief Slot called when the user wants to duplicate string(s).
495 */
duplicate_string_requested()496 void StringsEditor::duplicate_string_requested() {
497
498 QString prefix = model->get_selected_key();
499 QString new_prefix = prefix + tr("_copy");
500 QString key;
501 if (!model->can_duplicate_strings(prefix, new_prefix, key)) {
502 GuiTools::error_dialog(tr("String '%1' already exists").arg(key));
503 return;
504 }
505
506 try_command(new DuplicateStringsCommand(*this, prefix, new_prefix));
507 }
508
509 /**
510 * @brief Slot called when the user wants to change the key of a string.
511 */
change_string_key_requested()512 void StringsEditor::change_string_key_requested() {
513
514 if (model->is_selection_empty()) {
515 return;
516 }
517
518 QString old_key = model->get_selected_key();
519 QStringList prefix_keys = model->get_keys(old_key);
520 bool exists = false;
521 bool is_prefix = false;
522
523 if (prefix_keys.size() > 0) {
524 if (model->string_exists(old_key)) {
525 exists = true;
526 is_prefix = prefix_keys.size() > 1;
527 } else {
528 is_prefix = true;
529 }
530 } else {
531 return;
532 }
533
534 ChangeStringKeyDialog dialog(
535 model, old_key, is_prefix, is_prefix && exists, this);
536
537 int result = dialog.exec();
538 if (result != QDialog::Accepted) {
539 return;
540 }
541
542 QString new_key = dialog.get_string_key();
543
544 if (new_key == old_key) {
545 return;
546 }
547
548 if (dialog.get_prefix()) {
549 try_command(new SetKeyPrefixCommand(*this, old_key, new_key));
550 } else {
551 try_command(new SetStringKeyCommand(*this, old_key, new_key));
552 }
553 }
554
555 /**
556 * @brief Slot called when the user wants to delete a string.
557 */
delete_string_requested()558 void StringsEditor::delete_string_requested() {
559
560 if (model->is_selection_empty()) {
561 return;
562 }
563
564 QString key = model->get_selected_key();
565 if (!model->prefix_exists(key)) {
566 return;
567 }
568
569 if (model->string_exists(key)) {
570 try_command(new DeleteStringCommand(*this, key));
571 return;
572 }
573
574 QMessageBox::StandardButton answer = QMessageBox::question(
575 this,
576 tr("Delete confirmation"),
577 tr("Do you really want to delete all strings prefixed by '%1'?").arg(key),
578 QMessageBox::Yes | QMessageBox::No);
579
580 if (answer != QMessageBox::Yes) {
581 return;
582 }
583
584 try_command(new DeleteStringsCommand(*this, key));
585 }
586
587 /**
588 * @brief Slot called when the user wants to set the value of a string.
589 * @param key The key of the string to edit.
590 * @param value The value to set.
591 */
set_value_requested(const QString & key,const QString & value)592 void StringsEditor::set_value_requested(
593 const QString& key, const QString& value) {
594
595 // If no exists, try to create.
596 if (!model->string_exists(key)) {
597 if (!value.isEmpty()) {
598 try_command(new CreateStringCommand(*this, key, value));
599 }
600 }
601 // Else,
602 else {
603 // If value is empty, try to remove.
604 if (value.isEmpty()) {
605 try_command(new DeleteStringCommand(*this, key));
606 }
607 // Else if the value is different, try to change.
608 else if (value != model->get_string(key)) {
609 try_command(new SetStringValueCommand(*this, key, value));
610 }
611 }
612 }
613
614 /**
615 * @brief Slot called when the user changes the language in the selector.
616 */
translation_selector_activated()617 void StringsEditor::translation_selector_activated() {
618
619 const QString& old_language_id = model->get_translation_id();
620 const QString& new_language_id = ui.translation_field->get_selected_id();
621 if (new_language_id == old_language_id) {
622 // No change.
623 return;
624 }
625
626 // If language id is empty.
627 if (new_language_id.isEmpty()) {
628 // Clear the translation and hide his column, return.
629 model->clear_translation();
630 ui.strings_tree_view->setColumnHidden(2, true);
631 ui.translation_refresh_button->setEnabled(false);
632 return;
633 }
634
635
636 // If the translation column is hide.
637 if (ui.strings_tree_view->isColumnHidden(2)) {
638 // Show the column.
639 ui.strings_tree_view->setColumnHidden(2, false);
640
641 // Resize value and translation columns.
642 int col_width = ui.strings_tree_view->columnWidth(0);
643 int width = ui.strings_tree_view->viewport()->width() - col_width;
644 col_width = std::floor(width / 2.0);
645 ui.strings_tree_view->setColumnWidth(1, col_width);
646 ui.strings_tree_view->setColumnWidth(2, col_width);
647 }
648
649 // Set the translation.
650 model->set_translation_id(new_language_id);
651 ui.translation_refresh_button->setEnabled(true);
652 }
653
654 /**
655 * @brief Slot called when the user wants to refresh the translation language.
656 */
translation_refresh_requested()657 void StringsEditor::translation_refresh_requested() {
658 if (ui.translation_field->get_selected_id().isEmpty()) {
659 return;
660 }
661 model->reload_translation();
662 }
663
664 }
665