1 /*
2 * SPDX-FileCopyrightText: 2012 Sebastian Gottfried <sebastiangottfried@web.de>
3 *
4 * SPDX-License-Identifier: GPL-2.0-or-later
5 */
6
7 #include "resourceeditor.h"
8
9 #include <QUuid>
10 #include <QFile>
11 #include <QFileDialog>
12 #include <QDir>
13 #include <QPointer>
14 #include <QTimer>
15 #include <QAbstractItemView>
16 #include <QStandardPaths>
17 #include <QUndoGroup>
18
19 #include <KToolBar>
20 #include <KActionCollection>
21 #include <KStandardAction>
22 #include <KLocalizedString>
23 #include <KMessageBox>
24
25 #include "core/dataindex.h"
26 #include "core/dataaccess.h"
27 #include "core/resource.h"
28 #include "core/course.h"
29 #include "core/lesson.h"
30 #include "core/keyboardlayout.h"
31 #include "core/resourcedataaccess.h"
32 #include "core/userdataaccess.h"
33 #include "models/resourcemodel.h"
34 #include "models/categorizedresourcesortfilterproxymodel.h"
35 #include "resourceeditorwidget.h"
36 #include "newresourceassistant.h"
37 #include "application.h"
38
ResourceEditor(QWidget * parent)39 ResourceEditor::ResourceEditor(QWidget *parent) :
40 KMainWindow(parent),
41 m_dataIndex(Application::dataIndex()),
42 m_resourceModel(new ResourceModel(this)),
43 m_categorizedResourceModel(new CategorizedResourceSortFilterProxyModel(this)),
44 m_currentResource(0),
45 m_backupResource(0),
46 m_undoGroup(new QUndoGroup(this)),
47 m_actionCollection(new KActionCollection(this)),
48 m_newResourceAction(KStandardAction::openNew(this, SLOT(newResource()), m_actionCollection)),
49 m_deleteResourceAction(new QAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18n("Delete"), this)),
50 m_undoAction(KStandardAction::undo(m_undoGroup, SLOT(undo()), m_actionCollection)),
51 m_redoAction(KStandardAction::redo(m_undoGroup, SLOT(redo()), m_actionCollection)),
52 m_importResourceAction(new QAction(QIcon::fromTheme(QStringLiteral("document-import")), i18n("Import"), this)),
53 m_exportResourceAction(new QAction(QIcon::fromTheme(QStringLiteral("document-export")), i18n("Export"), this)),
54 m_editorWidget(new ResourceEditorWidget(this)),
55 m_saveTimer(new QTimer(this))
56
57 {
58 m_resourceModel->setDataIndex(m_dataIndex);
59 m_categorizedResourceModel->setResourceModel(m_resourceModel);
60 m_categorizedResourceModel->setCategorizedModel(true);
61
62 const int unit = fontMetrics().height();
63 setMinimumSize(39 * unit, 28 * unit);
64 setCaption(i18nc("@title:window", "Course and Keyboard Layout Editor"));
65
66 m_newResourceAction->setToolTip(i18n("Create a new course or keyboard layout"));
67 m_deleteResourceAction->setEnabled(false);
68 m_deleteResourceAction->setToolTip(i18n("Delete the current selected course or keyboard layout"));
69 m_actionCollection->addAction(QStringLiteral("deleteResource"), m_deleteResourceAction);
70 connect(m_deleteResourceAction, &QAction::triggered, this, &ResourceEditor::deleteResource);
71 m_undoAction->setEnabled(false);
72 connect(m_undoGroup, &QUndoGroup::canUndoChanged, m_undoAction, &QAction::setEnabled);
73 connect(m_undoGroup, &QUndoGroup::undoTextChanged, this, &ResourceEditor::setUndoText);
74 m_redoAction->setEnabled(false);
75 connect(m_undoGroup, &QUndoGroup::canRedoChanged, m_redoAction, &QAction::setEnabled);
76 connect(m_undoGroup, &QUndoGroup::redoTextChanged, this, &ResourceEditor::setRedoText);
77 m_actionCollection->addAction(QStringLiteral("importResource"), m_importResourceAction);
78 m_importResourceAction->setToolTip(i18n("Import a course or keyboard layout from a file"));
79 connect(m_importResourceAction, &QAction::triggered, this, &ResourceEditor::importResource);
80 m_exportResourceAction->setToolTip(i18n("Export the current selected course or keyboard layout to a file"));
81 m_actionCollection->addAction(QStringLiteral("exportResource"), m_exportResourceAction);
82 connect(m_exportResourceAction, &QAction::triggered, this, &ResourceEditor::exportResource);
83 m_exportResourceAction->setEnabled(false);
84
85 toolBar()->addAction(m_newResourceAction);
86 toolBar()->addAction(m_deleteResourceAction);
87 toolBar()->addSeparator();
88 toolBar()->addAction(m_undoAction);
89 toolBar()->addAction(m_redoAction);
90 toolBar()->addSeparator();
91 toolBar()->addAction(m_importResourceAction);
92 toolBar()->addAction(m_exportResourceAction);
93
94 setCentralWidget(m_editorWidget);
95
96 m_editorWidget->setResourceModel(m_resourceModel);
97 m_editorWidget->setUndoGroup(m_undoGroup);
98
99 QAbstractItemView* const resourceView = m_editorWidget->resourceView();
100 resourceView->setModel(m_categorizedResourceModel);
101 connect(resourceView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &ResourceEditor::onResourceSelected);
102
103 selectFirstResource();
104
105 connect(m_editorWidget, &ResourceEditorWidget::resourceRestorationRequested, this, &ResourceEditor::restoreResourceBackup);
106 connect(m_editorWidget, &ResourceEditorWidget::resourceRestorationDismissed, this, &ResourceEditor::clearResourceBackup);
107
108 connect(m_saveTimer, &QTimer::timeout, this, &ResourceEditor::save);
109 m_saveTimer->setInterval(60000);
110 }
111
~ResourceEditor()112 ResourceEditor::~ResourceEditor()
113 {
114 if (m_backupResource)
115 {
116 delete m_backupResource;
117 }
118 }
119
closeEvent(QCloseEvent * event)120 void ResourceEditor::closeEvent(QCloseEvent* event)
121 {
122 save();
123 KMainWindow::closeEvent(event);
124 }
125
newResource()126 void ResourceEditor::newResource()
127 {
128 QPointer<NewResourceAssistant> assistant = new NewResourceAssistant(m_resourceModel, this);
129
130 if (assistant->exec() == QDialog::Accepted)
131 {
132 save();
133 Resource* resource = assistant->createResource();
134 if (Resource* dataIndexResource = storeResource(resource))
135 {
136 m_editorWidget->clearUndoStackForResource(dataIndexResource);
137 selectDataResource(dataIndexResource);
138 }
139 delete resource;
140 }
141
142 delete assistant;
143 }
144
deleteResource()145 void ResourceEditor::deleteResource()
146 {
147 Q_ASSERT(m_currentResource);
148
149 save();
150
151 // HACK: disable categorization temporarily as mitigation against kdelibs bug 303228
152 m_categorizedResourceModel->setCategorizedModel(false);
153
154 DataAccess dataAccess;
155 UserDataAccess userDataAccess;
156
157 if (DataIndexCourse* course = qobject_cast<DataIndexCourse*>(m_currentResource))
158 {
159 for (int i = 0; i < m_dataIndex->courseCount(); i++)
160 {
161 if (m_dataIndex->course(i) == course)
162 {
163 Course* backup = new Course();
164 if (!dataAccess.loadCourse(m_dataIndex->course(i), backup))
165 {
166 KMessageBox::error(this, i18n("Error while opening course"));
167 return;
168 }
169 if (!userDataAccess.deleteCourse(backup))
170 {
171 delete backup;
172 KMessageBox::error(this, i18n("Error while deleting course"));
173 return;
174 }
175 prepareResourceRestore(backup);
176 m_dataIndex->removeCourse(i);
177 }
178 }
179 }
180 else if (DataIndexKeyboardLayout* keyboardLayout = qobject_cast<DataIndexKeyboardLayout*>(m_currentResource))
181 {
182 for (int i = 0; i < m_dataIndex->keyboardLayoutCount(); i++)
183 {
184 if (m_dataIndex->keyboardLayout(i) == keyboardLayout)
185 {
186 KeyboardLayout* backup = new KeyboardLayout();
187 if (!dataAccess.loadKeyboardLayout(m_dataIndex->keyboardLayout(i), backup))
188 {
189 KMessageBox::error(this, i18n("Error while opening keyboard layout"));
190 return;
191 }
192 if (!userDataAccess.deleteKeyboardLayout(backup))
193 {
194 KMessageBox::error(this, i18n("Error while deleting keyboard layout"));
195 return;
196 }
197 prepareResourceRestore(backup);
198 m_dataIndex->removeKeyboardLayout(i);
199 }
200 }
201 }
202
203 // HACK: reactivate categorization again
204 m_categorizedResourceModel->setCategorizedModel(true);
205
206 selectFirstResource();
207 }
208
importResource()209 void ResourceEditor::importResource()
210 {
211 const QString path = QFileDialog::getOpenFileName(this, QString(), QString(), i18n("XML files (*.xml)"));
212
213 if (!path.isNull())
214 {
215 save();
216
217 if (importCourse(path))
218 return;
219
220 if (importKeyboardLayout(path))
221 return;
222
223 KMessageBox::error(this, i18n("The selected file could not be imported."));
224 }
225 }
226
exportResource()227 void ResourceEditor::exportResource()
228 {
229 Q_ASSERT(m_currentResource);
230
231 DataAccess dataAccess;
232 ResourceDataAccess resourceDataAccess;
233
234 save();
235
236 if (DataIndexCourse* dataIndexCourse = qobject_cast<DataIndexCourse*>(m_currentResource))
237 {
238 for (int i = 0; i < m_dataIndex->courseCount(); i++)
239 {
240 if (m_dataIndex->course(i) == dataIndexCourse)
241 {
242 Course* course = new Course();
243
244 if (!dataAccess.loadCourse(m_dataIndex->course(i), course))
245 {
246 KMessageBox::error(this, i18n("Error while opening course"));
247 delete course;
248 return;
249 }
250
251 const QString initialFileName(QStringLiteral("%1.xml").arg(course->keyboardLayoutName()));
252 const QString path(QFileDialog::getSaveFileName(this, QString(), initialFileName, i18n("XML files (*.xml)")));
253
254 if (!path.isNull())
255 {
256 if (!resourceDataAccess.storeCourse(path, course))
257 {
258 KMessageBox::error(this, i18n("Error while saving course"));
259 delete course;
260 return;
261 }
262 }
263 delete course;
264 }
265 }
266 }
267 else if (DataIndexKeyboardLayout* dataIndexkeyboardLayout = qobject_cast<DataIndexKeyboardLayout*>(m_currentResource))
268 {
269 for (int i = 0; i < m_dataIndex->keyboardLayoutCount(); i++)
270 {
271 if (m_dataIndex->keyboardLayout(i) == dataIndexkeyboardLayout)
272 {
273 KeyboardLayout* keyboardlayout = new KeyboardLayout();
274
275 if (!dataAccess.loadKeyboardLayout(m_dataIndex->keyboardLayout(i), keyboardlayout))
276 {
277 KMessageBox::error(this, i18n("Error while opening keyboard layout"));
278 delete keyboardlayout;
279 return;
280 }
281
282 const QString initialFileName(QStringLiteral("%1.xml").arg(keyboardlayout->name()));
283 const QString path(QFileDialog::getSaveFileName(this, QString(), initialFileName, i18n("XML files (*.xml)")));
284
285 if (!path.isNull())
286 {
287 if (!resourceDataAccess.storeKeyboardLayout(path, keyboardlayout))
288 {
289 KMessageBox::error(this, i18n("Error while saving keyboard layout"));
290 delete keyboardlayout;
291 return;
292 }
293 }
294 delete keyboardlayout;
295 }
296 }
297 }
298 }
299
onResourceSelected()300 void ResourceEditor::onResourceSelected()
301 {
302 QAbstractItemView* const resourceView = m_editorWidget->resourceView();
303
304 save();
305
306 if (resourceView->selectionModel()->hasSelection())
307 {
308 QModelIndex current = resourceView->selectionModel()->selectedIndexes().first();
309 const QVariant variant = resourceView->model()->data(current, ResourceModel::DataRole);
310 QObject* const obj = variant.value<QObject*>();
311 m_currentResource = qobject_cast<Resource*>(obj);
312 const DataIndex::Source source = static_cast<DataIndex::Source>(resourceView->model()->data(current, ResourceModel::SourceRole).toInt());
313
314 Q_ASSERT(m_currentResource);
315
316 m_deleteResourceAction->setEnabled(source == DataIndex::UserResource);
317 m_exportResourceAction->setEnabled(true);
318 m_editorWidget->openResource(m_currentResource);
319 }
320 else
321 {
322 m_currentResource = 0;
323 m_deleteResourceAction->setEnabled(false);
324 m_exportResourceAction->setEnabled(false);
325 }
326 }
327
restoreResourceBackup()328 void ResourceEditor::restoreResourceBackup()
329 {
330 Q_ASSERT(m_backupResource);
331
332 save();
333
334 if (Resource* dataIndexResource = storeResource(m_backupResource))
335 {
336 selectDataResource(dataIndexResource);
337 }
338
339 clearResourceBackup();
340 }
341
clearResourceBackup()342 void ResourceEditor::clearResourceBackup()
343 {
344 Q_ASSERT(m_backupResource);
345
346 delete m_backupResource;
347 m_backupResource = 0;
348 }
349
save()350 void ResourceEditor::save()
351 {
352 if (m_undoGroup->activeStack() && !m_undoGroup->activeStack()->isClean())
353 {
354 m_editorWidget->save();
355 }
356
357 m_saveTimer->start();
358 }
359
setUndoText(const QString & text)360 void ResourceEditor::setUndoText(const QString& text)
361 {
362 m_undoAction->setToolTip(text);
363 }
364
setRedoText(const QString & text)365 void ResourceEditor::setRedoText(const QString& text)
366 {
367 m_redoAction->setToolTip(text);
368 }
369
prepareResourceRestore(Resource * backup)370 void ResourceEditor::prepareResourceRestore(Resource* backup)
371 {
372 QString msg;
373
374 if (Course* course = qobject_cast<Course*>(backup))
375 {
376 msg = i18n("Course <b>%1</b> deleted", course->title().toHtmlEscaped());
377 }
378 else if (KeyboardLayout* keyboardLayout = qobject_cast<KeyboardLayout*>(backup))
379 {
380 msg = i18n("Keyboard layout <b>%1</b> deleted", keyboardLayout->title().toHtmlEscaped());
381 }
382
383 m_editorWidget->showMessage(ResourceEditorWidget::ResourceDeletedMsg, msg);
384
385 if (m_backupResource)
386 {
387 delete m_backupResource;
388 }
389
390 m_backupResource = backup;
391 }
392
storeResource(Resource * resource,Resource * dataIndexResource)393 Resource* ResourceEditor::storeResource(Resource* resource, Resource* dataIndexResource)
394 {
395 // FIXME: Is all this path mangling necessary?
396 UserDataAccess userDataAccess;
397
398 if (Course* course = qobject_cast<Course*>(resource))
399 {
400 DataIndexCourse* dataIndexCourse = dataIndexResource == 0?
401 new DataIndexCourse():
402 dynamic_cast<DataIndexCourse*>(dataIndexResource);
403
404 QDir dir = QDir(QStandardPaths::writableLocation(QStandardPaths::DataLocation));
405 dir.mkpath(QStringLiteral("courses"));
406 dir.cd(QStringLiteral("courses"));
407
408 QString path = dataIndexResource == 0?
409 dir.filePath(QStringLiteral("%1.xml").arg(course->id())):
410 dataIndexCourse->path();
411
412 if (!userDataAccess.storeCourse(course))
413 {
414 KMessageBox::error(this, i18n("Error while saving course to disk."));
415 return 0;
416 }
417
418 dataIndexCourse->setSource(DataIndex::UserResource);
419 dataIndexCourse->setTitle(course->title());
420 dataIndexCourse->setDescription(course->description());
421 dataIndexCourse->setKeyboardLayoutName(course->keyboardLayoutName());
422 dataIndexCourse->setId(course->id());
423 dataIndexCourse->setPath(path);
424
425 if (dataIndexResource == 0)
426 {
427 m_dataIndex->addCourse(dataIndexCourse);
428 }
429
430 dataIndexResource = dataIndexCourse;
431 }
432 else if (KeyboardLayout* keyboardLayout = qobject_cast<KeyboardLayout*>(resource))
433 {
434 DataIndexKeyboardLayout* dataIndexKeyboardLayout = dataIndexResource == 0?
435 new DataIndexKeyboardLayout():
436 qobject_cast<DataIndexKeyboardLayout*>(dataIndexResource);
437
438 QDir dir = QDir(QStandardPaths::writableLocation(QStandardPaths::DataLocation));
439 dir.mkpath(QStringLiteral("keyboardlayouts"));
440 dir.cd(QStringLiteral("keyboardlayouts"));
441
442 QString path = dataIndexResource == 0?
443 dir.filePath(QStringLiteral("%1.xml").arg(keyboardLayout->id())):
444 dataIndexKeyboardLayout->path();
445
446 if (!userDataAccess.storeKeyboardLayout(keyboardLayout))
447 {
448 KMessageBox::error(this, i18n("Error while saving keyboard layout to disk."));
449 return 0;
450 }
451
452 dataIndexKeyboardLayout->setSource(DataIndex::UserResource);
453 dataIndexKeyboardLayout->setName(keyboardLayout->name());
454 dataIndexKeyboardLayout->setTitle(keyboardLayout->title());
455 dataIndexKeyboardLayout->setId(keyboardLayout->id());
456 dataIndexKeyboardLayout->setPath(path);
457
458 if (dataIndexResource == 0)
459 {
460 m_dataIndex->addKeyboardLayout(dataIndexKeyboardLayout);
461 }
462
463 dataIndexResource = dataIndexKeyboardLayout;
464 }
465
466 return dataIndexResource;
467 }
468
469
selectDataResource(Resource * resource)470 void ResourceEditor::selectDataResource(Resource* resource)
471 {
472 QAbstractItemView* const resourceView = m_editorWidget->resourceView();
473 QAbstractItemModel* const model = resourceView->model();
474
475 resourceView->selectionModel()->clearSelection();
476
477 for (int i = 0; i < model->rowCount(); i++)
478 {
479 const QModelIndex index = model->index(i, 0);
480 const QVariant var = model->data(index, ResourceModel::DataRole);
481 QObject* obj = var.value<QObject*>();
482
483 if (obj == resource)
484 {
485 resourceView->selectionModel()->select(index, QItemSelectionModel::ClearAndSelect);
486 break;
487 }
488 }
489 }
490
selectFirstResource()491 void ResourceEditor::selectFirstResource()
492 {
493 QAbstractItemView* const resourceView = m_editorWidget->resourceView();
494
495 if (resourceView->model()->rowCount() > 0)
496 {
497 resourceView->selectionModel()->select(resourceView->model()->index(0, 0), QItemSelectionModel::ClearAndSelect);
498 }
499 }
500
importCourse(const QString & path)501 bool ResourceEditor::importCourse(const QString& path)
502 {
503 ResourceDataAccess resourceDataAccess;
504 Course course;
505
506 if (!resourceDataAccess.loadCourse(path, &course))
507 return false;
508
509 DataIndexCourse* overwriteDataIndexCourse(0);
510
511 for (int i = 0; i < m_dataIndex->courseCount(); i++)
512 {
513 DataIndexCourse* const testCourse = m_dataIndex->course(i);
514
515 if (testCourse->source() == DataIndex::BuiltInResource && testCourse->id() == course.id())
516 {
517 switch (KMessageBox::questionYesNo(this, i18n("The selected course is already present as a built-in course."), QString(),
518 KGuiItem(i18n("Import as new course"), QStringLiteral("dialog-ok")),
519 KStandardGuiItem::cancel()
520 ))
521 {
522 case KMessageBox::Yes:
523 course.setId(QUuid::createUuid().toString());
524 for (int j = 0; j < course.lessonCount(); j++)
525 {
526 Lesson* const lesson = course.lesson(j);
527 lesson->setId(QUuid::createUuid().toString());
528 }
529 break;
530 default:
531 return true;
532 }
533 }
534
535 if (testCourse->source() == DataIndex::UserResource && testCourse->id() == course.id())
536 {
537 switch (KMessageBox::questionYesNoCancel(this, i18n("The selected course is already present as a user course."), QString(),
538 KGuiItem(i18n("Import as new course"), QStringLiteral("dialog-ok")),
539 KStandardGuiItem::overwrite(),
540 KStandardGuiItem::cancel()
541 ))
542 {
543 case KMessageBox::Yes:
544 course.setId(QUuid::createUuid().toString());
545 for (int j = 0; j < course.lessonCount(); j++)
546 {
547 Lesson* const lesson = course.lesson(j);
548 lesson->setId(QUuid::createUuid().toString());
549 }
550 break;
551 case KMessageBox::No:
552 overwriteDataIndexCourse = testCourse;
553 break;
554 default:
555 return true;
556 }
557 }
558 }
559
560 if (Resource* dataIndexResource = storeResource(&course, overwriteDataIndexCourse))
561 {
562 m_editorWidget->clearUndoStackForResource(dataIndexResource);
563 selectDataResource(dataIndexResource);
564 }
565
566 return true;
567 }
568
importKeyboardLayout(const QString & path)569 bool ResourceEditor::importKeyboardLayout(const QString& path)
570 {
571 ResourceDataAccess resourceDataAccess;
572 KeyboardLayout keyboardLayout;
573
574 if (!resourceDataAccess.loadKeyboardLayout(path, &keyboardLayout))
575 return false;
576
577 DataIndexKeyboardLayout* overwriteDataIndexKeyboardLayout(0);
578
579 for (int i = 0; i < m_dataIndex->keyboardLayoutCount(); i++)
580 {
581 DataIndexKeyboardLayout* const testKeyboardLayout = m_dataIndex->keyboardLayout(i);
582
583 if (testKeyboardLayout->source() == DataIndex::BuiltInResource && testKeyboardLayout->id() == keyboardLayout.id())
584 {
585 switch (KMessageBox::questionYesNo(this, i18n("The selected keyboard layout is already present as a built-in keyboard layout."), QString(),
586 KGuiItem(i18n("Import as new keyboard layout"), QStringLiteral("dialog-ok")),
587 KStandardGuiItem::cancel()
588 ))
589 {
590 case KMessageBox::Yes:
591 keyboardLayout.setId(QUuid::createUuid().toString());
592 break;
593 default:
594 return true;
595 }
596 }
597
598 if (testKeyboardLayout->source() == DataIndex::UserResource && testKeyboardLayout->id() == keyboardLayout.id())
599 {
600 switch (KMessageBox::questionYesNoCancel(this, i18n("The selected keyboard layout is already present as a user keyboard layout."), QString(),
601 KGuiItem(i18n("Import as new keyboard layout"), QStringLiteral("dialog-ok")),
602 KStandardGuiItem::overwrite(),
603 KStandardGuiItem::cancel()
604 ))
605 {
606 case KMessageBox::Yes:
607 keyboardLayout.setId(QUuid::createUuid().toString());
608 break;
609 case KMessageBox::No:
610 overwriteDataIndexKeyboardLayout = testKeyboardLayout;
611 break;
612 default:
613 return true;
614 }
615 }
616 }
617
618 if (Resource* dataIndexResource = storeResource(&keyboardLayout, overwriteDataIndexKeyboardLayout))
619 {
620 m_editorWidget->clearUndoStackForResource(dataIndexResource);
621 selectDataResource(dataIndexResource);
622 }
623
624 return true;
625 }
626
627