diff --git a/src/core/editorsession.h b/src/core/editorsession.h index 441d05b..9891987 100644 --- a/src/core/editorsession.h +++ b/src/core/editorsession.h @@ -1,130 +1,131 @@ /* * Copyright 2013-2015 Andreas Cord-Landwehr * * 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 . */ #ifndef EDITORSESSION_H #define EDITORSESSION_H #include "artikulatecore_export.h" #include "phrase.h" #include "isessionactions.h" class ILanguage; class IEditableCourse; class Unit; class IPhrase; class SkeletonResource; class IEditableRepository; /** * \class EditorSession * * An object of this class is used to set the current state of the editor. By this, we put all logic * how language, skeleton and course fit to each other into this class. The main concept is that * we have to fundamentally different workflows that both are modeled in this class: * * 1. Skeleton based workflow * - a skeleton is selected * - every language is available, since eventually the course should be available in every language * - for every language, there is at most one course (there is none only in case it is not created yet) * - adding new units or phrases is only possible in the skeleton course * - every course can update/sync with the skeleton * * 2. Course based workflow * - there is no skeleton from which the course is derived * - the base is a language that is selected first * - for a language there can be none to arbitrarily many courses * * The main switch is \c EditorSession::setSkeletonMode(bool) */ class ARTIKULATECORE_EXPORT EditorSession : public ISessionActions { Q_OBJECT Q_INTERFACES(ISessionActions) Q_PROPERTY(bool skeletonMode READ skeletonMode NOTIFY skeletonModeChanged) Q_PROPERTY(bool editSkeleton READ isEditSkeleton WRITE setEditSkeleton NOTIFY editSkeletonChanged) Q_PROPERTY(IEditableCourse *skeleton READ skeleton WRITE setSkeleton NOTIFY skeletonChanged) Q_PROPERTY(IEditableCourse *course READ course WRITE setCourse NOTIFY courseChanged) - + Q_PROPERTY(IUnit *unit READ activeUnit NOTIFY unitChanged) + Q_PROPERTY(ILanguage *language READ language NOTIFY languageChanged) // editor elements depending on currently selected mode, skeleton and course /** * @brief the displayed course (skeleton or course) depending on the user selection */ Q_PROPERTY(IEditableCourse *displayedCourse READ displayedCourse NOTIFY displayedCourseChanged) Q_PROPERTY(IPhrase *phrase READ activePhrase WRITE setActivePhrase NOTIFY phraseChanged) Q_PROPERTY(bool hasNextPhrase READ hasNextPhrase NOTIFY phraseChanged) Q_PROPERTY(bool hasPreviousPhrase READ hasPreviousPhrase NOTIFY phraseChanged) public: explicit EditorSession(QObject *parent = nullptr); void setRepository(IEditableRepository *repository); bool skeletonMode() const; void setEditSkeleton(bool enabled=true); bool isEditSkeleton() const; IEditableCourse * skeleton() const; void setSkeleton(IEditableCourse *skeleton); ILanguage * language() const; IEditableCourse * course() const; void setCourse(IEditableCourse *course); /** * @brief Open course resource by specifying the language * @param language the target language */ Q_INVOKABLE void setCourseByLanguage(ILanguage *language); IEditableCourse * displayedCourse() const; IUnit * activeUnit() const; void setActiveUnit(IUnit *unit); IPhrase * activePhrase() const; void setActivePhrase(IPhrase *phrase) override; IPhrase::Type phraseType() const; void setPhraseType(IPhrase::Type type); bool hasPreviousPhrase() const; bool hasNextPhrase() const; Q_INVOKABLE void switchToPreviousPhrase(); Q_INVOKABLE void switchToNextPhrase(); Q_INVOKABLE void updateCourseFromSkeleton(); TrainingAction * activeAction() const override; QVector trainingActions() const override; private Q_SLOTS: void updateDisplayedUnit(); Q_SIGNALS: void editSkeletonChanged(); void skeletonModeChanged(); void skeletonChanged(); void languageChanged(); void displayedCourseChanged(); void unitChanged(); private: Q_DISABLE_COPY(EditorSession) void updateTrainingActions(); IEditableRepository * m_repository{ nullptr }; bool m_editSkeleton{ false }; IEditableCourse *m_skeleton{ nullptr }; ILanguage *m_language{ nullptr }; IEditableCourse *m_course{ nullptr }; QVector m_actions; int m_indexUnit{-1}; int m_indexPhrase{-1}; }; #endif diff --git a/src/qml/EditCoursePage.qml b/src/qml/EditCoursePage.qml index cfaf10f..f4e30ed 100644 --- a/src/qml/EditCoursePage.qml +++ b/src/qml/EditCoursePage.qml @@ -1,195 +1,183 @@ /* * Copyright 2013-2019 Andreas Cord-Landwehr * * 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 . */ import QtQuick 2.5 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.2 import QtQml.Models 2.2 import org.kde.kirigami 2.7 as Kirigami import artikulate 1.0 Kirigami.ScrollablePage { id: root title: i18n("Edit Course") actions { left: Kirigami.Action { text: i18n("Previous") tooltip: i18n("Switch to previous phrase.") iconName: "go-previous" enabled: g_editorSession.hasPreviousPhrase onTriggered: g_editorSession.switchToPreviousPhrase() } right: Kirigami.Action { text: i18n("Next") tooltip: i18n("Switch to next phrase.") iconName: "go-next" enabled: g_editorSession.hasNextPhrase onTriggered: g_editorSession.switchToNextPhrase() } } ColumnLayout { id: main - anchors { - fill: parent - topMargin: 20 - rightMargin: 20 - bottomMargin: 20 - leftMargin: 20 - } spacing: 10 LanguageModel { id: languageModel view: LanguageModel.AllLanguages resourceModel: LanguageResourceModel { repository: g_repository } } CourseModel { id: courseModel } UnitModel { id: selectedUnitModel course: g_editorSession.displayedCourse } RowLayout { Label { text: i18n("Course Prototype:") } ComboBox { Layout.minimumWidth: 300 model: SkeletonModel { id: skeletonModel } textRole: "title" onCurrentIndexChanged: { g_editorSession.skeleton = skeletonModel.skeleton(currentIndex) } } Button { id: buttonEditSkeleton Layout.minimumWidth: 200 text: i18n("Edit Prototype") icon.name: "code-class" checkable: true enabled: g_editorSession.skeletonMode onClicked: g_editorSession.editSkeleton = checked } Item { Layout.fillWidth: true } Button { id: buttonSyncFromSkeleton enabled: !buttonEditSkeleton.checked Layout.minimumWidth: 200 text: i18n("Sync Prototype") icon.name: "view-refresh" ToolTip.visible: hovered ToolTip.delay: 1000 ToolTip.timeout: 5000 ToolTip.text: i18n("Update the course with elements from prototype.") onClicked: g_editorSession.updateCourseFromSkeleton() } } RowLayout { id: languageRow Label { text: i18n("Language:") } ComboBox { id: languageSelectionComboBox Layout.minimumWidth: 200 Layout.fillWidth: true enabled: !buttonEditSkeleton.checked model: languageModel textRole: "i18nTitle" onCurrentIndexChanged: { g_editorSession.setCourseByLanguage(languageModel.language(currentIndex)) } } } RowLayout { id: createNewCourseRow visible: { if (buttonEditSkeleton.checked || g_editorSession.displayedCourse !== null) { return false } return true } Label { text: i18n("There is no course in the selected language.") } ComboBox { // course selection only necessary when we do not edit skeleton derived course id: comboCourse visible: !g_editorSession.skeletonMode Layout.fillWidth: true model: CourseFilterModel { id: courseFilterModel courseModel: courseModel language: g_editorSession.language } textRole: "title" onCurrentIndexChanged: { if (courseFilterModel.course(currentIndex)) { g_editorSession.course = courseFilterModel.course(languageSelectionComboBox.currentIndex) } } onVisibleChanged: { if (visible && courseFilterModel.course(currentIndex)) { g_editorSession.course = courseFilterModel.course(languageSelectionComboBox.currentIndex) } } } Button { text: i18n("Create Course") icon.name: "journal-new" onClicked: { g_editorSession.course = g_repository.createCourse(languageModel.language(languageSelectionComboBox.currentIndex), g_editorSession.skeleton) } } Button { // add units only if skeleton id: newUnitButton visible: !g_editorSession.skeletonMode || g_editorSession.editSkeleton icon.name: "list-add" text: i18n("New Unit") onClicked: phraseModel.course.createUnit() } } ColumnLayout { - UnitEditor { - visible: g_editorSession.unit !== null && g_editorSession.phrase === null - unit: g_editorSession.unit - editPhrases: g_editorSession.skeletonMode && g_editorSession.editSkeleton - } PhraseEditor { visible: g_editorSession.phrase !== null phrase: g_editorSession.phrase isSkeletonPhrase: g_editorSession.editSkeleton Layout.minimumWidth: Math.floor(main.width * 0.6) Layout.fillHeight: true } } } } diff --git a/src/qml/EditorDrawer.qml b/src/qml/EditorDrawer.qml index 9e2c9ab..79b6c83 100644 --- a/src/qml/EditorDrawer.qml +++ b/src/qml/EditorDrawer.qml @@ -1,147 +1,147 @@ /* * Copyright 2018 Andreas Cord-Landwehr * * 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 . */ import QtQuick 2.5 import QtQuick.Layouts 1.3 import QtQuick.Controls 2.0 as QQC2 import org.kde.kirigami 2.7 as Kirigami import artikulate 1.0 Kirigami.GlobalDrawer { id: root title: "Editor" titleIcon: "artikulate" resetMenuOnTriggered: false bottomPadding: 0 property QtObject pageStack // enforce drawer always to be open modal: false handleVisible: false topContent: [ ColumnLayout { spacing: 0 Layout.fillWidth: true Layout.leftMargin: -root.leftPadding Layout.rightMargin: -root.rightPadding ActionListItem { action: Kirigami.Action { text: i18n("Courses") iconName: "artikulate" onTriggered: { root.pageStack.clear(); root.pageStack.push(welcomePageComponent); } } } ActionListItem { action: Kirigami.Action { text: i18n("Repository") - iconName: "document-edit" + iconName: "folder-sync" onTriggered: { root.pageStack.clear(); root.pageStack.push(repositoryPageComponent); } } } Kirigami.Separator { Layout.fillWidth: true } } ] // ordinary Kirigami actions are filled from training units/phrases actions: trainingActions.actions DrawerTrainingActions { id: trainingActions session: g_editorSession onTriggerTrainingView: { root.pageStack.clear(); root.pageStack.push(editCoursePageComponent); } } //TODO integrate again // [ // Kirigami.Action { // text: i18n("Help") // iconName: "help-about" // Kirigami.Action { // text: i18n("Artikulate Handbook") // iconName: "help-contents" // onTriggered: { // triggerAction("help_contents"); // globalDrawer.resetMenu(); // } // } // Kirigami.Action { // text: i18n("Report Bug") // iconName: "tools-report-bug" // onTriggered: { // triggerAction("help_report_bug"); // globalDrawer.resetMenu(); // } // } // Kirigami.Action { // text: i18n("About KDE") // iconName: "help-about" // onTriggered: { // triggerAction("help_about_kde") // globalDrawer.resetMenu(); // } // } // } // ] ColumnLayout { spacing: 0 Layout.fillWidth: true Layout.leftMargin: -root.leftPadding Layout.rightMargin: -root.rightPadding Kirigami.Separator { Layout.fillWidth: true } //TODO planned but not implemented // ActionListItem { // action: Kirigami.Action { // text: i18n("Upload Training") // iconName: "get-hot-new-stuff" // onTriggered: { // root.pageStack.pop(); // root.pageStack.push(downloadPageComponent); // } // } // } ActionListItem { action: Kirigami.Action { text: i18n("About") iconName: "help-about" onTriggered: { root.pageStack.pop(); root.pageStack.push(aboutPageComponent); } } } } } diff --git a/src/qml/PhraseEditor.qml b/src/qml/PhraseEditor.qml index 1e6ae54..c48a934 100644 --- a/src/qml/PhraseEditor.qml +++ b/src/qml/PhraseEditor.qml @@ -1,165 +1,183 @@ /* * Copyright 2013-2015 Andreas Cord-Landwehr * * 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 . */ import QtQuick 2.10 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.2 import artikulate 1.0 Item { id: root property Phrase phrase property bool isSkeletonPhrase: false // use for saving property int __changedPhraseType property string __changedPhraseText width: 500 height: editLoader.height Component { id: editComponent Row { width: root.width height: { if (!root.isSkeletonPhrase) textEdit.height + phonemeGrid.height + phraseEditStateSetter.height + phraseRecorder.height + phraseTypeSetter.height; else { // height if only editing skeleton textEdit.height + phraseTypeSetter.height; } } ColumnLayout { id: textEdit height: inputLine.height + originalPhraseInfo.height width: parent.width spacing: 5 Row { id: originalPhraseInfo property string originalPhrase : (root.phrase != null) ? root.phrase.i18nText : "" spacing: 10 visible: { root.phrase != null && originalPhrase != "" && !root.isSkeletonPhrase} Text { text: i18n("Original Phrase: %1", originalPhraseInfo.originalPhrase) width: root.width - 70 wrapMode: Text.WordWrap } } RowLayout { // controls for setting phrase id: inputLine TextArea { id: phraseInput property Phrase phrase: root.phrase Layout.fillWidth: true Layout.maximumHeight: 100 text: root.phrase.text onTextChanged: { if (root.phrase == null) { return } root.phrase.text = text } onPhraseChanged: { if (root.phrase != null) text = root.phrase.text else text = "" } } } PhraseEditorTypeComponent { id: phraseTypeSetter phrase: root.phrase } PhraseEditorSoundComponent { id: phraseRecorder visible: !root.isSkeletonPhrase phrase: root.phrase } Component { id: phonemeItem Text { Button { width: 100 text: model.title checkable: true checked: { phrase != null && phrase.hasPhoneme(model.dataRole) } onClicked: { //TODO this button has no undo operation yet if (checked) { phrase.addPhoneme(model.dataRole) } else { phrase.removePhoneme(model.dataRole) } } } } } GridView { id: phonemeGrid property int columns : width / cellWidth width: root.width height: 30 * count / columns + 60 cellWidth: 100 cellHeight: 30 model: PhonemeModel { language: g_editorSession.language } delegate: phonemeItem } RowLayout { id: controls - anchors { - left: parent.left - right: parent.right - } PhraseEditorEditStateComponent { id: phraseEditStateSetter visible: !root.isSkeletonPhrase phrase: root.phrase } Label { // dummy Layout.fillWidth: true } } } } } ColumnLayout { id: phraseRow + Row { + Text { + text: i18n("Unit Title:") + } + TextField { + width: 300 + text: g_editorSession.unit.title + onEditingFinished: { + g_editorSession.unit.title = text + } + } + Button { // add units only if skeleton + id: newUnitButton + icon.name: "list-add" + text: i18n("Create Phrase") + onClicked: phraseModel.course.createPhrase(unit) + } + Item { //dummy + Layout.fillHeight: true + } + } + Loader { id: editLoader sourceComponent: (phrase != null) ? editComponent : undefined onSourceComponentChanged: { if (sourceComponent == undefined) height = 0 else height = editComponent.height } } } } diff --git a/src/qml/UnitEditor.qml b/src/qml/UnitEditor.qml deleted file mode 100644 index 9e4defd..0000000 --- a/src/qml/UnitEditor.qml +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2013-2015 Andreas Cord-Landwehr - * - * 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 . - */ - -import QtQuick 2.1 -import QtQuick.Controls 1.2 -import QtQuick.Layouts 1.2 -import artikulate 1.0 - -Item { - id: root - Layout.fillWidth: true - Layout.fillHeight: true - height: 50 - - property Unit unit - property bool editPhrases : false - - ColumnLayout { - id: row - - RowLayout { - Text { - text: i18n("Unit Title:") - } - TextField { - width: 300 - text: unit.title - onEditingFinished: { - unit.title = text - } - } - } - Button { // add units only if skeleton - id: newUnitButton - visible: root.editPhrases - iconName: "list-add" - text: i18n("Create Phrase") - onClicked: phraseModel.course.createPhrase(unit) - } - Item { //dummy - Layout.fillHeight: true - } - } -} - diff --git a/src/qml/WelcomePageEditor.qml b/src/qml/WelcomePageEditor.qml index eb3463d..deed4e9 100644 --- a/src/qml/WelcomePageEditor.qml +++ b/src/qml/WelcomePageEditor.qml @@ -1,85 +1,84 @@ /* * Copyright 2015-2019 Andreas Cord-Landwehr * * 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 . */ import QtQuick 2.1 import QtQuick.Controls 2.1 as QQC2 import QtQuick.Layouts 1.3 import org.kde.kirigami 2.7 as Kirigami import artikulate 1.0 Kirigami.ScrollablePage { id: root title: i18n("Welcome to Artikulate Course Editor") Kirigami.CardsListView { id: listView width: root.width - 40 model: CourseModel { id: courseModel } delegate: Kirigami.AbstractCard { contentItem: Item { - implicitWidth: delegateLayout.implicitWidth implicitHeight: delegateLayout.implicitHeight GridLayout { id: delegateLayout anchors { left: parent.left top: parent.top right: parent.right } rowSpacing: Kirigami.Units.largeSpacing columnSpacing: Kirigami.Units.largeSpacing columns: width > Kirigami.Units.gridUnit * 20 ? 4 : 2 Kirigami.Icon { source: "language-artikulate" Layout.fillHeight: true Layout.maximumHeight: Kirigami.Units.iconSizes.huge Layout.preferredWidth: height } ColumnLayout { Kirigami.Heading { level: 2 text: i18nc("@title:window language / course name", "%1 / %2", model.language.title, model.title) } Kirigami.Separator { Layout.fillWidth: true } QQC2.Label { Layout.fillWidth: true wrapMode: Text.WordWrap text: model.description } } QQC2.Button { Layout.alignment: Qt.AlignRight|Qt.AlignVCenter Layout.columnSpan: 2 text: i18nc("@action:button", "Select Course") onClicked: { showPassiveNotification("Selected course for editor: " + model.title + "."); g_editorSession.course = model.dataRole } } } } } } } diff --git a/src/resources.qrc b/src/resources.qrc index 8d93365..91dd249 100644 --- a/src/resources.qrc +++ b/src/resources.qrc @@ -1,29 +1,28 @@ qml/ActionListItem.qml qml/ArtikulateDrawer.qml qml/DownloadPage.qml qml/EditCoursePage.qml qml/Editor.qml qml/EditorDrawer.qml qml/Main.qml qml/PhonemeUnitSelector.qml qml/PhraseEditor.qml qml/PhraseEditorEditStateComponent.qml qml/PhraseEditorSoundComponent.qml qml/PhraseEditorTypeComponent.qml qml/ProfileSelector.qml qml/ProfileSettingsPage.qml qml/ProfileUserImageItem.qml qml/SoundPlayer.qml qml/SoundRecorder.qml qml/TrainerCourseStatistics.qml qml/TrainingPage.qml - qml/UnitEditor.qml qml/WelcomePage.qml qml/WelcomePageEditor.qml artikulate.knsrc