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