Warning, /education/kwordquiz/src/qml/DeckEditorPage.qml is written in an unsupported language. File is not indexed.

0001 // SPDX-FileCopyrightText: 2023 Carl Schwan <carl@carlschwan.eu>
0002 // SPDX-License-Identifier: LGPL-2.0-or-later
0003 
0004 import QtQuick 2.15
0005 import QtQuick.Layouts 1.15
0006 import QtQuick.Controls 2.15 as QQC2
0007 import Qt.labs.platform 1.1
0008 import org.kde.kirigami 2.20 as Kirigami
0009 import org.kde.kwordquiz 1.0
0010 import org.kde.kitemmodels 1.0
0011 
0012 Kirigami.ScrollablePage {
0013     id: root
0014 
0015     property DocumentModel documentModel
0016     property int mode: DeckEditorPage.CreateMode
0017     property CardModel editorModel: CardModel {}
0018     property string filterText: ''
0019 
0020     readonly property bool editPage: true
0021 
0022     enum Mode {
0023         EditMode,
0024         CreateMode
0025     }
0026 
0027     Component.onCompleted: if (mode === DeckEditorPage.CreateMode) {
0028         root.editorModel.createNew();
0029     }
0030 
0031     title: if (mode === DeckEditorPage.CreateMode) {
0032         i18nc("@title:window", "Create Deck")
0033     } else {
0034         i18nc("@title:window", "Edit Deck")
0035     }
0036 
0037     actions: [
0038         Kirigami.Action {
0039             text: i18n("Filter")
0040             displayComponent: Kirigami.SearchField {
0041                 focus: false
0042                 placeholderText: i18n("Filter...")
0043                 onTextChanged: {
0044                     root.filterText = text;
0045                     filterProxy.invalidate();
0046                 }
0047             }
0048         },
0049         Kirigami.Action {
0050             text: i18nc("@action:button", "Print Preview")
0051             icon.name: "document-print-preview"
0052 
0053             Kirigami.Action {
0054                 text: i18n("Flashcard")
0055                 icon.name: "org.kde.kwordquiz"
0056                 onTriggered: exporter.printPreview(Exporter.Flashcard)
0057             }
0058 
0059             Kirigami.Action {
0060                 text: i18n("List")
0061                 icon.name: "view-list-text"
0062                 onTriggered: exporter.printPreview(Exporter.List)
0063             }
0064 
0065             Kirigami.Action {
0066                 text: i18n("Exam")
0067                 icon.name: "table"
0068                 onTriggered: exporter.printPreview(Exporter.Exam)
0069             }
0070         },
0071         Kirigami.Action {
0072             text: i18nc("@action:button", "Print")
0073             icon.name: "document-print"
0074 
0075             Kirigami.Action {
0076                 text: i18n("Flashcard")
0077                 onTriggered: exporter.print(Exporter.Flashcard)
0078                 icon.name: "org.kde.kwordquiz"
0079             }
0080 
0081             Kirigami.Action {
0082                 text: i18n("List")
0083                 icon.name: "view-list-text"
0084                 onTriggered: exporter.print(Exporter.List)
0085             }
0086 
0087             Kirigami.Action {
0088                 text: i18n("Exam")
0089                 icon.name: "table"
0090                 onTriggered: exporter.print(Exporter.Exam)
0091             }
0092         },
0093         Kirigami.Action {
0094             text: mode === DeckEditorPage.CreateMode ? i18nc("@action:button", "Create") : i18nc("@action:button", "Save")
0095             icon.name: "document-save"
0096             onTriggered: {
0097                 root.editorModel.save();
0098                 if (mode === DeckEditorPage.CreateMode) {
0099                     root.documentModel.add(root.editorModel.document);
0100                 } else {
0101                     root.editorModel.reloaded();
0102                 }
0103                 applicationWindow().pageStack.layers.pop();
0104             }
0105         }
0106     ]
0107 
0108     component FileSelectorButton: QQC2.ToolButton {
0109         id: fileButton
0110 
0111         required property string file
0112         required property bool isImage
0113         readonly property bool isSound: !isImage
0114 
0115         signal fileRemoved()
0116         signal fileAdded(url file)
0117 
0118         icon.name: file ? "delete" : (isImage ? "insert-image" : "folder-music")
0119         text: if (isImage) {
0120             file ? i18nc("@action:button", "Remove Image") : i18nc("@action:button", "Link Image")
0121         } else {
0122             file ? i18nc("@action:button", "Remove Sound") : i18nc("@action:button", "Link Sound")
0123         }
0124         display: QQC2.ToolButton.IconOnly
0125 
0126         QQC2.ToolTip.text: text
0127         QQC2.ToolTip.visible: hovered
0128         QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
0129 
0130         onClicked: {
0131             if (file) {
0132                 fileButton.fileRemoved();
0133                 return;
0134             }
0135 
0136             var dialog = isImage
0137                 ? imageFileDialog.createObject(QQC2.ApplicationWindow.Overlay)
0138                 : soundFileDialog.createObject(QQC2.ApplicationWindow.Overlay)
0139 
0140             dialog.accepted.connect(() => {
0141                 if (!dialog.file) {
0142                     return;
0143                 }
0144                 fileButton.fileAdded(dialog.file);
0145             });
0146             dialog.open();
0147         }
0148     }
0149 
0150     component LanguageSelectorButton: QQC2.ToolButton {
0151         id: button
0152 
0153         property string language
0154 
0155         QQC2.ToolTip.text: i18nc("@action:button", "Set language")
0156         QQC2.ToolTip.visible: hovered
0157         QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
0158 
0159         font.family: language.length > 0 ? "emoji" : Kirigami.Theme.defaultFont.family
0160 
0161         text: language.length > 0 ? LanguageListModel.flagFromName(language) : i18nc("@action:button", "Set Language")
0162         display: language.length > 0 ? QQC2.ToolButton.TextOnly : QQC2.ToolButton.IconOnly
0163         icon.name: language.length > 0 ? "" : "language-chooser"
0164 
0165         onClicked: {
0166             var page  = applicationWindow().pageStack.pushDialogLayer(languageSelectorPage, {
0167                 width: Kirigami.Units.gridUnits * 10,
0168                 height: Kirigami.Units.gridUnits * 12,
0169                 language: button.language,
0170             });
0171 
0172             page.languageChanged.connect(() => {
0173                 button.language = page.language;
0174             });
0175         }
0176     }
0177 
0178     Component {
0179         id: imageFileDialog
0180 
0181         FileDialog {
0182             title: i18n("Please Choose an Image")
0183             folder: StandardPaths.writableLocation(StandardPaths.PicturesLocation)
0184         }
0185     }
0186 
0187     Component {
0188         id: soundFileDialog
0189 
0190         FileDialog {
0191             title: i18n("Please Choose a Sound")
0192             folder: StandardPaths.writableLocation(StandardPaths.MusicLocation)
0193         }
0194     }
0195 
0196     Component {
0197         id: languageSelectorPage
0198 
0199         LanguageSelectorPage {}
0200     }
0201 
0202     ListView {
0203         id: listView
0204 
0205         header: QQC2.Pane {
0206             width: parent.width
0207 
0208             Kirigami.Theme.colorSet: Kirigami.Theme.Window
0209             Kirigami.Theme.inherit: false
0210 
0211             function focusQuestionField() {
0212                 identifierLeftField.forceActiveFocus();
0213             }
0214 
0215             function focusAnswerField() {
0216                 identifierRightField.forceActiveFocus();
0217             }
0218 
0219             contentItem: ColumnLayout {
0220                 GridLayout {
0221                     Layout.fillWidth: true
0222 
0223                     columns: 2
0224 
0225                     QQC2.Label {
0226                         text: i18nc("@label", "Name:")
0227                     }
0228 
0229                     QQC2.TextField {
0230                         Layout.fillWidth: true
0231                         text: root.editorModel.title
0232                         onTextChanged: root.editorModel.title = text;
0233                         enabled: root.editorModel.enabled
0234                     }
0235 
0236                     QQC2.Label {
0237                         text: i18nc("@label", "Author:")
0238                     }
0239 
0240                     QQC2.TextField {
0241                         Layout.fillWidth: true
0242                         text: root.editorModel.author
0243                         onTextChanged: root.editorModel.author = text;
0244                         enabled: root.editorModel.enabled
0245                     }
0246 
0247                     QQC2.Label {
0248                         text: i18nc("@label", "License:")
0249                     }
0250 
0251                     QQC2.ComboBox {
0252                         Layout.fillWidth: true
0253                         editText: root.editorModel.license
0254                         onCurrentIndexChanged: if (root.editorModel.document) {
0255                             root.editorModel.license = currentIndex;
0256                         }
0257                         currentIndex: 0
0258                         Component.onCompleted: if (root.editorModel.license.length === 0) {
0259                             editText = currentValue;
0260                         }
0261                         enabled: root.editorModel.enabled
0262                         editable: true
0263                         model: [
0264                             'CC-BY-SA-4.0',
0265                             'CC-BY-4.0',
0266                             'CC0-1.0',
0267                         ]
0268                     }
0269                 }
0270 
0271                 Kirigami.Separator {
0272                     Layout.fillWidth: true
0273                 }
0274 
0275                 RowLayout {
0276                     Layout.fillWidth: true
0277 
0278                     QQC2.TextField {
0279                         id: identifierLeftField
0280                         text: root.editorModel.identifierLeft
0281                         background: null
0282                         onEditingFinished: root.editorModel.identifierLeft = text
0283                         enabled: root.editorModel.enabled
0284 
0285                         Layout.fillWidth: true
0286                         font.bold: true
0287                         Keys.onDownPressed: {
0288                             const item = listView.itemAtIndex(0);
0289                             if (item) {
0290                                 item.focusQuestionField();
0291                             }
0292                         }
0293                         onAccepted: identifierRightField.forceActiveFocus()
0294                     }
0295 
0296                     LanguageSelectorButton {
0297                         id: identifierLeftLanguage
0298                         onLanguageChanged: {
0299                             editorModel.langQuestion = language;
0300                             root.editorModel.identifierLeft = LanguageListModel.languageName(language);
0301                         }
0302                     }
0303 
0304                     Kirigami.Separator {
0305                         Layout.fillHeight: true
0306                         Layout.preferredWidth: 1
0307                     }
0308 
0309                     QQC2.TextField {
0310                         id: identifierRightField
0311                         text: root.editorModel.identifierRight
0312                         background: null
0313                         onEditingFinished: root.editorModel.identifierRight = text
0314                         enabled: root.editorModel.enabled
0315 
0316                         Layout.fillWidth: true
0317                         font.bold: true
0318                         Keys.onDownPressed: {
0319                             const item = listView.itemAtIndex(0);
0320                             if (item) {
0321                                 item.focusAnswerField();
0322                             }
0323                         }
0324 
0325                         onAccepted: {
0326                             const item = listView.itemAtIndex(0);
0327                             if (item) {
0328                                 item.focusQuestionField();
0329                             }
0330                         }
0331                     }
0332 
0333                     LanguageSelectorButton {
0334                         id: identifierRightLanguage
0335                         onLanguageChanged: {
0336                             editorModel.langAnswer = language;
0337                             root.editorModel.identifierRight = LanguageListModel.languageName(language);
0338                         }
0339                     }
0340                 }
0341             }
0342         }
0343 
0344         footer: QQC2.ItemDelegate {
0345             id: footer
0346 
0347             function insertRow() {
0348                 if (newQuestionField.text.length === 0 || newAnswerField.text.length === 0) {
0349                     return;
0350                 }
0351 
0352                 root.editorModel.add(newQuestionField.text, newAnswerField.text);
0353                 newQuestionField.text = '';
0354                 newAnswerField.text = '';
0355             }
0356 
0357             function focusQuestionField() {
0358                 newQuestionField.forceActiveFocus();
0359             }
0360 
0361             function focusAnswerField() {
0362                 newAnswerField.forceActiveFocus();
0363             }
0364 
0365             width: parent.width
0366 
0367             contentItem: RowLayout {
0368                 QQC2.TextField {
0369                     id: newQuestionField
0370                     background: null
0371                     placeholderText: root.editorModel.identifierLeft
0372                     enabled: root.editorModel.enabled
0373                     onEditingFinished: footer.insertRow();
0374                     onAccepted: newAnswerField.forceActiveFocus();
0375 
0376                     Layout.fillWidth: true
0377                     Keys.onUpPressed: {
0378                         const item = listView.itemAtIndex(listView.count - 1);
0379                         if (item) {
0380                             item.focusQuestionField();
0381                         }
0382                     }
0383                 }
0384 
0385                 Kirigami.Separator {
0386                     Layout.fillHeight: true
0387                     Layout.preferredWidth: 1
0388                 }
0389 
0390                 QQC2.TextField {
0391                     id: newAnswerField
0392                     background: null
0393                     placeholderText: root.editorModel.identifierRight
0394                     onEditingFinished: footer.insertRow()
0395                     onAccepted: newQuestionField.forceActiveFocus();
0396                     enabled: root.editorModel.enabled
0397 
0398                     Layout.fillWidth: true
0399                     Keys.onUpPressed: {
0400                         const item = listView.itemAtIndex(listView.count - 1);
0401                         if (item) {
0402                             item.focusAnswerField();
0403                         }
0404                     }
0405                 }
0406             }
0407         }
0408 
0409         model: KSortFilterProxyModel {
0410             id: filterProxy
0411             sourceModel: root.editorModel
0412             filterRowCallback: function(sourceRow, sourceParent) {
0413                 let question = sourceModel.data(sourceModel.index(sourceRow, 0, sourceParent), CardModel.QuestionRole);
0414                 let answer = sourceModel.data(sourceModel.index(sourceRow, 0, sourceParent), CardModel.AnswerRole);
0415 
0416                 return question.includes(filterText) || answer.includes(filterText);
0417             }
0418         }
0419 
0420         delegate: ColumnLayout {
0421             id: editorDelegate
0422 
0423             required property int index
0424 
0425             required property string question
0426             required property string questionImage
0427             required property string questionSound
0428             required property string answer
0429             required property string answerImage
0430             required property string answerSound
0431 
0432             function focusQuestionField() {
0433                 questionField.forceActiveFocus();
0434             }
0435 
0436             function focusAnswerField() {
0437                 answerField.forceActiveFocus();
0438             }
0439 
0440             width: ListView.view.width
0441             spacing: 0
0442 
0443             QQC2.ItemDelegate {
0444                 Layout.fillWidth: true
0445 
0446                 contentItem: RowLayout {
0447                     QQC2.TextField {
0448                         id: questionField
0449 
0450                         text: editorDelegate.question
0451                         background: null
0452                         onEditingFinished: root.editorModel.edit(editorDelegate.index, questionField.text, answerField.text)
0453 
0454                         Layout.fillWidth: true
0455                         Keys.onUpPressed: {
0456                             const item = listView.itemAtIndex(editorDelegate.index - 1);
0457                             if (item) {
0458                                 item.focusQuestionField();
0459                             } else {
0460                                 listView.headerItem.focusQuestionField();
0461                             }
0462                         }
0463                         Keys.onDownPressed: {
0464                             const item = listView.itemAtIndex(editorDelegate.index + 1);
0465                             if (item) {
0466                                 item.focusQuestionField();
0467                             } else {
0468                                 listView.footerItem.focusQuestionField();
0469                             }
0470                         }
0471                         onAccepted: answerField.forceActiveFocus()
0472                     }
0473 
0474                     FileSelectorButton {
0475                         isImage: true
0476                         file: editorDelegate.questionImage
0477                         onFileRemoved: root.editorModel.removeQuestionImage(editorDelegate.index);
0478                         onFileAdded: file => root.editorModel.addQuestionImage(editorDelegate.index, file);
0479                     }
0480 
0481                     FileSelectorButton {
0482                         isImage: false
0483                         file: editorDelegate.questionSound
0484                         onFileRemoved: root.editorModel.removeQuestionSound(editorDelegate.index);
0485                         onFileAdded: root.editorModel.addQuestionSound(editorDelegate.index, file);
0486                     }
0487 
0488                     Kirigami.Separator {
0489                         Layout.fillHeight: true
0490                         Layout.preferredWidth: 1
0491                     }
0492 
0493                     QQC2.TextField {
0494                         id: answerField
0495 
0496                         text: editorDelegate.answer
0497                         background: null
0498                         onEditingFinished: root.editorModel.edit(editorDelegate.index, questionField.text, answerField.text)
0499 
0500                         Layout.fillWidth: true
0501                         Keys.onUpPressed: {
0502                             const item = listView.itemAtIndex(editorDelegate.index - 1);
0503                             if (item) {
0504                                 item.focusAnswerField();
0505                             } else {
0506                                 root.listView.headerItem.focusAnswerField();
0507                             }
0508                         }
0509                         Keys.onDownPressed: {
0510                             const item = listView.itemAtIndex(editorDelegate.index + 1);
0511                             if (item) {
0512                                 item.focusAnswerField();
0513                             } else {
0514                                 listView.footerItem.focusAnswerField();
0515                             }
0516                         }
0517                         onAccepted: {
0518                             const item = listView.itemAtIndex(editorDelegate.index + 1);
0519                             if (item) {
0520                                 item.focusQuestionField();
0521                             } else {
0522                                 root.listView.headerItem.focusQuestionField();
0523                             }
0524                         }
0525 
0526                     }
0527 
0528                     FileSelectorButton {
0529                         isImage: true
0530                         file: editorDelegate.answerImage
0531                         onFileRemoved: root.editorModel.removeAnswerImage(editorDelegate.index);
0532                         onFileAdded: root.editorModel.addAnswerImage(editorDelegate.index, file);
0533                     }
0534 
0535                     FileSelectorButton {
0536                         isImage: false
0537                         file: editorDelegate.answerSound
0538                         onFileRemoved: root.editorModel.removeAnswerSound(editorDelegate.index);
0539                         onFileAdded: root.editorModel.addAnswerSound(editorDelegate.index, file);
0540                     }
0541                 }
0542             }
0543 
0544             Kirigami.Separator {
0545                 Layout.fillWidth: true
0546             }
0547         }
0548     }
0549 
0550     Exporter {
0551         id: exporter
0552         cardModel: root.editorModel
0553     }
0554 }