Commit ea615ea3 authored by Andreas Cord-Landwehr's avatar Andreas Cord-Landwehr
Browse files

Add export dialog for GHNS packaging.

Instead of using external scripts, now the editor allows
to generate export archives for courses. By this, the
generation is intelligent enough to exclude all empty
phrases.
parent d1d773c1
......@@ -54,6 +54,7 @@ find_package(Qt5 5.5 REQUIRED COMPONENTS
Test
)
find_package(KF5 REQUIRED COMPONENTS
Archive
Config
ConfigWidgets
Crash
......
......@@ -71,6 +71,7 @@ target_link_libraries(artikulatecore
artikulatelearnerprofile
artikulatesound
Qt5::XmlPatterns
KF5::Archive
KF5::ConfigGui
KF5::WidgetsAddons
)
......@@ -147,11 +148,13 @@ set(artikulate_editor_SRCS
ui/sounddevicedialogpage.cpp
ui/appearencedialogpage.cpp
ui/resourcesdialogpage.cpp
ui/exportghnsdialog.cpp
)
qt5_wrap_ui (artikulate_editor_SRCS
ui/resourcesdialogpage.ui
ui/sounddevicedialogpage.ui
ui/appearencedialogpage.ui
ui/exportghnsdialog.ui
)
kconfig_add_kcfg_files (artikulate_editor_SRCS settings.kcfgc)
......
......@@ -9,6 +9,7 @@
<MenuBar>
<Menu name="file" >
<Action name="save" />
<Action name="export_ghns" />
</Menu>
<Menu name="settings" >
<Action name="settings" />
......
/*
* Copyright 2013 Andreas Cord-Landwehr <cordlandwehr@kde.org>
* Copyright 2013-2015 Andreas Cord-Landwehr <cordlandwehr@kde.org>
* Copyright 2013 Oindrila Gupta <oindrila.gupta92@gmail.com>
*
* This program is free software; you can redistribute it and/or
......@@ -35,6 +35,7 @@
#include <QFile>
#include <QFileInfo>
#include <QDir>
#include <KTar>
#include "artikulate_debug.h"
......@@ -43,7 +44,7 @@ class CourseResourcePrivate
public:
CourseResourcePrivate(ResourceManager *resourceManager)
: m_resourceManager(resourceManager)
, m_courseResource(0)
, m_courseResource(nullptr)
{
}
......@@ -157,7 +158,7 @@ void CourseResource::sync()
Q_ASSERT(!path().isEmpty());
// if resource was never loaded, it cannot be changed
if (d->m_courseResource == 0) {
if (d->m_courseResource == nullptr) {
qCDebug(ARTIKULATE_LOG) << "Aborting sync, course was not parsed.";
return;
}
......@@ -169,67 +170,9 @@ void CourseResource::sync()
// return;
// }
QDomDocument document;
// prepare xml header
QDomProcessingInstruction header = document.createProcessingInstruction("xml", "version=\"1.0\"");
document.appendChild(header);
// create main element
QDomElement root = document.createElement("course");
document.appendChild(root);
QDomElement idElement = document.createElement("id");
QDomElement titleElement = document.createElement("title");
QDomElement descriptionElement = document.createElement("description");
QDomElement languageElement = document.createElement("language");
idElement.appendChild(document.createTextNode(d->m_courseResource->id()));
titleElement.appendChild(document.createTextNode(d->m_courseResource->title()));
descriptionElement.appendChild(document.createTextNode(d->m_courseResource->description()));
languageElement.appendChild(document.createTextNode(d->m_courseResource->language()->id()));
QDomElement unitListElement = document.createElement("units");
// create units
foreach (Unit *unit, d->m_courseResource->unitList()) {
QDomElement unitElement = document.createElement("unit");
QDomElement unitIdElement = document.createElement("id");
QDomElement unitTitleElement = document.createElement("title");
QDomElement unitPhraseListElement = document.createElement("phrases");
unitIdElement.appendChild(document.createTextNode(unit->id()));
unitTitleElement.appendChild(document.createTextNode(unit->title()));
// construct phrases
foreach (Phrase *phrase, unit->phraseList()) {
unitPhraseListElement.appendChild(serializePhrase(phrase, document));
}
// construct the unit element
unitElement.appendChild(unitIdElement);
if (!unit->foreignId().isEmpty()) {
QDomElement unitForeignIdElement = document.createElement("foreignId");
unitForeignIdElement.appendChild(document.createTextNode(unit->foreignId()));
unitElement.appendChild(unitForeignIdElement);
}
unitElement.appendChild(unitTitleElement);
unitElement.appendChild(unitPhraseListElement);
unitListElement.appendChild(unitElement);
}
root.appendChild(idElement);
if (!d->m_courseResource->foreignId().isEmpty()) {
QDomElement courseForeignIdElement = document.createElement("foreignId");
courseForeignIdElement.appendChild(document.createTextNode(d->m_courseResource->foreignId()));
root.appendChild(courseForeignIdElement);
}
root.appendChild(titleElement);
root.appendChild(descriptionElement);
root.appendChild(languageElement);
root.appendChild(unitListElement);
// write back to file
QFileInfo info(path().adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash).path()); // create directories if necessary
// create directories if necessary
QFileInfo info(path().adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash).path());
if (!info.exists()) {
qCDebug(ARTIKULATE_LOG) << "create xml output file directory, not existing";
QDir dir;
......@@ -243,55 +186,36 @@ void CourseResource::sync()
return;
}
file.write(document.toByteArray());
file.write(serializedDocument().toByteArray());
return;
}
QDomElement CourseResource::serializePhrase(Phrase *phrase, QDomDocument &document)
void CourseResource::exportGhns(const QString &path)
{
QDomElement phraseElement = document.createElement("phrase");
QDomElement phraseIdElement = document.createElement("id");
QDomElement phraseTextElement = document.createElement("text");
QDomElement phrasei18nTextElement = document.createElement("i18nText");
QDomElement phraseSoundFileElement = document.createElement("soundFile");
QDomElement phraseTypeElement = document.createElement("type");
QDomElement phraseEditStateElement = document.createElement("editState");
QDomElement phrasePhonemeListElement = document.createElement("phonemes");
phraseIdElement.appendChild(document.createTextNode(phrase->id()));
phraseTextElement.appendChild(document.createTextNode(phrase->text()));
phrasei18nTextElement.appendChild(document.createTextNode(phrase->i18nText()));
phraseSoundFileElement.appendChild(document.createTextNode(phrase->sound().fileName()));
phraseTypeElement.appendChild(document.createTextNode(phrase->typeString()));
phraseEditStateElement.appendChild(document.createTextNode(phrase->editStateString()));
// add phonemes
foreach (Phoneme *phoneme, phrase->phonemes()) {
QDomElement phonemeElement = document.createElement("phonemeID");
phonemeElement.appendChild(document.createTextNode(phoneme->id()));
phrasePhonemeListElement.appendChild(phonemeElement);
// ensure that course is loaded before exporting it
Course *course = CourseResource::course();
// filename
const QString fileName = identifier() + ".tar.bz2";
KTar tar = KTar(path + '/' + fileName, QString("application/x-bzip"));
if (!tar.open(QIODevice::WriteOnly)) {
qCWarning(ARTIKULATE_LOG) << "Unable to open tar file"
<< path + '/' + fileName
<< "in write mode, aborting.";
return;
}
phraseElement.appendChild(phraseIdElement);
if (!phrase->foreignId().isEmpty()) {
QDomElement phraseForeignIdElement = document.createElement("foreignId");
phraseForeignIdElement.appendChild(document.createTextNode(phrase->foreignId()));
phraseElement.appendChild(phraseForeignIdElement);
foreach (auto *unit, course->unitList()) {
foreach (auto *phrase, unit->phraseList()) {
if (QFile::exists(phrase->soundFileUrl())) {
tar.addLocalFile(phrase->soundFileUrl(), phrase->id() + ".ogg");
}
}
phraseElement.appendChild(phraseTextElement);
phraseElement.appendChild(phrasei18nTextElement);
phraseElement.appendChild(phraseSoundFileElement);
phraseElement.appendChild(phraseTypeElement);
phraseElement.appendChild(phraseEditStateElement);
phraseElement.appendChild(phrasePhonemeListElement);
if (phrase->isExcluded()) {
QDomElement phraseIsExcludedElement = document.createElement("excluded");
phraseIsExcludedElement.appendChild(document.createTextNode("true"));
phraseElement.appendChild(phraseIsExcludedElement);
}
return phraseElement;
tar.writeFile(identifier() + ".xml", serializedDocument(true).toByteArray());
tar.close();
}
void CourseResource::close()
......@@ -315,7 +239,7 @@ QUrl CourseResource::path() const
QObject * CourseResource::resource()
{
if (d->m_courseResource != 0) {
if (d->m_courseResource != nullptr) {
return d->m_courseResource;
}
......@@ -331,12 +255,12 @@ QObject * CourseResource::resource()
// load existing file
QXmlSchema schema = loadXmlSchema("course");
if (!schema.isValid()) {
return 0;
return nullptr;
}
QDomDocument document = loadDomDocument(path(), schema);
if (document.isNull()) {
qCWarning(ARTIKULATE_LOG) << "Could not parse document " << path().toLocalFile() << ", aborting.";
return 0;
return nullptr;
}
// create course
......@@ -360,9 +284,9 @@ QObject * CourseResource::resource()
break;
}
}
if (d->m_courseResource->language() == 0) {
if (d->m_courseResource->language() == nullptr) {
qCWarning(ARTIKULATE_LOG) << "Language ID" << language << "unknown, could not register any language, aborting";
return 0;
return nullptr;
}
// create units
......@@ -445,3 +369,120 @@ Phrase* CourseResource::parsePhrase(QDomElement phraseNode, Unit* parentUnit) co
return phrase;
}
QDomDocument CourseResource::serializedDocument(bool trainingExport) const
{
QDomDocument document;
// prepare xml header
QDomProcessingInstruction header = document.createProcessingInstruction("xml", "version=\"1.0\"");
document.appendChild(header);
// create main element
QDomElement root = document.createElement("course");
document.appendChild(root);
QDomElement idElement = document.createElement("id");
QDomElement titleElement = document.createElement("title");
QDomElement descriptionElement = document.createElement("description");
QDomElement languageElement = document.createElement("language");
idElement.appendChild(document.createTextNode(d->m_courseResource->id()));
titleElement.appendChild(document.createTextNode(d->m_courseResource->title()));
descriptionElement.appendChild(document.createTextNode(d->m_courseResource->description()));
languageElement.appendChild(document.createTextNode(d->m_courseResource->language()->id()));
QDomElement unitListElement = document.createElement("units");
// create units
foreach (Unit *unit, d->m_courseResource->unitList()) {
QDomElement unitElement = document.createElement("unit");
QDomElement unitIdElement = document.createElement("id");
QDomElement unitTitleElement = document.createElement("title");
QDomElement unitPhraseListElement = document.createElement("phrases");
unitIdElement.appendChild(document.createTextNode(unit->id()));
unitTitleElement.appendChild(document.createTextNode(unit->title()));
// construct phrases
foreach (Phrase *phrase, unit->phraseList()) {
if (trainingExport && phrase->soundFileUrl().isEmpty()) {
continue;
}
unitPhraseListElement.appendChild(serializedPhrase(phrase, document));
}
if (trainingExport && unitPhraseListElement.childNodes().count() == 0) {
continue;
}
// construct the unit element
unitElement.appendChild(unitIdElement);
if (!unit->foreignId().isEmpty()) {
QDomElement unitForeignIdElement = document.createElement("foreignId");
unitForeignIdElement.appendChild(document.createTextNode(unit->foreignId()));
unitElement.appendChild(unitForeignIdElement);
}
unitElement.appendChild(unitTitleElement);
unitElement.appendChild(unitPhraseListElement);
unitListElement.appendChild(unitElement);
}
root.appendChild(idElement);
if (!d->m_courseResource->foreignId().isEmpty()) {
QDomElement courseForeignIdElement = document.createElement("foreignId");
courseForeignIdElement.appendChild(document.createTextNode(d->m_courseResource->foreignId()));
root.appendChild(courseForeignIdElement);
}
root.appendChild(titleElement);
root.appendChild(descriptionElement);
root.appendChild(languageElement);
root.appendChild(unitListElement);
return document;
}
QDomElement CourseResource::serializedPhrase(Phrase *phrase, QDomDocument &document) const
{
QDomElement phraseElement = document.createElement("phrase");
QDomElement phraseIdElement = document.createElement("id");
QDomElement phraseTextElement = document.createElement("text");
QDomElement phrasei18nTextElement = document.createElement("i18nText");
QDomElement phraseSoundFileElement = document.createElement("soundFile");
QDomElement phraseTypeElement = document.createElement("type");
QDomElement phraseEditStateElement = document.createElement("editState");
QDomElement phrasePhonemeListElement = document.createElement("phonemes");
phraseIdElement.appendChild(document.createTextNode(phrase->id()));
phraseTextElement.appendChild(document.createTextNode(phrase->text()));
phrasei18nTextElement.appendChild(document.createTextNode(phrase->i18nText()));
phraseSoundFileElement.appendChild(document.createTextNode(phrase->sound().fileName()));
phraseTypeElement.appendChild(document.createTextNode(phrase->typeString()));
phraseEditStateElement.appendChild(document.createTextNode(phrase->editStateString()));
// add phonemes
foreach (Phoneme *phoneme, phrase->phonemes()) {
QDomElement phonemeElement = document.createElement("phonemeID");
phonemeElement.appendChild(document.createTextNode(phoneme->id()));
phrasePhonemeListElement.appendChild(phonemeElement);
}
phraseElement.appendChild(phraseIdElement);
if (!phrase->foreignId().isEmpty()) {
QDomElement phraseForeignIdElement = document.createElement("foreignId");
phraseForeignIdElement.appendChild(document.createTextNode(phrase->foreignId()));
phraseElement.appendChild(phraseForeignIdElement);
}
phraseElement.appendChild(phraseTextElement);
phraseElement.appendChild(phrasei18nTextElement);
phraseElement.appendChild(phraseSoundFileElement);
phraseElement.appendChild(phraseTypeElement);
phraseElement.appendChild(phraseEditStateElement);
phraseElement.appendChild(phrasePhonemeListElement);
if (phrase->isExcluded()) {
QDomElement phraseIsExcludedElement = document.createElement("excluded");
phraseIsExcludedElement.appendChild(document.createTextNode("true"));
phraseElement.appendChild(phraseIsExcludedElement);
}
return phraseElement;
}
\ No newline at end of file
......@@ -27,6 +27,7 @@
#include <QObject>
class QDomElement;
class QString;
class CourseResourcePrivate;
class Course;
class Unit;
......@@ -77,6 +78,11 @@ public:
virtual void sync();
/**
* export course as <course-id>.tar.bz2 file in the specified folder.
*/
void exportGhns(const QString &path);
/**
* close resource without writing changes back to file
*/
......@@ -101,7 +107,12 @@ public:
private:
Phrase * parsePhrase(QDomElement phraseNode, Unit *parentUnit) const;
QDomElement serializePhrase(Phrase * phrase, QDomDocument &document);
/**
* \return serialized course as DOM document
* \param trainingExport if true phrases without recording and empty units are excluded
*/
QDomDocument serializedDocument(bool trainingExport=false) const;
QDomElement serializedPhrase(Phrase * phrase, QDomDocument &document) const;
const QScopedPointer<CourseResourcePrivate> d;
};
......
......@@ -22,6 +22,7 @@
#include "ui/resourcesdialogpage.h"
#include "ui/sounddevicedialogpage.h"
#include "ui/appearencedialogpage.h"
#include "ui/exportghnsdialog.h"
#include "core/resourcemanager.h"
#include "core/editorsession.h"
#include "core/resources/courseresource.h"
......@@ -56,8 +57,8 @@
using namespace LearnerProfile;
MainWindowEditor::MainWindowEditor()
: m_editorSession(new EditorSession(this))
, m_resourceManager(new ResourceManager(this))
: m_resourceManager(new ResourceManager(this))
, m_editorSession(new EditorSession(this))
, m_widget(new QQuickWidget)
{
m_editorSession->setResourceManager(m_resourceManager);
......@@ -124,10 +125,18 @@ ResourceManager * MainWindowEditor::resourceManager() const
void MainWindowEditor::setupActions()
{
QAction *settingsAction = new QAction(i18nc("@item:inmenu", "Configure Artikulate"), this);
connect(settingsAction, SIGNAL(triggered()), SLOT(showSettingsDialog()));
connect(settingsAction, &QAction::triggered, this, &MainWindowEditor::showSettingsDialog);
actionCollection()->addAction("settings", settingsAction);
settingsAction->setIcon(QIcon::fromTheme("configure"));
QAction *exportAction = new QAction(i18nc("@item:inmenu", "Export GHNS Files"), this);
connect(exportAction, &QAction::triggered, [=]() {
QPointer<QDialog> dialog = new ExportGhnsDialog(m_resourceManager);
dialog->exec();
});
actionCollection()->addAction("export_ghns", exportAction);
exportAction->setIcon(QIcon::fromTheme("document-export"));
KStandardAction::quit(this, SLOT(quit()), actionCollection());
setupGUI(Keys | Save | Create, "artikulateui_editor.rc");
......
......@@ -66,8 +66,8 @@ Q_SIGNALS:
private:
ResourceManager *m_resourceManager;
QQuickWidget *m_widget;
EditorSession *m_editorSession;
QQuickWidget *m_widget;
};
#endif
/*
* Copyright 2015 Andreas Cord-Landwehr <cordlandwehr@kde.org>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of
* the License or (at your option) version 3 or any later version
* accepted by the membership of KDE e.V. (or its successor approved
* by the membership of KDE e.V.), which shall act as a proxy
* defined in Section 14 of version 3 of the license.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "exportghnsdialog.h"
#include "core/resourcemanager.h"
#include "core/resources/languageresource.h"
#include "core/resources/courseresource.h"
#include "artikulate_debug.h"
#include <KLocalizedString>
#include <QFileDialog>
#include <QPushButton>
#include <QStandardPaths>
#include <QWidget>
ExportGhnsDialog::ExportGhnsDialog(ResourceManager *manager)
: m_manager(manager)
{
ui = new Ui::ExportGhnsDialog;
ui->setupUi(this);
// require to set a proper directory
ui->buttonBox->button(QDialogButtonBox::Apply)->setDisabled(true);
ui->buttonBox->button(QDialogButtonBox::Apply)->setText(i18n("Export"));
connect(ui->exportDirectory, &QLineEdit::textChanged, this, [=](){
const bool directorySet = !ui->exportDirectory->text().isEmpty();
ui->buttonBox->button(QDialogButtonBox::Apply)->setEnabled(directorySet);
});
connect(ui->buttonBox->button(QDialogButtonBox::Apply), &QPushButton::clicked,
this, &ExportGhnsDialog::onExportCourse);
// directory selection dialog
connect(ui->selectDirectoryButton, &QToolButton::clicked, this, [=]() {
// TODO save last path in config file
const QString dir = QFileDialog::getExistingDirectory(
this,
i18n("Export Directory"),
QString(),
QFileDialog::ShowDirsOnly);
ui->exportDirectory->setText(dir);
});
// add courses to combo box
int counter = 0;
foreach (auto *languageRes, manager->languageResources()) {
foreach (auto *courseRes, manager->courseResources(languageRes->language())) {
ui->courseListCombo->insertItem(counter, courseRes->i18nTitle(),
QVariant::fromValue<QObject*>(courseRes));
++counter;
}
}
connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
}
ExportGhnsDialog::~ExportGhnsDialog()
{
delete ui;
}
void ExportGhnsDialog::onExportCourse()
{
CourseResource *res = qobject_cast<CourseResource *>(
ui->courseListCombo->currentData().value<QObject*>());
qCDebug(ARTIKULATE_LOG) << res << "export GHNS file for" << res->i18nTitle();
res->exportGhns(ui->exportDirectory->text());
}
/*
* Copyright 2013 Andreas Cord-Landwehr <cordlandwehr@kde.org>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of
* the License or (at your option) version 3 or any later version
* accepted by the membership of KDE e.V. (or its successor approved
* by the membership of KDE e.V.), which shall act as a proxy
* defined in Section 14 of version 3 of the license.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef EXPORTGHNSDIALOG_H
#define EXPORTGHNSDIALOG_H
#include "ui_exportghnsdialog.h"
#include <QTemporaryFile>
#include <QDialog>
class ResourceManager;
class ExportGhnsDialog :
public QDialog
{
Q_OBJECT
public:
ExportGhnsDialog(ResourceManager *manager);
~ExportGhnsDialog();
public Q_SLOTS:
void onExportCourse();
private:
Ui::ExportGhnsDialog *ui;
const ResourceManager *m_manager;
};
#endif
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ExportGhnsDialog</class>
<widget class="QWidget" name="ExportGhnsDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>367</width>
<height>150</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">