diff --git a/src/editor/resourceeditor.cpp b/src/editor/resourceeditor.cpp index e296eba..2f9e360 100644 --- a/src/editor/resourceeditor.cpp +++ b/src/editor/resourceeditor.cpp @@ -1,636 +1,637 @@ /* * Copyright 2012 Sebastian Gottfried * * 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) any later version. * * 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 . */ #include "resourceeditor.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "core/dataindex.h" #include "core/dataaccess.h" #include "core/resource.h" #include "core/course.h" #include "core/lesson.h" #include "core/keyboardlayout.h" #include "core/resourcedataaccess.h" #include "core/userdataaccess.h" #include "models/resourcemodel.h" #include "models/categorizedresourcesortfilterproxymodel.h" #include "resourceeditorwidget.h" #include "newresourceassistant.h" #include "application.h" ResourceEditor::ResourceEditor(QWidget *parent) : KMainWindow(parent), m_dataIndex(Application::dataIndex()), m_resourceModel(new ResourceModel(this)), m_categorizedResourceModel(new CategorizedResourceSortFilterProxyModel(this)), m_currentResource(0), m_backupResource(0), m_undoGroup(new QUndoGroup(this)), m_actionCollection(new KActionCollection(this)), m_newResourceAction(KStandardAction::openNew(this, SLOT(newResource()), m_actionCollection)), m_deleteResourceAction(new QAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18n("Delete"), this)), m_undoAction(KStandardAction::undo(m_undoGroup, SLOT(undo()), m_actionCollection)), m_redoAction(KStandardAction::redo(m_undoGroup, SLOT(redo()), m_actionCollection)), m_importResourceAction(new QAction(QIcon::fromTheme(QStringLiteral("document-import")), i18n("Import"), this)), m_exportResourceAction(new QAction(QIcon::fromTheme(QStringLiteral("document-export")), i18n("Export"), this)), m_editorWidget(new ResourceEditorWidget(this)), m_saveTimer(new QTimer(this)) { m_resourceModel->setDataIndex(m_dataIndex); m_categorizedResourceModel->setResourceModel(m_resourceModel); m_categorizedResourceModel->setCategorizedModel(true); - setMinimumSize(700, 500); + const int unit = fontMetrics().height(); + setMinimumSize(39 * unit, 28 * unit); setCaption(i18n("Course and Keyboard Layout Editor")); m_newResourceAction->setToolTip(i18n("Create a new course or keyboard layout")); m_deleteResourceAction->setEnabled(false); m_deleteResourceAction->setToolTip(i18n("Delete the current selected course or keyboard layout")); m_actionCollection->addAction(QStringLiteral("deleteResource"), m_deleteResourceAction); connect(m_deleteResourceAction, &QAction::triggered, this, &ResourceEditor::deleteResource); m_undoAction->setEnabled(false); connect(m_undoGroup, &QUndoGroup::canUndoChanged, m_undoAction, &QAction::setEnabled); connect(m_undoGroup, &QUndoGroup::undoTextChanged, this, &ResourceEditor::setUndoText); m_redoAction->setEnabled(false); connect(m_undoGroup, &QUndoGroup::canRedoChanged, m_redoAction, &QAction::setEnabled); connect(m_undoGroup, &QUndoGroup::redoTextChanged, this, &ResourceEditor::setRedoText); m_actionCollection->addAction(QStringLiteral("importResource"), m_importResourceAction); m_importResourceAction->setToolTip(i18n("Import a course or keyboard layout from a file")); connect(m_importResourceAction, &QAction::triggered, this, &ResourceEditor::importResource); m_exportResourceAction->setToolTip(i18n("Export the current selected course or keyboard layout to a file")); m_actionCollection->addAction(QStringLiteral("exportResource"), m_exportResourceAction); connect(m_exportResourceAction, &QAction::triggered, this, &ResourceEditor::exportResource); m_exportResourceAction->setEnabled(false); toolBar()->addAction(m_newResourceAction); toolBar()->addAction(m_deleteResourceAction); toolBar()->addSeparator(); toolBar()->addAction(m_undoAction); toolBar()->addAction(m_redoAction); toolBar()->addSeparator(); toolBar()->addAction(m_importResourceAction); toolBar()->addAction(m_exportResourceAction); setCentralWidget(m_editorWidget); m_editorWidget->setResourceModel(m_resourceModel); m_editorWidget->setUndoGroup(m_undoGroup); QAbstractItemView* const resourceView = m_editorWidget->resourceView(); resourceView->setModel(m_categorizedResourceModel); connect(resourceView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &ResourceEditor::onResourceSelected); selectFirstResource(); connect(m_editorWidget, &ResourceEditorWidget::resourceRestorationRequested, this, &ResourceEditor::restoreResourceBackup); connect(m_editorWidget, &ResourceEditorWidget::resourceRestorationDismissed, this, &ResourceEditor::clearResourceBackup); connect(m_saveTimer, &QTimer::timeout, this, &ResourceEditor::save); m_saveTimer->setInterval(60000); } ResourceEditor::~ResourceEditor() { if (m_backupResource) { delete m_backupResource; } } void ResourceEditor::closeEvent(QCloseEvent* event) { save(); KMainWindow::closeEvent(event); } void ResourceEditor::newResource() { QPointer assistant = new NewResourceAssistant(m_resourceModel, this); if (assistant->exec() == QDialog::Accepted) { save(); Resource* resource = assistant->createResource(); if (Resource* dataIndexResource = storeResource(resource)) { m_editorWidget->clearUndoStackForResource(dataIndexResource); selectDataResource(dataIndexResource); } delete resource; } delete assistant; } void ResourceEditor::deleteResource() { Q_ASSERT(m_currentResource); save(); // HACK: disable categorization temporarily as mitigation against kdelibs bug 303228 m_categorizedResourceModel->setCategorizedModel(false); DataAccess dataAccess; UserDataAccess userDataAccess; if (DataIndexCourse* course = qobject_cast(m_currentResource)) { for (int i = 0; i < m_dataIndex->courseCount(); i++) { if (m_dataIndex->course(i) == course) { Course* backup = new Course(); if (!dataAccess.loadCourse(m_dataIndex->course(i), backup)) { KMessageBox::error(this, i18n("Error while opening course")); return; } if (!userDataAccess.deleteCourse(backup)) { delete backup; KMessageBox::error(this, i18n("Error while deleting course")); return; } prepareResourceRestore(backup); m_dataIndex->removeCourse(i); } } } else if (DataIndexKeyboardLayout* keyboardLayout = qobject_cast(m_currentResource)) { for (int i = 0; i < m_dataIndex->keyboardLayoutCount(); i++) { if (m_dataIndex->keyboardLayout(i) == keyboardLayout) { KeyboardLayout* backup = new KeyboardLayout(); if (!dataAccess.loadKeyboardLayout(m_dataIndex->keyboardLayout(i), backup)) { KMessageBox::error(this, i18n("Error while opening keyboard layout")); return; } if (!userDataAccess.deleteKeyboardLayout(backup)) { KMessageBox::error(this, i18n("Error while deleting keyboard layout")); return; } prepareResourceRestore(backup); m_dataIndex->removeKeyboardLayout(i); } } } // HACK: reactivate categorization again m_categorizedResourceModel->setCategorizedModel(true); selectFirstResource(); } void ResourceEditor::importResource() { const QString path = QFileDialog::getOpenFileName(this, QString(), QString(), i18n("XML files (*.xml)")); if (!path.isNull()) { save(); if (importCourse(path)) return; if (importKeyboardLayout(path)) return; KMessageBox::error(this, i18n("The selected file could not be imported.")); } } void ResourceEditor::exportResource() { Q_ASSERT(m_currentResource); DataAccess dataAccess; ResourceDataAccess resourceDataAccess; save(); if (DataIndexCourse* dataIndexCourse = qobject_cast(m_currentResource)) { for (int i = 0; i < m_dataIndex->courseCount(); i++) { if (m_dataIndex->course(i) == dataIndexCourse) { Course* course = new Course(); if (!dataAccess.loadCourse(m_dataIndex->course(i), course)) { KMessageBox::error(this, i18n("Error while opening course")); delete course; return; } const QString initialFileName(QStringLiteral("%1.xml").arg(course->keyboardLayoutName())); const QString path(QFileDialog::getSaveFileName(this, QString(), initialFileName, i18n("XML files (*.xml)"))); if (!path.isNull()) { if (!resourceDataAccess.storeCourse(path, course)) { KMessageBox::error(this, i18n("Error while saving course")); delete course; return; } } delete course; } } } else if (DataIndexKeyboardLayout* dataIndexkeyboardLayout = qobject_cast(m_currentResource)) { for (int i = 0; i < m_dataIndex->keyboardLayoutCount(); i++) { if (m_dataIndex->keyboardLayout(i) == dataIndexkeyboardLayout) { KeyboardLayout* keyboardlayout = new KeyboardLayout(); if (!dataAccess.loadKeyboardLayout(m_dataIndex->keyboardLayout(i), keyboardlayout)) { KMessageBox::error(this, i18n("Error while opening keyboard layout")); delete keyboardlayout; return; } const QString initialFileName(QStringLiteral("%1.xml").arg(keyboardlayout->name())); const QString path(QFileDialog::getSaveFileName(this, QString(), initialFileName, i18n("XML files (*.xml)"))); if (!path.isNull()) { if (!resourceDataAccess.storeKeyboardLayout(path, keyboardlayout)) { KMessageBox::error(this, i18n("Error while saving keyboard layout")); delete keyboardlayout; return; } } delete keyboardlayout; } } } } void ResourceEditor::onResourceSelected() { QAbstractItemView* const resourceView = m_editorWidget->resourceView(); save(); if (resourceView->selectionModel()->hasSelection()) { QModelIndex current = resourceView->selectionModel()->selectedIndexes().first(); const QVariant variant = resourceView->model()->data(current, ResourceModel::DataRole); QObject* const obj = variant.value(); m_currentResource = qobject_cast(obj); const DataIndex::Source source = static_cast(resourceView->model()->data(current, ResourceModel::SourceRole).toInt()); Q_ASSERT(m_currentResource); m_deleteResourceAction->setEnabled(source == DataIndex::UserResource); m_exportResourceAction->setEnabled(true); m_editorWidget->openResource(m_currentResource); } else { m_currentResource = 0; m_deleteResourceAction->setEnabled(false); m_exportResourceAction->setEnabled(false); } } void ResourceEditor::restoreResourceBackup() { Q_ASSERT(m_backupResource); save(); if (Resource* dataIndexResource = storeResource(m_backupResource)) { selectDataResource(dataIndexResource); } clearResourceBackup(); } void ResourceEditor::clearResourceBackup() { Q_ASSERT(m_backupResource); delete m_backupResource; m_backupResource = 0; } void ResourceEditor::save() { if (m_undoGroup->activeStack() && !m_undoGroup->activeStack()->isClean()) { m_editorWidget->save(); } m_saveTimer->start(); } void ResourceEditor::setUndoText(const QString& text) { m_undoAction->setToolTip(text); } void ResourceEditor::setRedoText(const QString& text) { m_redoAction->setToolTip(text); } void ResourceEditor::prepareResourceRestore(Resource* backup) { QString msg; if (Course* course = qobject_cast(backup)) { msg = i18n("Course %1 deleted", course->title().toHtmlEscaped()); } else if (KeyboardLayout* keyboardLayout = qobject_cast(backup)) { msg = i18n("Keyboard layout %1 deleted", keyboardLayout->title().toHtmlEscaped()); } m_editorWidget->showMessage(ResourceEditorWidget::ResourceDeletedMsg, msg); if (m_backupResource) { delete m_backupResource; } m_backupResource = backup; } Resource* ResourceEditor::storeResource(Resource* resource, Resource* dataIndexResource) { // FIXME: Is all this path mangling necessary? UserDataAccess userDataAccess; if (Course* course = qobject_cast(resource)) { DataIndexCourse* dataIndexCourse = dataIndexResource == 0? new DataIndexCourse(): dynamic_cast(dataIndexResource); QDir dir = QDir(QStandardPaths::writableLocation(QStandardPaths::DataLocation)); dir.mkpath(QStringLiteral("courses")); dir.cd(QStringLiteral("courses")); QString path = dataIndexResource == 0? dir.filePath(QStringLiteral("%1.xml").arg(course->id())): dataIndexCourse->path(); if (!userDataAccess.storeCourse(course)) { KMessageBox::error(this, i18n("Error while saving course to disk.")); return 0; } dataIndexCourse->setSource(DataIndex::UserResource); dataIndexCourse->setTitle(course->title()); dataIndexCourse->setDescription(course->description()); dataIndexCourse->setKeyboardLayoutName(course->keyboardLayoutName()); dataIndexCourse->setId(course->id()); dataIndexCourse->setPath(path); if (dataIndexResource == 0) { m_dataIndex->addCourse(dataIndexCourse); } dataIndexResource = dataIndexCourse; } else if (KeyboardLayout* keyboardLayout = qobject_cast(resource)) { DataIndexKeyboardLayout* dataIndexKeyboardLayout = dataIndexResource == 0? new DataIndexKeyboardLayout(): qobject_cast(dataIndexResource); QDir dir = QDir(QStandardPaths::writableLocation(QStandardPaths::DataLocation)); dir.mkpath(QStringLiteral("keyboardlayouts")); dir.cd(QStringLiteral("keyboardlayouts")); QString path = dataIndexResource == 0? dir.filePath(QStringLiteral("%1.xml").arg(keyboardLayout->id())): dataIndexKeyboardLayout->path(); if (!userDataAccess.storeKeyboardLayout(keyboardLayout)) { KMessageBox::error(this, i18n("Error while saving keyboard layout to disk.")); return 0; } dataIndexKeyboardLayout->setSource(DataIndex::UserResource); dataIndexKeyboardLayout->setName(keyboardLayout->name()); dataIndexKeyboardLayout->setTitle(keyboardLayout->title()); dataIndexKeyboardLayout->setId(keyboardLayout->id()); dataIndexKeyboardLayout->setPath(path); if (dataIndexResource == 0) { m_dataIndex->addKeyboardLayout(dataIndexKeyboardLayout); } dataIndexResource = dataIndexKeyboardLayout; } return dataIndexResource; } void ResourceEditor::selectDataResource(Resource* resource) { QAbstractItemView* const resourceView = m_editorWidget->resourceView(); QAbstractItemModel* const model = resourceView->model(); resourceView->selectionModel()->clearSelection(); for (int i = 0; i < model->rowCount(); i++) { const QModelIndex index = model->index(i, 0); const QVariant var = model->data(index, ResourceModel::DataRole); QObject* obj = var.value(); if (obj == resource) { resourceView->selectionModel()->select(index, QItemSelectionModel::ClearAndSelect); break; } } } void ResourceEditor::selectFirstResource() { QAbstractItemView* const resourceView = m_editorWidget->resourceView(); if (resourceView->model()->rowCount() > 0) { resourceView->selectionModel()->select(resourceView->model()->index(0, 0), QItemSelectionModel::ClearAndSelect); } } bool ResourceEditor::importCourse(const QString& path) { ResourceDataAccess resourceDataAccess; Course course; if (!resourceDataAccess.loadCourse(path, &course)) return false; DataIndexCourse* overwriteDataIndexCourse(0); for (int i = 0; i < m_dataIndex->courseCount(); i++) { DataIndexCourse* const testCourse = m_dataIndex->course(i); if (testCourse->source() == DataIndex::BuiltInResource && testCourse->id() == course.id()) { switch (KMessageBox::questionYesNo(this, i18n("The selected course is already present as a built-in course."), QString(), KGuiItem(i18n("Import as new course"), QStringLiteral("dialog-ok")), KStandardGuiItem::cancel() )) { case KMessageBox::Yes: course.setId(QUuid::createUuid().toString()); for (int j = 0; j < course.lessonCount(); j++) { Lesson* const lesson = course.lesson(j); lesson->setId(QUuid::createUuid().toString()); } break; default: return true; } } if (testCourse->source() == DataIndex::UserResource && testCourse->id() == course.id()) { switch (KMessageBox::questionYesNoCancel(this, i18n("The selected course is already present as a user course."), QString(), KGuiItem(i18n("Import as new course"), QStringLiteral("dialog-ok")), KStandardGuiItem::overwrite(), KStandardGuiItem::cancel() )) { case KMessageBox::Yes: course.setId(QUuid::createUuid().toString()); for (int j = 0; j < course.lessonCount(); j++) { Lesson* const lesson = course.lesson(j); lesson->setId(QUuid::createUuid().toString()); } break; case KMessageBox::No: overwriteDataIndexCourse = testCourse; break; default: return true; } } } if (Resource* dataIndexResource = storeResource(&course, overwriteDataIndexCourse)) { m_editorWidget->clearUndoStackForResource(dataIndexResource); selectDataResource(dataIndexResource); } return true; } bool ResourceEditor::importKeyboardLayout(const QString& path) { ResourceDataAccess resourceDataAccess; KeyboardLayout keyboardLayout; if (!resourceDataAccess.loadKeyboardLayout(path, &keyboardLayout)) return false; DataIndexKeyboardLayout* overwriteDataIndexKeyboardLayout(0); for (int i = 0; i < m_dataIndex->keyboardLayoutCount(); i++) { DataIndexKeyboardLayout* const testKeyboardLayout = m_dataIndex->keyboardLayout(i); if (testKeyboardLayout->source() == DataIndex::BuiltInResource && testKeyboardLayout->id() == keyboardLayout.id()) { switch (KMessageBox::questionYesNo(this, i18n("The selected keyboard layout is already present as a built-in keyboard layout."), QString(), KGuiItem(i18n("Import as new keyboard layout"), QStringLiteral("dialog-ok")), KStandardGuiItem::cancel() )) { case KMessageBox::Yes: keyboardLayout.setId(QUuid::createUuid().toString()); break; default: return true; } } if (testKeyboardLayout->source() == DataIndex::UserResource && testKeyboardLayout->id() == keyboardLayout.id()) { switch (KMessageBox::questionYesNoCancel(this, i18n("The selected keyboard layout is already present as a user keyboard layout."), QString(), KGuiItem(i18n("Import as new keyboard layout"), QStringLiteral("dialog-ok")), KStandardGuiItem::overwrite(), KStandardGuiItem::cancel() )) { case KMessageBox::Yes: keyboardLayout.setId(QUuid::createUuid().toString()); break; case KMessageBox::No: overwriteDataIndexKeyboardLayout = testKeyboardLayout; break; default: return true; } } } if (Resource* dataIndexResource = storeResource(&keyboardLayout, overwriteDataIndexKeyboardLayout)) { m_editorWidget->clearUndoStackForResource(dataIndexResource); selectDataResource(dataIndexResource); } return true; } diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 0df2076..abdcd87 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1,77 +1,78 @@ /* * Copyright 2012 Sebastian Gottfried * * 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) any later version. * * 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 . */ #include "mainwindow.h" #include #include #include #include #include #include #include #include "application.h" #include "ktouchcontext.h" MainWindow::MainWindow(QWidget* parent): KMainWindow(parent), m_view(new QQuickView()), m_context(new KTouchContext(this, m_view, this)) { init(); } MainWindow::~MainWindow() { } void MainWindow::init() { QWidget* viewWidget = QWidget::createWindowContainer(m_view, this); - viewWidget->setMinimumSize(1000, 700); + const int unit = fontMetrics().height(); + viewWidget->setMinimumSize(56 * unit, 39 * unit); viewWidget->setFocusPolicy(Qt::StrongFocus); setCentralWidget(viewWidget); Application::setupDeclarativeBindings(m_view->engine()); m_view->connect(m_view, &QQuickView::statusChanged, this, &MainWindow::onViewStatusChanged); m_view->rootContext()->setContextProperty(QStringLiteral("ktouch"), m_context); m_view->setResizeMode(QQuickView::SizeRootObjectToView); m_view->setSource(QUrl(QStringLiteral("qrc:/ktouch/qml/main.qml"))); } void MainWindow::onViewStatusChanged(QQuickView::Status status) { if (status == QQuickView::Error) { QStringList errorMessages; foreach (auto error, m_view->errors()) { errorMessages.append(error.toString()); } QMessageBox qmlErrorMsgBox; qmlErrorMsgBox.setText(i18n("%1 has encountered a runtime error and has to be closed.", KAboutData::applicationData().displayName())); qmlErrorMsgBox.setDetailedText(errorMessages.join(QLatin1Char('\n'))); qmlErrorMsgBox.setStandardButtons(QMessageBox::Close); qmlErrorMsgBox.setIcon(QMessageBox::Critical); qmlErrorMsgBox.exec(); exit(1); } } diff --git a/src/qml/common/Balloon.qml b/src/qml/common/Balloon.qml index bf91734..41166fa 100644 --- a/src/qml/common/Balloon.qml +++ b/src/qml/common/Balloon.qml @@ -1,166 +1,166 @@ /* * Copyright 2012 Marco Martin * Copyright 2015 Sebastian Gottfried * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2, or * (at your option) any later version. * * 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 Library General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import QtQuick 2.9 import QtGraphicalEffects 1.0 Loader { id: root property Item visualParent property string status: 'closed' default property Item data active: status != 'closed' function open() { root.status = 'loading' } function close() { root.status = 'closing' } sourceComponent: Component { MouseArea { id: dismissArea anchors.fill: parent opacity: root.active && (root.status == 'open' || root.status =='opening')? 1 : 0 layer.enabled: true layer.effect: DropShadow { anchors.fill: parent - radius: 5 - samples: 11 + radius: Units.smallSpacing + samples: 2 * radius + 1 } Behavior on opacity { SequentialAnimation { NumberAnimation { - duration: 250 + duration: Units.shortDuration easing.type: Easing.InOutQuad properties: "opacity" } ScriptAction { script: { root.status = root.status == 'opening' ? 'open' : 'closed' } } } } SystemPalette { id: palette colorGroup: SystemPalette.Active } Rectangle { id: internal color: palette.alternateBase - radius: 5 + radius: Units.smallSpacing property variant parentPos: root.visualParent? root.visualParent.mapToItem(null, 0, 0): Qt.point(0, 0) property bool under: root.visualParent ? internal.parentPos.y + root.visualParent.height + height < dismissArea.height : true // bindings don't work for anchor definition onUnderChanged: { if (under) { balloonTip.anchors.top = undefined balloonTip.anchors.bottom = balloonTip.parent.top } else { balloonTip.anchors.bottom = undefined balloonTip.anchors.top = balloonTip.parent.bottom } } property int preferedX: internal.parentPos.x - internal.width/2 + root.visualParent.width/2 x: Math.round(Math.max(radius, Math.min(dismissArea.width - internal.width - radius, preferedX))) y: { if (root.visualParent) { if (under) { Math.round(internal.parentPos.y + root.visualParent.height + balloonTip.height + radius) } else { Math.round(internal.parentPos.y - internal.height - balloonTip.height - radius) } } else { Math.round(dismissArea.height/2 - internal.height/2) } } width: contentItem.width + 2 * internal.radius height: contentItem.height + 2 * internal.radius Rectangle { id: balloonTip color: internal.color anchors { horizontalCenter: parent.horizontalCenter horizontalCenterOffset: internal.preferedX - internal.x top: parent.bottom } - width: 10 - height: 10 + width: Math.floor(Units.gridUnit / 2) + height: width visible: false } Image { id: balloonTipMask anchors.fill: balloonTip visible: false source: "qrc:///ktouch/images/balloontip.svgz" sourceSize: Qt.size(width, height) } OpacityMask { anchors.fill: balloonTip visible: root.visualParent != null source: balloonTip maskSource: balloonTipMask rotation: internal.under? 0: 180 } Item { id: contentItem x: internal.radius y: internal.radius width: childrenRect.width height: childrenRect.height + 2 data: root.data } } onClicked: { root.close() } Component.onCompleted: { var candidate = root while (candidate.parent.parent) { candidate = candidate.parent } if (candidate) { dismissArea.parent = candidate } root.status = 'opening' } } } } diff --git a/src/qml/common/Collapsable.qml b/src/qml/common/Collapsable.qml index 7434c84..59e045f 100644 --- a/src/qml/common/Collapsable.qml +++ b/src/qml/common/Collapsable.qml @@ -1,72 +1,74 @@ /* * Copyright 2017 Sebastian Gottfried * * 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) any later version. * * 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.9 import QtQuick.Controls 2.2 as Controls import ktouch 1.0 Controls.Control { id: root property bool collapsed: false default property Item data signal contentReadyForSwap function swapContent() { contentItem.isSwapping = true } property KColorScheme colorScheme: KColorScheme { colorGroup: KColorScheme.Active colorSet: KColorScheme.Window } height: collapsed && contentItem.opacity == 0? 0: contentItem.implicitHeight visible: height > 0 Behavior on height { NumberAnimation { - duration: 150 + duration: Units.shortDuration easing.type: Easing.InOutQuad } } background: Rectangle { color: colorScheme.normalBackground } contentItem: Item { property bool isSwapping: false opacity: !root.collapsed && !isSwapping && root.height === implicitHeight? 1: 0 data: root.data - implicitHeight: children.length > 0? children[0].implicitHeight: 0 + implicitHeight: children.length > 0? + children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.topMargin: + 0 Behavior on opacity { NumberAnimation { - duration: 150 + duration: Units.shortDuration } } onOpacityChanged: { if (isSwapping && opacity === 0) { contentReadyForSwap() isSwapping = false; } } } } diff --git a/src/qml/common/ComboBox.qml b/src/qml/common/ComboBox.qml index 76aad88..f01f2a2 100644 --- a/src/qml/common/ComboBox.qml +++ b/src/qml/common/ComboBox.qml @@ -1,131 +1,132 @@ /* * Copyright 2017 Sebastian Gottfried * * 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) any later version. * * 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.9 import QtQuick.Controls 2.2 import QtGraphicalEffects 1.0 import ktouch 1.0 ComboBox { id: control property string iconRole: "icon" property KColorScheme colorScheme: KColorScheme { colorGroup: control.enabled? KColorScheme.Active: KColorScheme.Inactive colorSet: KColorScheme.Button } property alias popupListView: list KColorScheme { id: listColorScheme colorGroup: KColorScheme.Active colorSet: KColorScheme.View } hoverEnabled: true - spacing: 0.7 * font.pixelSize + spacing: Units.largeSpacing background: Item { Rectangle { anchors.fill: parent opacity: (popup.visible? 0.6: 0.0) + (control.hovered || control.visualFocus? 0.3: 0.0) color: colorScheme.normalBackground Behavior on opacity { NumberAnimation { - duration: 150 + duration: Units.shortDuration } } } FocusBar { anchors { left: parent.left right: parent.right bottom: parent.bottom } height: 3 control: control } } contentItem: IconLabel { text: control.displayText iconName: currentIndex != -1 && currentIndex < model.count ? control.model.get(currentIndex)[control.iconRole]: "" color: colorScheme.normalText } indicator: MonochromeIcon { color: colorScheme.normalText icon: "arrow-down-double" x: control.width - width - control.contentItem.padding y: control.topPadding + (control.availableHeight - height) / 2 rotation: control.popup.opacity * -180 } delegate: ListItem { width: control.width text: control.textRole ? (Array.isArray(control.model) ? modelData[control.textRole] : model[control.textRole]) : modelData iconName: control.iconRole ? (Array.isArray(control.model) ? modelData[control.iconRole] : model[control.iconRole]) : "" highlighted: control.currentIndex === index } popup: Popup { y: control.height width: control.width implicitHeight: contentItem.implicitHeight padding: 0 opacity: 0 enter: Transition { NumberAnimation { property: "opacity" to: 1.0 - duration: 150 + duration: Units.shortDuration } } exit: Transition { NumberAnimation { property: "opacity" to: 0.0 - duration: 150 + duration: Units.shortDuration } } contentItem: ListView { id: list clip: true implicitHeight: contentHeight width: parent.width model: control.popup.visible ? control.delegateModel : null currentIndex: control.highlightedIndex } background: Rectangle { color: listColorScheme.normalBackground layer.enabled: true layer.effect: DropShadow { - samples: 24 + radius: Units.smallSpacing + samples: 2 * radius + 1 horizontalOffset: 0 verticalOffset: 0 } } } } diff --git a/src/qml/common/FocusBar.qml b/src/qml/common/FocusBar.qml index 2a65a44..bb62eed 100644 --- a/src/qml/common/FocusBar.qml +++ b/src/qml/common/FocusBar.qml @@ -1,34 +1,34 @@ /* * Copyright 2017 Sebastian Gottfried * * 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) any later version. * * 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.9 import QtQuick.Controls 2.2 as Controls import ktouch 1.0 Rectangle { property Item control - height: 2 + height: Math.max(Math.floor(Units.gridUnit / 6), 1) color: control.colorScheme.focusDecoration opacity: control.activeFocus? 1: 0 Behavior on opacity { NumberAnimation { duration: 150 } } } diff --git a/src/qml/common/GridView.qml b/src/qml/common/GridView.qml index ac6f228..38065e8 100644 --- a/src/qml/common/GridView.qml +++ b/src/qml/common/GridView.qml @@ -1,62 +1,60 @@ /* * Copyright 2017 Sebastian Gottfried * * 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) any later version. * * 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.9 import QtQuick.Controls 2.2 as Controls import ktouch 1.0 GridView { id: control property alias colorScheme: colorScheme property alias background: backgroundItem KColorScheme { id: colorScheme colorGroup: control.enabled? KColorScheme.Active: KColorScheme.Disabled colorSet: KColorScheme.View } Rectangle { id: backgroundItem anchors.fill: parent color: colorScheme.normalBackground z: -1 } FocusBar { anchors { top: parent.top left: parent.left right: parent.right } - height: 3 control: control } FocusBar { anchors { left: parent.left right: parent.right bottom: parent.bottom } - height: 3 control: control } Controls.ScrollBar.vertical: ScrollBar { } } diff --git a/src/qml/common/Icon.qml b/src/qml/common/Icon.qml index bfdd73e..97e63ad 100644 --- a/src/qml/common/Icon.qml +++ b/src/qml/common/Icon.qml @@ -1,22 +1,23 @@ /* * Copyright 2017 Sebastian Gottfried * * 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) any later version. * * 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 org.kde.kquickcontrolsaddons 2.0 as Addons Addons.QIconItem { - + height: Units.fontMetrics.roundedIconSize(Units.gridUnit) + width: height } diff --git a/src/qml/common/IconButton.qml b/src/qml/common/IconButton.qml index 563a92e..74280e6 100644 --- a/src/qml/common/IconButton.qml +++ b/src/qml/common/IconButton.qml @@ -1,88 +1,87 @@ /* * Copyright 2017 Sebastian Gottfried * * 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) any later version. * * 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.9 import QtQuick.Controls 2.2 import ktouch 1.0 import QtGraphicalEffects 1.0 Button { id: button property alias color: content.color property alias bgColor: bg.color property alias iconName: content.iconName property alias colorScheme: buttonColorScheme padding: 0 hoverEnabled: true KColorScheme { id: buttonColorScheme colorGroup: button.enabled? KColorScheme.Active: KColorScheme.Disabled colorSet: KColorScheme.Button } contentItem: IconLabel { color: buttonColorScheme.normalText id: content text: button.text elide: "ElideNone" Behavior on color { - ColorAnimation { duration: 150 } + ColorAnimation { duration: Units.shortDuration } } } background: Item { Rectangle { anchors.fill: parent; id: bg color: buttonColorScheme.alternateBackground HueSaturation { anchors.fill: bg source: bg saturation: hovered? 0.3: 0 lightness: hovered? -0.04: 0 Behavior on saturation { NumberAnimation { - duration: 150 + duration: Units.shortDuration } } Behavior on lightness { NumberAnimation { - duration: 150 + duration: Units.shortDuration } } } } FocusBar { anchors { left: parent.left right: parent.right bottom: parent.bottom } - height: 3 control: button } } } diff --git a/src/qml/common/IconLabel.qml b/src/qml/common/IconLabel.qml index 4c9ad69..b3dc09d 100644 --- a/src/qml/common/IconLabel.qml +++ b/src/qml/common/IconLabel.qml @@ -1,44 +1,42 @@ /* * Copyright 2017 Sebastian Gottfried * * 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) any later version. * * 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.9 import QtQuick.Controls 2.2 Label { property string iconName: "" property bool reserveSpaceForIcon: false id: label elide: Text.ElideRight - padding: Math.ceil(0.7 * font.pixelSize) + padding: Units.largeSpacing leftPadding: (iconItem.visible || reserveSpaceForIcon? padding + iconItem.width: 0) + (label.text !== ""? padding: 0) verticalAlignment: Text.AlignVCenter MonochromeIcon { id: iconItem visible: label.iconName != "" color: label.color anchors { left: parent.left leftMargin: label.text === ""? (label.width - width) / 2: label.padding verticalCenter: parent.verticalCenter } icon: label.iconName - width: 22 - height: 22 } } diff --git a/src/qml/common/IconToolButton.qml b/src/qml/common/IconToolButton.qml index 211581c..4f0dd96 100644 --- a/src/qml/common/IconToolButton.qml +++ b/src/qml/common/IconToolButton.qml @@ -1,68 +1,67 @@ /* * Copyright 2017 Sebastian Gottfried * * 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) any later version. * * 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.9 import QtQuick.Controls 2.2 import ktouch 1.0 ToolButton { id: button property alias color: content.color property alias iconName: content.iconName property color backgroundColor: button.colorScheme.normalBackground property KColorScheme colorScheme: KColorScheme { id: buttonColorScheme colorGroup: button.enabled? KColorScheme.Active: KColorScheme.Disabled colorSet: KColorScheme.Button } padding: 0 hoverEnabled: true contentItem: IconLabel { id: content text: button.text color: button.colorScheme.normalText elide: "ElideNone" } background: Item { Rectangle { anchors.fill: parent opacity: (button.checked? 0.6: 0) + (button.activeFocus || button.hovered? 0.3: 0) color: button.backgroundColor Behavior on opacity { NumberAnimation { - duration: 150 + duration: Units.shortDuration } } } FocusBar { anchors { left: parent.left right: parent.right bottom: parent.bottom } - height: 3 control: button } } } diff --git a/src/qml/common/InformationTable.qml b/src/qml/common/InformationTable.qml index c4faa4f..db767e7 100644 --- a/src/qml/common/InformationTable.qml +++ b/src/qml/common/InformationTable.qml @@ -1,73 +1,74 @@ /* * Copyright 2012 Sebastian Gottfried * Copyright 2015 Sebastian Gottfried * * 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) any later version. * * 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.9 import QtQuick.Controls 2.2 import QtQuick.Layouts 1.3 import ktouch 1.0 Item { property alias model: repeator.model id: root height: childrenRect.height KColorScheme { id: colorScheme colorGroup: KColorScheme.Active colorSet: KColorScheme.View } Column { id: column - spacing: 3 + spacing: Units.largeSpacing width: parent.width height: childrenRect.height Repeater { id: repeator Row { - spacing: 5 + spacing: Units.smallSpacing height: Math.max(titleLabel.height, valueLabel.height) width: root.width Label { id: titleLabel width: Math.round((parent.width - parent.spacing) / 2) horizontalAlignment: Text.AlignRight verticalAlignment: Text.AlignTop - color: "#888" + color: colorScheme.normalText + opacity: 0.7 text: model.modelData.title wrapMode: Text.Wrap height: Math.max(paintedHeight, valueLabel.paintedHeight) } Label { id: valueLabel width: parent.width - titleLabel.width - parent.spacing horizontalAlignment: Text.AlignLeft verticalAlignment: Text.AlignBottom text: model.modelData.text wrapMode: Text.Wrap height: Math.max(paintedHeight, titleLabel.paintedHeight) color: colorScheme.normalText } } } } } diff --git a/src/qml/common/InlineToolbar.qml b/src/qml/common/InlineToolbar.qml index 8a5b8d9..46cbb45 100644 --- a/src/qml/common/InlineToolbar.qml +++ b/src/qml/common/InlineToolbar.qml @@ -1,55 +1,55 @@ /* * Copyright 2013 Sebastian Gottfried * Copyright 2015 Sebastian Gottfried * * 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) any later version. * * 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.9 Item { id: root property alias content: contentArea.data property alias color: background.color - width: contentArea.width + 6 - height: contentArea.height + 6 + width: contentArea.width + Units.largeSpacing + height: contentArea.height + Units.largeSpacing Rectangle { anchors.fill: parent id: background color: activePalette.mid opacity: 0.7 - radius: 3 + radius: Units.smallSpacing SystemPalette { id: activePalette colorGroup: SystemPalette.Active } } Behavior on opacity { - NumberAnimation { duration: 250 } + NumberAnimation { duration: Units.shortDuration } } Row { id: contentArea anchors.centerIn: parent height: childrenRect.height - spacing: 3 + spacing: Units.smallSpacing } } diff --git a/src/qml/common/LearningProgressChart.qml b/src/qml/common/LearningProgressChart.qml index 2df455b..75864ac 100644 --- a/src/qml/common/LearningProgressChart.qml +++ b/src/qml/common/LearningProgressChart.qml @@ -1,115 +1,115 @@ /* * Copyright 2012 Sebastian Gottfried * * 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) any later version. * * 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.9 import QtQuick.Layouts 1.3 import org.kde.charts 0.1 as Charts import ktouch 1.0 Charts.LineChart { id: root property Charts.Dimension accuracy: accuracyDimension property Charts.Dimension charactersPerMinute: charactersPerMinuteDimension - pitch: 60 + pitch: 3 * Units.gridUnit function minAccuracy(accuracy) { var canditades = [0.9, 0.8, 0.5] for (var i = 0; i < canditades.length; i++) { if (canditades[i] < accuracy) { return (canditades[i]) } } return 0; } dimensions: [ Charts.Dimension { id: accuracyDimension dataColumn: 5 color: "#ffb12d" minimumValue: root.minAccuracy(model.minAccuracy) maximumValue: 1.0 label: i18n("Accuracy") unit: "%" unitFactor: 100 }, Charts.Dimension { id: charactersPerMinuteDimension dataColumn: 6 color: "#38aef4" maximumValue: Math.max(Math.ceil(model.maxCharactersTypedPerMinute / 120) * 120, 120) label: i18n("Characters per Minute") } ] onElemEntered: { learningProgressPointTooltip.visualParent = elem learningProgressPointTooltip.row = row learningProgressPointTooltip.open() } onElemExited: { learningProgressPointTooltip.close() } Balloon { id: learningProgressPointTooltip property int row: -1 function findLessonTitle(id) { var course = model.courseFilter if (course) { for (var i = 0; i < course.lessonCount; i++) { if (course.lesson(i).id === id) { return course.lesson(i).title } } } return i18n("Unknown") } InformationTable { property list infoModel: [ InfoItem { title: i18nc("Statistics on lesson:", "Lesson:") text: learningProgressPointTooltip.row !== -1? learningProgressPointTooltip.findLessonTitle(learningProgressModel.lessonId(learningProgressPointTooltip.row)): "" }, InfoItem { title: i18n("Training on:") text: learningProgressPointTooltip.row !== -1? learningProgressModel.date(learningProgressPointTooltip.row).toLocaleString(): "" }, InfoItem { title: i18n("Accuracy:") text: learningProgressPointTooltip.row !== -1? strFormatter.formatAccuracy(learningProgressModel.accuracy(learningProgressPointTooltip.row)): "" }, InfoItem { title: i18n("Characters per Minute:") text: learningProgressPointTooltip.row !== -1? learningProgressModel.charactersPerMinute(learningProgressPointTooltip.row): "" } ] - width: 450 + width: 25 * Units.gridUnit model: infoModel } } } diff --git a/src/qml/common/ListItem.qml b/src/qml/common/ListItem.qml index ab9a2c4..4d869d2 100644 --- a/src/qml/common/ListItem.qml +++ b/src/qml/common/ListItem.qml @@ -1,63 +1,63 @@ /* * Copyright 2012 Sebastian Gottfried * Copyright 2015 Sebastian Gottfried * * 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) any later version. * * 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.9 import QtQuick.Controls 2.2 import ktouch 1.0 ItemDelegate { id: root property string iconName property alias label: label property alias bg: bg property alias reserveSpaceForIcon: label.reserveSpaceForIcon hoverEnabled: true padding: 0 KColorScheme { id: listItemColorSchemeNormal colorGroup: KColorScheme.Active colorSet: KColorScheme.View } KColorScheme { id: listItemColorSchemeHighlighted colorGroup: KColorScheme.Active colorSet: KColorScheme.Selection } background: Rectangle { id: bg anchors.fill: parent color: listItemColorSchemeHighlighted.normalBackground opacity: root.highlighted ? 1 : (root.hovered? 0.3: 0) Behavior on opacity { NumberAnimation { - duration: 150 + duration: Units.shortDuration } } } contentItem: IconLabel { id: label text: root.text iconName: root.iconName color: root.highlighted? listItemColorSchemeHighlighted.activeText: listItemColorSchemeNormal.normalText; } } diff --git a/src/qml/common/ListView.qml b/src/qml/common/ListView.qml index 4c7013b..c125746 100644 --- a/src/qml/common/ListView.qml +++ b/src/qml/common/ListView.qml @@ -1,64 +1,62 @@ /* * Copyright 2017 Sebastian Gottfried * * 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) any later version. * * 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.9 import QtQuick.Controls 2.2 as Controls import ktouch 1.0 ListView { id: control property alias colorScheme: colorScheme activeFocusOnTab: true KColorScheme { id: colorScheme colorGroup: control.enabled? KColorScheme.Active: KColorScheme.Disabled colorSet: KColorScheme.View } Rectangle { anchors.fill: parent color: colorScheme.normalBackground z: -1 } FocusBar { anchors { top: parent.top left: parent.left right: parent.right } - height: 3 control: control z: -1 } FocusBar { anchors { left: parent.left right: parent.right bottom: parent.bottom } - height: 3 control: control z: -1 } Controls.ScrollBar.vertical: ScrollBar { } } diff --git a/src/qml/common/MessageBox.qml b/src/qml/common/MessageBox.qml index 9ed45ca..9d7b4f9 100644 --- a/src/qml/common/MessageBox.qml +++ b/src/qml/common/MessageBox.qml @@ -1,111 +1,111 @@ /* * Copyright 2018 Sebastian Gottfried * * 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) any later version. * * 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.9 import "../common" Rectangle { id: item anchors.verticalCenter: parent.verticalCenter - radius: 3 + radius: Units.smallSpacing color: "#eee4be" - height: label.height + 6 - width: label.width + 6 + height: label.height + width: label.width smooth: true function showMessage(msg, iconSource) { item.state = "hidden"; label.text = msg label.iconName = iconSource || "" item.state = "normal" } function clearMessage() { item.state = "cleared" } function clearMessageImmediately() { item.state = "hidden" } border { width: 1 color: "#decd87" } transform: Scale { id: itemScaler origin.x: 0 origin.y: 0 } state: "hidden" IconLabel { anchors.centerIn: parent + padding: Units.smallSpacing id: label - padding: 2 } states: [ State { name: "normal" PropertyChanges { target: itemScaler xScale: 1 } }, State { name: "hidden" PropertyChanges { target: itemScaler xScale: 0 } }, State { name: "cleared" PropertyChanges { target: itemScaler xScale: 0 } } ] transitions: [ Transition { from: "*" to: "normal" NumberAnimation { target: itemScaler property: "xScale" duration: 500 easing.type: Easing.InOutQuad } }, Transition { from: "normal" to: "cleared" NumberAnimation { target: itemScaler property: "xScale" duration: 500 easing.type: Easing.InOutQuad } } ] } diff --git a/src/qml/common/MonochromeIcon.qml b/src/qml/common/MonochromeIcon.qml index c100bfd..2b7b530 100644 --- a/src/qml/common/MonochromeIcon.qml +++ b/src/qml/common/MonochromeIcon.qml @@ -1,30 +1,28 @@ /* * Copyright 2017 Sebastian Gottfried * * 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) any later version. * * 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 QtGraphicalEffects 1.0 Icon { property color color: "#000000" id: icon - width: 22 - height: 22 ColorOverlay { anchors.fill: parent source: icon color: parent.color } } diff --git a/src/qml/common/PopupDialog.qml b/src/qml/common/PopupDialog.qml index 6868cd0..564f739 100644 --- a/src/qml/common/PopupDialog.qml +++ b/src/qml/common/PopupDialog.qml @@ -1,144 +1,144 @@ /* * Copyright 2017 Sebastian Gottfried * * 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) any later version. * * 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.9 import QtQuick.Controls 2.2 import QtQuick.Layouts 1.3 import ktouch 1.0 import QtGraphicalEffects 1.0 Dialog { id: root dim: true opacity: 0 scale: 0.9 leftMargin: Math.floor((parent.width - width) / 2) topMargin: Math.floor((parent.height - height) / 2) Component.onCompleted: { var candidate = root while (candidate.parent) { candidate = candidate.parent } if (candidate) { root.parent = candidate } } onOpened: { contentItem.forceActiveFocus() } Item { id: dimOverlay parent: root.parent width: root.parent.width height: root.parent.height visible: root.visible && root.dim opacity: 0 ShaderEffectSource { id: effectSource sourceItem: root.parent.appContent anchors.fill: parent hideSource: false } HueSaturation { id: desaturatedBackground source: effectSource anchors.fill: parent lightness: -0.3 saturation: -0.5 visible: false } FastBlur { anchors.fill: parent source: desaturatedBackground radius: 50 } Rectangle { anchors.fill: parent color: "#55000000" } } enter: Transition { // grow_fade_in - NumberAnimation { property: "scale"; to: 1.0; easing.type: Easing.OutQuint; duration: 220 } - NumberAnimation { property: "opacity"; to: 1.0; easing.type: Easing.OutCubic; duration: 150 } - NumberAnimation { target: dimOverlay; property: "opacity"; to: 1.0; easing.type: Easing.OutCubic; duration: 220 } + NumberAnimation { property: "scale"; to: 1.0; easing.type: Easing.OutQuint; duration: Units.longDuration } + NumberAnimation { property: "opacity"; to: 1.0; easing.type: Easing.OutCubic; duration: Units.shortDuration } + NumberAnimation { target: dimOverlay; property: "opacity"; to: 1.0; easing.type: Easing.OutCubic; duration: Units.longDuration } } exit: Transition { // shrink_fade_out - NumberAnimation { property: "scale"; to: 0.9; easing.type: Easing.OutQuint; duration: 220 } - NumberAnimation { property: "opacity"; to: 0.0; easing.type: Easing.OutCubic; duration: 150 } - NumberAnimation { target: dimOverlay; property: "opacity"; to: 0.0; easing.type: Easing.OutCubic; duration: 220 } + NumberAnimation { property: "scale"; to: 0.9; easing.type: Easing.OutQuint; duration: Units.longDuration } + NumberAnimation { property: "opacity"; to: 0.0; easing.type: Easing.OutCubic; duration: Units.shortDuration } + NumberAnimation { target: dimOverlay; property: "opacity"; to: 0.0; easing.type: Easing.OutCubic; duration: Units.longDuration } } background: Rectangle { color: dialogColorScheme.normalBackground KColorScheme { id: dialogColorScheme colorGroup: KColorScheme.Active colorSet: KColorScheme.Window } } header: Rectangle { implicitHeight: Math.max(titleLabel.implicitHeight, closeButton.implicitHeight) color: toolbarColorScheme.toolbarBackground KColorScheme { id: toolbarColorScheme colorGroup: KColorScheme.Active colorSet: KColorScheme.Complementary property color toolbarBackground: Qt.darker(toolbarColorScheme.shade(toolbarColorScheme.hoverDecoration, KColorScheme.MidShade, toolbarColorScheme.contrast, -0.2), 1.3) } RowLayout { anchors.fill: parent Label { id: titleLabel text: root.title width: parent.width color: toolbarColorScheme.normalText font.bold: true padding: font.pixelSize Layout.fillWidth: true } IconToolButton { id: closeButton iconName: "window-close-symbolic" color: toolbarColorScheme.normalText visible: root.closePolicy & Popup.CloseOnEscape backgroundColor: toolbarColorScheme.normalBackground activeFocusOnTab: false Layout.preferredWidth: titleLabel.implicitHeight Layout.preferredHeight: titleLabel.implicitHeight onClicked: root.close() } } } } diff --git a/src/qml/common/RadioButton.qml b/src/qml/common/RadioButton.qml index f0441a3..e53106f 100644 --- a/src/qml/common/RadioButton.qml +++ b/src/qml/common/RadioButton.qml @@ -1,93 +1,93 @@ /* * Copyright 2017 Sebastian Gottfried * * 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) any later version. * * 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.9 import QtQuick.Controls 2.2 import ktouch 1.0 RadioButton { id: control hoverEnabled: true - spacing: label.font.pixelSize + spacing: Units.largeSpacing padding: 0 property alias colorScheme: colorScheme property alias selectionColorScheme: selectionColorScheme property alias label: label KColorScheme { id: colorScheme colorGroup: control.enabled? KColorScheme.Active: KColorScheme.Disabled colorSet: KColorScheme.Button } KColorScheme { id: selectionColorScheme colorGroup: control.enabled? KColorScheme.Active: KColorScheme.Disabled colorSet: KColorScheme.Selection } indicator: Rectangle { implicitWidth: control.font.pixelSize implicitHeight: control.font.pixelSize x: control.leftPadding y: parent.height / 2 - height / 2 radius: height / 2 border.color: control.checked? Qt.lighter(colorScheme.focusDecoration, control.hovered? 1.3: 1.0): utils.alpha(colorScheme.normalText, control.hovered? 0.9: 0.5) color: "#00000000" Behavior on border.color { ColorAnimation { - duration: 150 + duration: Units.shortDuration } } Rectangle { anchors.fill: parent anchors.margins: 3 radius: height / 2 color: Qt.lighter(colorScheme.focusDecoration, control.hovered? 1.3: 1.0) visible: control.checked } } contentItem: Label { id: label text: control.text color: control.colorScheme.normalText horizontalAlignment: Text.AlignLeft verticalAlignment: Text.AlignVCenter - topPadding: 3 + topPadding: Units.smallSpacing leftPadding: control.indicator.width + control.spacing - bottomPadding: 3 + bottomPadding: Units.smallSpacing FocusBar { anchors { left: parent.left right: parent.right bottom: parent.bottom leftMargin: parent.leftPadding } control: control } } } diff --git a/src/qml/common/ScrollBar.qml b/src/qml/common/ScrollBar.qml index 7ca6cdc..11a8089 100644 --- a/src/qml/common/ScrollBar.qml +++ b/src/qml/common/ScrollBar.qml @@ -1,50 +1,51 @@ /* * Copyright 2017 Sebastian Gottfried * * 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) any later version. * * 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.9 import QtQuick.Controls 2.2 as Controls import ktouch 1.0 Controls.ScrollBar { id: control property alias colorScheme: colorScheme KColorScheme { id: colorScheme colorGroup: control.enabled? KColorScheme.Active: KColorScheme.Disabled colorSet: KColorScheme.Window } contentItem: Rectangle { - implicitWidth: control.orientation == Qt.Horizontal? 100: 6 - implicitHeight: control.orientation == Qt.Horizontal? 6: 100 - radius: 3 + property int size: Math.floor(Units.gridUnit / 3) + implicitWidth: control.orientation == Qt.Horizontal? 100: size + implicitHeight: control.orientation == Qt.Horizontal? size: 100 + radius: size / 2 color: control.pressed ? colorScheme.focusDecoration: colorScheme.alternateBackground opacity: control.active? 1: 0.3 Behavior on opacity { NumberAnimation { - duration: 150 + duration: Units.shortDuration } } Behavior on color { ColorAnimation { - duration: 150 + duration: Units.shortDuration } } } } diff --git a/src/qml/common/SelectionGrip.qml b/src/qml/common/SelectionGrip.qml index 8e260f1..67fd5a4 100644 --- a/src/qml/common/SelectionGrip.qml +++ b/src/qml/common/SelectionGrip.qml @@ -1,119 +1,119 @@ /* * Copyright 2012 Sebastian Gottfried * * 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) any later version. * * 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.9 import ktouch 1.0 Item { id: root property KeyboardLayout keyboardLayout property variant target: null property variant targetItem: null property string horizontalPosition property string verticalPosition property bool interactive property bool xLocked: false property bool yLocked: false - property int minSize: 20 + property int minSize: Units.gridUnit x: targetItem? targetItem.x + (horizontalPosition == "left"? 0: targetItem.width - 1): 0 y: targetItem? targetItem.y + (verticalPosition == "top"? 0: targetItem.height - 1): 0 onXChanged: { if (!xLocked && mouseArea.drag.active) { xLocked = true var effX = Math.round(x / scaleFactor) if (horizontalPosition == "left") { keyboardLayoutEditor.setKeyGeometry(targetItem.keyIndex, effX, target.top, target.width + target.left - effX, target.height) } else { keyboardLayoutEditor.setKeyGeometry(targetItem.keyIndex, target.left, target.top, effX - target.left, target.height) } xLocked = false } } onYChanged: { if (!yLocked && mouseArea.drag.active) { yLocked = true var effY = Math.round(y / scaleFactor) if (verticalPosition == "top") { keyboardLayoutEditor.setKeyGeometry(targetItem.keyIndex, target.left, effY, target.width, target.height + target.top - effY) } else { keyboardLayoutEditor.setKeyGeometry(targetItem.keyIndex, target.left, target.top, target.width, effY - target.top) } yLocked = false } } width: 1 height: 1 SystemPalette { id: palette colorGroup: SystemPalette.Active } Rectangle { anchors { centerIn: parent - verticalCenterOffset: verticalPosition == "top"? -10: 10 - horizontalCenterOffset: horizontalPosition == "left"? -10: 10 + verticalCenterOffset: verticalPosition == "top"? -Units.largeSpacing: Units.largeSpacing + horizontalCenterOffset: horizontalPosition == "left"? -Units.largeSpacing: Units.largeSpacing } - width: 15 - height: 15 + width: Math.floor(Units.gridUnit / 2) + height: Math.floor(Units.gridUnit / 2) color: palette.highlight visible: interactive MouseArea { id: mouseArea anchors.fill: parent cursorShape: ((horizontalPosition == "left") != (verticalPosition == "top"))? Qt.SizeBDiagCursor: Qt.SizeFDiagCursor onClicked: { mouse.accepted = true } drag { target: root axis: Drag.XandYAxis minimumX: !targetItem? 0: horizontalPosition == "left"? 0: targetItem.x + scaleFactor * minSize maximumX: !targetItem? 0: horizontalPosition == "left"? targetItem.x + targetItem.width - minSize: keyboardLayout.width * scaleFactor minimumY: !targetItem? 0: verticalPosition == "top"? 0: targetItem.y + scaleFactor * minSize maximumY: !targetItem? 0: verticalPosition == "top"? targetItem.y + targetItem.height - minSize: keyboardLayout.height * scaleFactor onActiveChanged: { targetItem.manipulated = drag.active if (!drag.active) { var left = 10 * Math.round(target.left / 10) var top = 10 * Math.round(target.top / 10) var width = 10 * Math.round(target.width / 10) var height = 10 * Math.round(target.height / 10) keyboardLayoutEditor.setKeyGeometry(targetItem.keyIndex, left, top, width, height) } } } } } } diff --git a/src/qml/common/TextArea.qml b/src/qml/common/TextArea.qml index 3ec5200..107eafd 100644 --- a/src/qml/common/TextArea.qml +++ b/src/qml/common/TextArea.qml @@ -1,57 +1,57 @@ /* * Copyright 2017 Sebastian Gottfried * * 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) any later version. * * 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.9 import QtQuick.Controls 2.2 as Controls import ktouch 1.0 Controls.TextArea { id: control property KColorScheme colorScheme: KColorScheme { colorGroup: control.enabled? KColorScheme.Active: KColorScheme.Disabled colorSet:KColorScheme.View } property KColorScheme selectionColorScheme: KColorScheme { colorGroup: control.enabled? KColorScheme.Active: KColorScheme.Disabled colorSet:KColorScheme.Selection } selectByMouse: true selectByKeyboard: true color: control.colorScheme.normalText selectionColor: selectionColorScheme.normalBackground selectedTextColor: selectionColorScheme.normalText background: Rectangle { color: control.colorScheme.normalBackground anchors.fill: parent border.width: 1 border.color: control.activeFocus? control.colorScheme.focusDecoration: Qt.hsva(control.colorScheme.focusDecoration.hslHue, 0, control.colorScheme.focusDecoration.hslValue, control.enabled? 0.5: 0.2) Behavior on border.color { - ColorAnimation { duration: 150 } + ColorAnimation { duration: Units.shortDuration } } } } diff --git a/src/qml/common/ToolSeparator.qml b/src/qml/common/ToolSeparator.qml index 74ff0b2..cc7a351 100644 --- a/src/qml/common/ToolSeparator.qml +++ b/src/qml/common/ToolSeparator.qml @@ -1,41 +1,39 @@ /* * Copyright 2017 Sebastian Gottfried * * 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) any later version. * * 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.9 import QtQuick.Controls 2.2 as Controls import ktouch 1.0 Controls.ToolSeparator { id: control property alias colorScheme: colorScheme KColorScheme { id: colorScheme colorGroup: KColorScheme.Active colorSet: KColorScheme.Complementary } - padding: vertical? 6: 2 - topPadding: vertical? 2: 6 - bottomPadding: vertical? 2: 6 + padding: Units.smallSpacing contentItem: Rectangle { - implicitWidth: control.vertical? 1: 24 - implicitHeight: control.vertical? 24: 1 + implicitWidth: control.vertical? 1: Units.gridUnit + implicitHeight: control.vertical? Units.gridUnit: 1 color: control.colorScheme.normalText } } diff --git a/src/qml/common/Units.qml b/src/qml/common/Units.qml new file mode 100644 index 0000000..b6f5063 --- /dev/null +++ b/src/qml/common/Units.qml @@ -0,0 +1,137 @@ +/* + * Copyright 2015 Marco Martin + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, or + * (at your option) any later version. + * + * 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 Library General Public License for more details + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import QtQuick 2.4 +import QtQuick.Window 2.2 + +pragma Singleton + +/** + * A set of values to define semantically sizes and durations + * @inherit QtQuick.QtObject + */ +QtObject { + id: units + + /** + * The fundamental unit of space that should be used for sizes, expressed in pixels. + * Given the screen has an accurate DPI settings, it corresponds to a width of + * the capital letter M + */ + property int gridUnit: fontMetrics.height + + /** + * units.iconSizes provides access to platform-dependent icon sizing + * + * The icon sizes provided are normalized for different DPI, so icons + * will scale depending on the DPI. + * + * Icon sizes from KIconLoader, adjusted to devicePixelRatio: + * * small + * * smallMedium + * * medium + * * large + * * huge + * * enormous + * + * Not devicePixelRation-adjusted:: + * * desktop + */ + property QtObject iconSizes: QtObject { + property int small: Math.floor(fontMetrics.roundedIconSize(16 * devicePixelRatio)) + property int smallMedium: Math.floor(fontMetrics.roundedIconSize(22 * devicePixelRatio)) + property int medium: Math.floor(fontMetrics.roundedIconSize(32 * devicePixelRatio)) + property int large: Math.floor(fontMetrics.roundedIconSize(48 * devicePixelRatio)) + property int huge: Math.floor(fontMetrics.roundedIconSize(64 * devicePixelRatio)) + property int enormous: Math.floor(128 * devicePixelRatio) + } + + property int borderWidth: Math.max(1, Math.floor(gridUnit / 18)) + + /** + * units.smallSpacing is the amount of spacing that should be used around smaller UI elements, + * for example as spacing in Columns. Internally, this size depends on the size of + * the default font as rendered on the screen, so it takes user-configured font size and DPI + * into account. + */ + property int smallSpacing: Math.floor(gridUnit/4) + + /** + * units.largeSpacing is the amount of spacing that should be used inside bigger UI elements, + * for example between an icon and the corresponding text. Internally, this size depends on + * the size of the default font as rendered on the screen, so it takes user-configured font + * size and DPI into account. + */ + property int largeSpacing: smallSpacing*2 + + /** + * The ratio between physical and device-independent pixels. This value does not depend on the \ + * size of the configured font. If you want to take font sizes into account when scaling elements, + * use theme.mSize(theme.defaultFont), units.smallSpacing and units.largeSpacing. + * The devicePixelRatio follows the definition of "device independent pixel" by Microsoft. + */ + property real devicePixelRatio: Math.max(1, ((fontMetrics.font.pixelSize*0.75) / fontMetrics.font.pointSize)) + + /** + * units.longDuration should be used for longer, screen-covering animations, for opening and + * closing of dialogs and other "not too small" animations + */ + property int longDuration: 250 + + /** + * units.shortDuration should be used for short animations, such as accentuating a UI event, + * hover events, etc.. + */ + property int shortDuration: 150 + + /** + * time in ms by which the display of tooltips will be delayed. + * + * @sa ToolTip.delay property + */ + property int toolTipDelay: 700 + + /** + * How much the mouse scroll wheel scrolls, expressed in lines of text. + * Note: this is strictly for classical mouse wheels, touchpads 2 figer scrolling won't be affected + */ + readonly property int wheelScrollLines: 3 + + /** + * metrics used by the default font + */ + property variant fontMetrics: TextMetrics { + text: "M" + function roundedIconSize(size) { + if (size < 16) { + return size; + } else if (size < 22) { + return 16; + } else if (size < 32) { + return 22; + } else if (size < 48) { + return 32; + } else if (size < 64) { + return 48; + } else { + return size; + } + } + } +} diff --git a/src/qml/common/qmldir b/src/qml/common/qmldir new file mode 100644 index 0000000..ec09362 --- /dev/null +++ b/src/qml/common/qmldir @@ -0,0 +1 @@ +singleton Units 1.0 Units.qml diff --git a/src/qml/homescreen/CourseDescriptionItem.qml b/src/qml/homescreen/CourseDescriptionItem.qml index d854afd..406839e 100644 --- a/src/qml/homescreen/CourseDescriptionItem.qml +++ b/src/qml/homescreen/CourseDescriptionItem.qml @@ -1,50 +1,55 @@ /* * Copyright 2012 Sebastian Gottfried * Copyright 2015 Sebastian Gottfried * * 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) any later version. * * 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.9 import ktouch 1.0 import "../common" Collapsable { id: root property string description onDescriptionChanged: { if (contentItem.opacity === 0) { descriptionLabel.text = description } else { swapContent() } } onContentReadyForSwap: { descriptionLabel.text = description } Label { id: descriptionLabel - leftPadding: 20 - rightPadding: 20 - topPadding: 10 - bottomPadding: 10 + anchors { + fill: parent + leftMargin: Units.gridUnit + rightMargin: Units.gridUnit + topMargin: Units.largeSpacing + bottomMargin: Units.largeSpacing + } + + wrapMode: Text.Wrap } } diff --git a/src/qml/homescreen/CourseSelector.qml b/src/qml/homescreen/CourseSelector.qml index 5ba48b8..e471ca9 100644 --- a/src/qml/homescreen/CourseSelector.qml +++ b/src/qml/homescreen/CourseSelector.qml @@ -1,196 +1,195 @@ /* * Copyright 2012 Sebastian Gottfried * Copyright 2015 Sebastian Gottfried * * 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) any later version. * * 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.9 import QtQuick.Layouts 1.3 import QtQuick.Controls 2.2 as Controls import ktouch 1.0 import "../common" FocusScope { id: root property Profile profile property string currentKeyboardLayoutName property string selectedKeyboardLayoutName property DataIndexKeyboardLayout selectedKeyboardLayout property DataIndexCourse selectedCourse signal lessonSelected(variant course, variant lesson) signal courseSelectec(Course course) function selectLastUsedCourse() { if (!profile) { return } var courseId = profile.lastUsedCourseId; // fist try to to select the course the user has used last for (var i = 0; i < allCoursesModel.rowCount(); i++) { var dataIndexCourse = allCoursesModel.data(allCoursesModel.index(i, 0), ResourceModel.DataRole); if (dataIndexCourse.id === courseId) { root.selectedCourse = dataIndexCourse return } } // if this fails try to select course matching the current keyboard layout if (coursesForCurrentKeyboardLayoutModel.rowCount() > 0) { - var blub = coursesForCurrentKeyboardLayoutModel.data(coursesForCurrentKeyboardLayoutModel.index(0, 0), ResourceModel.DataRole); - console.log(blub) - root.selectedCourse = blub + var course = coursesForCurrentKeyboardLayoutModel.data(coursesForCurrentKeyboardLayoutModel.index(0, 0), ResourceModel.DataRole); + root.selectedCourse = course return; } // finally just select the first course if (allCoursesModel.rowCount() > 0) { root.selectedCourse = allCoursesModel.data(allCoursesModel.index(0, 0), ResourceModel.DataRole); } } onSelectedCourseChanged: { root.selectedKeyboardLayoutName = root.selectedCourse.keyboardLayoutName; for (var i = 0; i < ktouch.globalDataIndex.keyboardLayoutCount; i++) { var dataIndexLayout = ktouch.globalDataIndex.keyboardLayout(i) if (dataIndexLayout.name === root.selectedKeyboardLayoutName) { root.selectedKeyboardLayout = dataIndexLayout; return } } root.selectedKeyboardLayout = null; } function saveLastUsedCourse(course) { if (profile.lastUsedCourseId != course.id) { profile.lastUsedCourseId = course.id; profileDataAccess.updateProfile(profileDataAccess.indexOfProfile(profile)); } } onProfileChanged: selectLastUsedCourse() ResourceModel { id: allResourcesModel dataIndex: ktouch.globalDataIndex onRowsRemoved: { selectLastUsedCourse() } onRowsInserted: { selectLastUsedCourse() } } CategorizedResourceSortFilterProxyModel { id: allCoursesModel resourceModel: allResourcesModel resourceTypeFilter: ResourceModel.CourseItem } CategorizedResourceSortFilterProxyModel { id: coursesForCurrentKeyboardLayoutModel resourceModel: allResourcesModel resourceTypeFilter: ResourceModel.CourseItem keyboardLayoutNameFilter: root.currentKeyboardLayoutName } CategorizedResourceSortFilterProxyModel { id: currentKeyboardLayoutsModel resourceModel: allResourcesModel resourceTypeFilter: ResourceModel.KeyboardLayoutItem keyboardLayoutNameFilter: root.currentKeyboardLayoutName } CategorizedResourceSortFilterProxyModel { id: otherKeyboardLayoutsModel resourceModel: allResourcesModel resourceTypeFilter: ResourceModel.KeyboardLayoutItem keyboardLayoutNameFilter: root.currentKeyboardLayoutName invertedKeyboardLayoutNameFilter: true } KColorScheme { id: courseSelectorColorScheme colorGroup: KColorScheme.Active colorSet: KColorScheme.View } Rectangle { id: bg anchors.fill: parent color: courseSelectorColorScheme.normalBackground } Flickable { clip: true anchors.fill: parent contentWidth: width contentHeight: content.height Column { id: content width: parent.width Loader { width: parent.width active: root.currentKeyboardLayoutName != 'unknown' sourceComponent: CourseSelectorKeyboardLayoutList { width: parent.width title: i18n("Courses For Your Keyboard Layout") model: currentKeyboardLayoutsModel resourceModel: allResourcesModel colorScheme: courseSelectorColorScheme selectedKeyboardLayoutName: root.selectedKeyboardLayoutName selectedCourse: root.selectedCourse onCourseSelected: { root.selectedCourse = course root.saveLastUsedCourse(course) } } } CourseSelectorKeyboardLayoutList { width: parent.width title: root.currentKeyboardLayoutName == 'unknown'? i18n("Courses"): i18n("Other Courses") model: otherKeyboardLayoutsModel resourceModel: allResourcesModel colorScheme: courseSelectorColorScheme selectedKeyboardLayoutName: root.selectedKeyboardLayoutName selectedCourse: root.selectedCourse onCourseSelected: { root.selectedCourse = course root.saveLastUsedCourse(course) } } } Controls.ScrollBar.vertical: ScrollBar { } } } diff --git a/src/qml/homescreen/CourseSelectorKeyboardLayoutItem.qml b/src/qml/homescreen/CourseSelectorKeyboardLayoutItem.qml index b59743f..110d272 100644 --- a/src/qml/homescreen/CourseSelectorKeyboardLayoutItem.qml +++ b/src/qml/homescreen/CourseSelectorKeyboardLayoutItem.qml @@ -1,116 +1,116 @@ /* * Copyright 2012 Sebastian Gottfried * Copyright 2015 Sebastian Gottfried * * 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) any later version. * * 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.9 import ktouch 1.0 import "../common" Column { id: root property string name property alias title: keyboardLayoutItem.text property ResourceModel resourceModel property string selectedKeyboardLayoutName property DataIndexCourse selectedCourse signal courseSelected(DataIndexCourse course) height: keyboardLayoutItem.height + (loader.active? loader.height: 0) clip: true onSelectedKeyboardLayoutNameChanged: { if (selectedKeyboardLayoutName == root.name) { loader.active = true } } CategorizedResourceSortFilterProxyModel { id: courseModel resourceModel: root.resourceModel resourceTypeFilter: ResourceModel.CourseItem keyboardLayoutNameFilter: loader.keyboardLayoutNameFilter } ListItem { id: keyboardLayoutItem iconName: "input-keyboard" width: parent.width onClicked: { loader.active = !loader.active if (loader.active) { if (courseModel.rowCount()) { courseSelected(courseModel.data(courseModel.index(0, 0), ResourceModel.DataRole)) } } } } Loader { id: loader width: parent.width active: false property string keyboardLayoutNameFilter: root.name sourceComponent: Component { id: courseSelectionComponent Column { Repeater { id: courseRepeater model: courseModel ListItem { text: dataRole.title width: parent.width reserveSpaceForIcon: true highlighted: root.selectedCourse == dataRole onClicked: { courseSelected(dataRole) } } } ListItem { DataIndexCourse { id: customLessonsCourse title: i18n("Custom Lessons") keyboardLayoutName: root.name Component.onCompleted: { id = "custom_lessons" } } text: customLessonsCourse.title id: ownLessonsItem reserveSpaceForIcon: true width: parent.width highlighted: root.selectedCourse == customLessonsCourse onClicked: { courseSelected(customLessonsCourse) } } } } } Behavior on height { NumberAnimation { - duration: 150 + duration: Units.shortDuration easing.type: Easing.InOutQuad } } } diff --git a/src/qml/homescreen/HomeScreen.qml b/src/qml/homescreen/HomeScreen.qml index 13b0ad1..4f8a5c7 100644 --- a/src/qml/homescreen/HomeScreen.qml +++ b/src/qml/homescreen/HomeScreen.qml @@ -1,147 +1,140 @@ /* * Copyright 2012 Sebastian Gottfried * Copyright 2015 Sebastian Gottfried * * 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) any later version. * * 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.9 import QtQuick.Layouts 1.3 import QtGraphicalEffects 1.0 import ktouch 1.0 import "../common" FocusScope { id: screen property KeyboardLayout selectedKeyboardLayout: KeyboardLayout {} property string activeKeyboardLayoutName signal lessonSelected(variant course, variant lesson, variant profile) Connections { target: profileDataAccess onProfileCountChanged: findCurrentProfile() } function start() {} function reset() { profileDataAccess.loadProfiles(); } function findCurrentProfile() { profileComboBox.profile = null var lastProfileId = preferences.lastUsedProfileId for (var i = 0; i < profileDataAccess.profileCount; i++) { var profile = profileDataAccess.profile(i) if (profile.id === lastProfileId) { profileComboBox.profile = profile return; } } if (profileDataAccess.profileCount > 0) { profileComboBox.profile = profileDataAccess.profile(0) preferences.lastUsedProfileId = profileComboBox.profile.id preferences.writeConfig() } } function safeLastUsedProfile(profile) { preferences.lastUsedProfileId = profile.id preferences.writeConfig() } RowLayout { anchors.fill: parent spacing: 0 Item { id: navigationArea z: 2 - Layout.preferredWidth: 300 + Layout.preferredWidth: 16 * Units.gridUnit Layout.fillHeight: true DropShadow { anchors.fill: navigationAreaLayout source: navigationAreaLayout - samples: 16 + radius: Units.largeSpacing + samples: 2 * radius + 1 horizontalOffset: 0 verticalOffset: 0 } ColumnLayout { id: navigationAreaLayout anchors.fill: parent spacing: 0 ToolBar { id: header Layout.fillWidth: true dimFactor: 1.3 - RowLayout { + ProfileComboBox { anchors.fill: parent - spacing: 5 - - - ProfileComboBox { - id: profileComboBox - colorScheme: header.colorScheme - manageProfileButtonBgColor: header.colorScheme.toolbarBackground - Layout.fillHeight: true - Layout.preferredWidth: 300 - Layout.fillWidth: true - onActivated: { - safeLastUsedProfile(profile) - } + id: profileComboBox + colorScheme: header.colorScheme + manageProfileButtonBgColor: header.colorScheme.toolbarBackground + onActivated: { + safeLastUsedProfile(profile) } } } CourseSelector { id: courseSelector Layout.fillHeight: true Layout.fillWidth: true profile: profileComboBox.profile currentKeyboardLayoutName: screen.activeKeyboardLayoutName onSelectedKeyboardLayoutChanged: { dataAccess.loadKeyboardLayout(courseSelector.selectedKeyboardLayout, screen.selectedKeyboardLayout) } } } } LessonSelector { Layout.fillHeight: true Layout.fillWidth: true profile: profileComboBox.profile selectedKeyboardLayout: screen.selectedKeyboardLayout activeKeyboardLayoutName: screen.activeKeyboardLayoutName dataIndexCourse: courseSelector.selectedCourse onLessonSelected: screen.lessonSelected(course, lesson, profileComboBox.profile) z: 1 focus: true } } InitialProfileDialog { id: initialProfileForm visible: profileDataAccess.profileCount == 0 } } diff --git a/src/qml/homescreen/InitialProfileDialog.qml b/src/qml/homescreen/InitialProfileDialog.qml index 349637b..c98e138 100644 --- a/src/qml/homescreen/InitialProfileDialog.qml +++ b/src/qml/homescreen/InitialProfileDialog.qml @@ -1,46 +1,46 @@ /* * Copyright 2012 Sebastian Gottfried * Copyright 2015 Sebastian Gottfried * * 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) any later version. * * 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.9 import QtQuick.Layouts 1.3 import ktouch 1.0 import '../common' PopupDialog { id: root modal: true title: i18n("Welcome to Typewriting Trainer") closePolicy: PopupDialog.NoAutoClose - padding: 20 + padding: Units.gridUnit function save() { var profile = profileDataAccess.createProfile() profile.name = form.name profile.skillLevel = form.skillLevel profileDataAccess.addProfile(profile) close() } contentItem: ProfileForm { id: form doneButtonIconSource: "go-next-view" doneButtonText: i18n("Start Training") onDone: save() } } diff --git a/src/qml/homescreen/KeyboardLayoutMismatchMessage.qml b/src/qml/homescreen/KeyboardLayoutMismatchMessage.qml index b0633eb..2ac3bba 100644 --- a/src/qml/homescreen/KeyboardLayoutMismatchMessage.qml +++ b/src/qml/homescreen/KeyboardLayoutMismatchMessage.qml @@ -1,84 +1,83 @@ /* * Copyright 2017 Sebastian Gottfried * * 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) any later version. * * 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.9 import QtQuick.Layouts 1.3 import ktouch 1.0 import "../common" Collapsable { id: root property string activeKeyboardLayoutName: '' background: Rectangle { color: colorScheme.neutralBackground } Item { implicitWidth: root.width - implicitHeight: layout.implicitHeight + 20 + implicitHeight: layout.implicitHeight + 2 * Units.largeSpacing GridLayout { id: layout anchors.centerIn: parent - width: root.width - 40 - rowSpacing: label.font.pixelSize - columnSpacing: 10 + width: root.width - 2 * Units.gridUnit + rowSpacing: Units.gridUnit + columnSpacing: Units.gridUnit Icon { Layout.column: 0 Layout.row: 0 - width: 32 - height: 32 + height: Units.fontMetrics.roundedIconSize(2 * Units.gridUnit) icon: "dialog-warning" } Label { id: label Layout.column: 1 Layout.row: 0 Layout.fillWidth: true font.bold: true wrapMode: Text.Wrap text: root.activeKeyboardLayoutName == 'unknown'? i18n("The selected course is intended for a specific keyboard layout."): i18n("The selected course is not intended for your computer's active keyboard layout.") } Label { Layout.column: 1 Layout.row: 1 Layout.columnSpan: configureKeyboardButton.visible? 2: 1 Layout.fillWidth: true wrapMode: Text.Wrap text: i18n("KTouch can't switch or set up keyboard layouts. Before training, you have to configure your computer to use the correct keyboard layout. Otherwise you will have to use your current keyboard layout to type the lesson text.") opacity: 0.7 } IconButton { id: configureKeyboardButton Layout.column: 2 Layout.row: 0 visible: ktouch.keyboardKCMAvailable text: i18n("Configure Keyboard") onClicked: { ktouch.showKeyboardKCM(); } } } } } diff --git a/src/qml/homescreen/LessonDeletedMessage.qml b/src/qml/homescreen/LessonDeletedMessage.qml index aadb34b..4776f25 100644 --- a/src/qml/homescreen/LessonDeletedMessage.qml +++ b/src/qml/homescreen/LessonDeletedMessage.qml @@ -1,80 +1,80 @@ /* * Copyright 2017 Sebastian Gottfried * * 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) any later version. * * 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.9 import QtQuick.Layouts 1.3 import ktouch 1.0 import "../common" Collapsable { id: root collapsed: true property Lesson deletedLesson: Lesson {} signal undeleteRequested() property bool completed: false onVisibleChanged: { if (!visible && completed) { destroy(); } } Component.onCompleted: { root.collapsed = false; root.completed = true; } Item { implicitWidth: root.width - implicitHeight: layout.implicitHeight + 20 + implicitHeight: layout.implicitHeight + 2 * Units.largeSpacing RowLayout { id: layout - width: root.width - 40 + width: root.width - 2 * Units.gridUnit anchors.centerIn: parent Label { text: root.deletedLesson.title? i18n("Lesson %1 deleted.", root.deletedLesson.title): i18n("Lesson without title deleted.") wrapMode: Text.Wrap Layout.fillWidth: true } IconButton { iconName: "edit-undo" text: i18n("Undo") onClicked: { root.undeleteRequested(); root.collapsed = true; } } AutoTriggerButton { id: okButton iconName: "dialog-ok" text: i18n("Confirm") onClicked: { root.collapsed = true; } } } } } diff --git a/src/qml/homescreen/LessonEditorDialog.qml b/src/qml/homescreen/LessonEditorDialog.qml index ce44836..fed4cce 100644 --- a/src/qml/homescreen/LessonEditorDialog.qml +++ b/src/qml/homescreen/LessonEditorDialog.qml @@ -1,109 +1,109 @@ /* * Copyright 2017 Sebastian Gottfried * * 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) any later version. * * 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.9 import QtQuick.Layouts 1.3 import ktouch 1.0 import '../common' PopupDialog { id: root property Lesson lesson: Lesson {} property KeyboardLayout keyboardLayout: KeyboardLayout {} property Profile profile: Profile {} onClosed: { profileDataAccess.storeCustomLesson(root.lesson, root.profile, root.keyboardLayout.name) } title: i18n("Edit lesson") margins: { - left: 40 - bottom: 40 - right: 40 - top: 40 + left: 2 * Units.gridUnit + bottom: 2 * Units.gridUnit + right: 2 * Units.gridUnit + top: 2 * Units.gridUnit } width: parent.width - leftMargin - rightMargin height: parent.height - topMargin - bottomMargin - padding: titleLabel.font.pixelSize + padding: Units.gridUnit contentItem: GridLayout { - columnSpacing: titleLabel.font.pixelSize - rowSpacing: titleLabel.font.pixelSize + columnSpacing: Units.gridUnit + rowSpacing: Units.gridUnit Label { id: titleLabel text: i18n("Title:") Layout.row: 0 Layout.column: 0 } TextField { id: titleTextField text: root.lesson? root.lesson.title: "" onTextChanged: { if (root.lesson) { root.lesson.title = text } } Layout.row: 0 Layout.column: 1 Layout.fillWidth: true } ScrollView { Layout.row: 1 Layout.column: 0 Layout.columnSpan: 2 Layout.fillHeight: true Layout.fillWidth: true TextArea { id: lessonTextArea text: root.lesson? root.lesson.text: "" onTextChanged: { if (root.lesson) { root.lesson.text = text } } placeholderText: i18n("Lesson text") font.family: "monospace" LessonTextHighlighter { document: lessonTextArea.textDocument allowedCharacters: root.visible? keyboardLayout.allCharacters(): "" } } } } footer: IconButton { text: i18n("Done") iconName: "dialog-ok" bgColor: colorScheme.positiveBackground onClicked: { root.close() } } } diff --git a/src/qml/homescreen/LessonLockedNotice.qml b/src/qml/homescreen/LessonLockedNotice.qml index 33e13ce..4ffc960 100644 --- a/src/qml/homescreen/LessonLockedNotice.qml +++ b/src/qml/homescreen/LessonLockedNotice.qml @@ -1,82 +1,83 @@ /* * Copyright 2012 Sebastian Gottfried * Copyright 2015 Sebastian Gottfried * * 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) any later version. * * 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.9 import QtQuick.Layouts 1.3 import QtGraphicalEffects 1.0 import ktouch 1.0 import '../common' Item { id: root property color glowColor: "#ffffff" KColorScheme { id: palette colorGroup: KColorScheme.Active colorSet: KColorScheme.Window } ColumnLayout { id: content anchors.centerIn: parent width: parent.witdh - spacing: 10 + spacing: Units.largeSpacing Icon { id: icon icon: "object-locked" - Layout.preferredWidth: 128 - Layout.preferredHeight: 128 + Layout.preferredWidth: Units.fontMetrics.roundedIconSize(8 * Units.gridUnit) + Layout.preferredHeight: Layout.preferredWidth Layout.alignment: Qt.AlignHCenter } Label { id: text text: i18n("Complete Previous Lessons to Unlock") horizontalAlignment: Text.AlignHCenter wrapMode: Text.Wrap - topPadding: text.font.pixelSize / 2 - bottomPadding: topPadding + topPadding: Units.largeSpacing + bottomPadding: Units.largeSpacing background: Rectangle { radius: text.font.pixelSize color: palette.neutralBackground } FontMetrics { id: metrics font: text.font } Layout.alignment: Qt.AlignHCenter Layout.maximumWidth: root.width Layout.preferredWidth: metrics.boundingRect(text.text).width + 2 * text.font.pixelSize } } Glow { anchors.fill: content source: content color: root.glowColor - samples: 25 + radius: Math.floor(.75 * Units.gridUnit) + samples: 2 * radius + 1 } } diff --git a/src/qml/homescreen/LessonSelector.qml b/src/qml/homescreen/LessonSelector.qml index 63b5238..317f9cb 100644 --- a/src/qml/homescreen/LessonSelector.qml +++ b/src/qml/homescreen/LessonSelector.qml @@ -1,398 +1,400 @@ /* * Copyright 2012 Sebastian Gottfried * Copyright 2015 Sebastian Gottfried * * 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) any later version. * * 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.9 import QtQuick.Layouts 1.3 import ktouch 1.0 import QtGraphicalEffects 1.0 import "../common" FocusScope { id: root property Profile profile property DataIndexCourse dataIndexCourse property KeyboardLayout selectedKeyboardLayout property string activeKeyboardLayoutName property alias course: courseItem property Lesson selectedLesson: null signal lessonSelected(Course course, Lesson lesson) function update() { if (!course.isValid) return; if (!profile) return; course.updateLastUnlockedLessonIndex(); selectLastLesson() } function selectLastLesson() { var lessonId = profileDataAccess.courseProgress(profile, course.id, ProfileDataAccess.LastSelectedLesson); if (lessonId !== "") { for (var index = 0; index < course.lessonCount; index++) { if (course.lesson(index).id === lessonId) { root.selectedLesson = course.lesson(index) content.currentIndex = index return } } } if (course.lessonCount > 0) { root.selectedLesson = course.lesson(0) content.currentIndex = 0 } } onProfileChanged: update() onDataIndexCourseChanged: { root.selectedLesson = null; course.update(); root.update(); } Course { id: courseItem property int lastUnlockedLessonIndex: -1 property bool editable: courseItem.isValid && courseItem.id === "custom_lessons" function update() { if (root.dataIndexCourse === null) { return } if (dataIndexCourse.id == "custom_lessons") { profileDataAccess.loadCustomLessons(root.profile, dataIndexCourse.keyboardLayoutName, courseItem) } else { if (isValid && courseItem.id === dataIndexCourse.id) { return } dataAccess.loadCourse(dataIndexCourse, courseItem) } } function updateLastUnlockedLessonIndex() { lastUnlockedLessonIndex = 0; if (course.kind == Course.LessonCollection || profile.skillLevel === Profile.Advanced) { lastUnlockedLessonIndex = course.lessonCount - 1; return } var lastUnlockedLessonId = profileDataAccess.courseProgress(profile, course.id, ProfileDataAccess.LastUnlockedLesson); if (lastUnlockedLessonId !== "") { for (var index = 0; index < course.lessonCount; index++) { lastUnlockedLessonIndex = index if (course.lesson(index).id === lastUnlockedLessonId) { return } } } } function createNewCustomLesson() { var lesson = ktouch.createLesson(); lesson.id = utils.uuid() profileDataAccess.storeCustomLesson(lesson, root.profile, root.selectedKeyboardLayout.name) course.addLesson(lesson) updateLastUnlockedLessonIndex() content.currentIndex = course.indexOfLesson(lesson) root.selectedLesson = lesson lessonEditorDialog.open() } function deleteCustomLesson() { var msgItem = lessonDeletedMessageComponent.createObject(header); msgItem.deletedLesson.copyFrom(selectedLesson); profileDataAccess.deleteCustomLesson(selectedLesson.id); course.removeLesson(course.indexOfLesson(selectedLesson)); root.selectedLesson = null; content.currentIndex = -1; } function restoreCustomLesson(lesson) { course.addLesson(lesson); profileDataAccess.storeCustomLesson(lesson, root.profile, root.selectedKeyboardLayout.name) content.currentIndex = course.indexOfLesson(lesson); updateLastUnlockedLessonIndex(); } Component.onCompleted: update() } StatPopupDialog { id: statPopupDialog profile: root.profile course: courseItem lesson: root.selectedLesson } LessonEditorDialog { id: lessonEditorDialog profile: root.profile keyboardLayout: root.selectedKeyboardLayout lesson: root.selectedLesson } ColumnLayout { anchors.fill: parent spacing: 0 Item { Layout.fillWidth: true Layout.preferredHeight: header.height z: 2 Column { id: header width: parent.width ToolBar { id: toolbar width: parent.width dimFactor: 1.5 RowLayout { anchors.fill: parent - anchors.leftMargin: 20 - spacing: 5 + anchors.leftMargin: Units.gridUnit + spacing: Units.smallSpacing Label { text: root.course? root.course.title: "" font.bold: true color: toolbar.colorScheme.normalText } IconToolButton { id: toggleCourseDesciptionButton iconName: "help-about" checkable: true color: toolbar.colorScheme.normalText backgroundColor: toolbar.colorScheme.normalBackground Layout.fillHeight: true Layout.preferredWidth: toolbar.height } ToolSeparator { visible: courseItem.editable } IconToolButton { id: newLessonButton iconName: "document-new" text: "Add New Lesson" color: toolbar.colorScheme.normalText backgroundColor: toolbar.colorScheme.normalBackground visible: courseItem.editable Layout.fillHeight: true onClicked: { course.createNewCustomLesson() } } Item { Layout.fillWidth: true } IconToolButton { id: configureButton iconName: "application-menu" color: toolbar.colorScheme.normalText backgroundColor: toolbar.colorScheme.normalBackground Layout.fillHeight: true Layout.preferredWidth: toolbar.height - Layout.rightMargin: 20 + Layout.rightMargin: Units.gridUnit onClicked: { var position = mapToItem(null, 0, height) ktouch.showMenu(position.x, position.y) } } } } CourseDescriptionItem { id: courseDescriptionItem width: parent.width collapsed: !toggleCourseDesciptionButton.checked description: courseItem.description } KeyboardLayoutMismatchMessage { width: parent.width activeKeyboardLayoutName: root.activeKeyboardLayoutName collapsed: !root.course || !root.course.isValid || root.activeKeyboardLayoutName == root.course.keyboardLayoutName } Component { id: lessonDeletedMessageComponent LessonDeletedMessage { width: parent.width onUndeleteRequested: { course.restoreCustomLesson(deletedLesson) } } } } DropShadow { anchors.fill: header source: header - samples: 16 + radius: Units.largeSpacing + samples: 2 * radius + 1 horizontalOffset: 0 verticalOffset: 0 } } Item { Layout.fillHeight: true Layout.fillWidth: true z: 1 Rectangle { anchors.fill: parent color: content.colorScheme.shade(content.colorScheme.normalBackground, KColorScheme.DarkShade, 1, 0.0) } GridView { id: content anchors.fill: parent clip: true focus: true background.color: colorScheme.shade(colorScheme.normalBackground, KColorScheme.DarkShade, 1, 0.0) - property int columns: Math.floor(width / (300 + 20)) + property int columns: Math.floor(width / (17 * Units.gridUnit)) cellWidth: Math.floor(content.contentWidth / content.columns) cellHeight: Math.round(cellWidth * 2 / 3) - contentWidth: width - 20 - rightMargin: 20 - bottomMargin: 20 + contentWidth: width - Units.gridUnit + rightMargin: Units.gridUnit + bottomMargin: Units.gridUnit Keys.onPressed: { if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { event.accepted = true; if (root.selectedLesson && content.currentIndex <= course.lastUnlockedLessonIndex) { lessonSelected(course, root.selectedLesson) } } } model: LessonModel { id: lessonModel course: courseItem } onCurrentIndexChanged: { if (lessonModel.rowCount() > 0 && currentIndex != -1) { root.selectedLesson = lessonModel.data(lessonModel.index(currentIndex, 0), LessonModel.DataRole) } else { root.selectedLesson = null } } delegate: Item { id: item width: content.cellWidth height: content.cellHeight LessonSelectorItem { id: lessonItem anchors.fill: parent - anchors.leftMargin: 20 - anchors.topMargin: 20 + anchors.leftMargin: Units.gridUnit + anchors.topMargin: Units.gridUnit anchors.centerIn: parent lesson: dataRole selected: content.currentIndex == index editable: courseItem.editable onClicked: { item.forceActiveFocus() content.currentIndex = index } onDoubleClicked: { if (index <= course.lastUnlockedLessonIndex) { lessonSelected(course, dataRole) } } onStatButtonClicked: { statPopupDialog.open() } onEditButtonClicked: { lessonEditorDialog.open() } onDeleteButtonClicked: { course.deleteCustomLesson(); } LessonLockedNotice { anchors.fill: parent - anchors.margins: 5 + anchors.margins: Units.smallSpacing visible: index > course.lastUnlockedLessonIndex glowColor: lessonItem.background.color } } } } } Item { Layout.fillWidth: true height: footer.height z: 2 ToolBar { id: footer width: parent.width KColorScheme { id: footerColorScheme colorGroup: KColorScheme.Active colorSet: KColorScheme.Window } background: Rectangle { color: footerColorScheme.normalBackground } RowLayout { anchors.fill: parent spacing: 0 Item { Layout.fillWidth: true height: startButton.implicitHeight IconButton { id: startButton iconName: "go-next-view" bgColor: colorScheme.positiveBackground - anchors.centerIn: parent text: i18n("Start Training") + anchors.centerIn: parent enabled: root.selectedLesson && content.currentIndex <= course.lastUnlockedLessonIndex onClicked: lessonSelected(course, root.selectedLesson) } } } } DropShadow { anchors.fill: footer source: footer - samples: 16 + radius: Units.largeSpacing + samples: 2 * radius + 1 horizontalOffset: 0 verticalOffset: 0 } } } } diff --git a/src/qml/homescreen/LessonSelectorItem.qml b/src/qml/homescreen/LessonSelectorItem.qml index 29bb54b..6d34b5f 100644 --- a/src/qml/homescreen/LessonSelectorItem.qml +++ b/src/qml/homescreen/LessonSelectorItem.qml @@ -1,159 +1,159 @@ /* * Copyright 2017 Sebastian Gottfried * * 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) any later version. * * 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.9 import QtQuick.Layouts 1.3 import ktouch 1.0 import '../common' Item { id: root property Lesson lesson property bool selected property bool editable: false property alias background: background signal clicked signal doubleClicked signal deleteButtonClicked signal editButtonClicked signal statButtonClicked clip: true KColorScheme { id: selectionColorScheme colorGroup: KColorScheme.Active colorSet: KColorScheme.Selection } Rectangle { id: background anchors.fill: parent color: selected? Qt.tint(selectionColorScheme.normalBackground, "#a0ffffff"): "#ffffff" } MouseArea { anchors.fill: parent onClicked: { root.clicked() } onDoubleClicked: { root.doubleClicked() } } GridLayout { id: content anchors.fill: parent - anchors.margins: 10 + anchors.margins: Units.largeSpacing Label { id: titleLabel Layout.column: 0 Layout.row: 0 Layout.fillWidth: true Layout.preferredHeight: buttonRow.implicitHeight text: lesson? lesson.title: "" color: "#000000" font.bold: true elide: Label.ElideRight verticalAlignment: Qt.AlignVCenter ToolTip { parent: titleLabel text: titleLabel.text visible: titleMouseArea.containsMouse } MouseArea { id: titleMouseArea anchors.fill: parent acceptedButtons: Qt.NoButton hoverEnabled: titleLabel.truncated } } Row { id: buttonRow Layout.column: 1 Layout.row: 0 visible: root.selected IconToolButton { id: editButton visible: root.editable iconName: 'edit-entry' color: "#000000" backgroundColor: "#c0c0c0c0" onClicked: { root.editButtonClicked(); } } IconToolButton { id: deleteButton visible: root.editable iconName: 'edit-delete' color: "#000000" backgroundColor: "#c0c0c0c0" onClicked: { root.deleteButtonClicked(); } } IconToolButton { iconName: 'view-statistics' color: "#000000" backgroundColor: "#c0c0c0c0" onClicked: { root.statButtonClicked(); } } } Item { Layout.column: 0 Layout.row: 1 Layout.columnSpan: 2 Layout.fillHeight: true Layout.fillWidth: true clip: true Label { id: textLabel anchors.fill: parent text: lesson? lesson.text: "" color: "#000000" font.family: 'monospace' lineHeight: 1.5 scale: Math.min(1, width / implicitWidth) transformOrigin: Item.TopLeft } Rectangle { anchors.fill: parent gradient: Gradient { GradientStop { position: 0.0; color: "#00000000" } GradientStop { position: 0.8; color: Qt.rgba(background.color.r, background.color.g, background.color.b, 0) } GradientStop { position: 1.0; color: background.color } } } } } } diff --git a/src/qml/homescreen/ProfileComboBox.qml b/src/qml/homescreen/ProfileComboBox.qml index f1c78bf..e9f537e 100644 --- a/src/qml/homescreen/ProfileComboBox.qml +++ b/src/qml/homescreen/ProfileComboBox.qml @@ -1,112 +1,112 @@ /* * Copyright 2017 Sebastian Gottfried * * 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) any later version. * * 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.9 import QtGraphicalEffects 1.0 import ktouch 1.0 import "../common" ComboBox { id: root property Profile profile property color manageProfileButtonBgColor model: profileDataAccess.profileCount contentItem: IconLabel { iconName: "user-identity" text: profile !== null? profile.name: "" color: colorScheme.normalText } onActivated: { profile = index < profileDataAccess.profileCount? profileDataAccess.profile(index): null } onProfileChanged: { for (var i = 0; i < profileDataAccess.profileCount; i++) { if (profile === profileDataAccess.profile(i)) { if (i != currentIndex) { currentIndex = i; } break; } } } delegate: ListItem { width: root.width text: index < profileDataAccess.profileCount? profileDataAccess.profile(index).name: "" highlighted: root.currentIndex === index } popupListView.footer: IconButton { id: manageProfileButton width: parent.width color: root.colorScheme.normalText bgColor: root.manageProfileButtonBgColor text: i18n("Manage Profiles") iconName: "user-properties" MouseArea { anchors.fill: parent hoverEnabled: false onPressed: { mouse.accepted = true } onClicked: { root.popup.close() mouse.accepted = true manageProfileDialog.open() } } } PopupDialog { id: manageProfileDialog margins: { - left: 40 - bottom: 40 - right: 40 - top: 40 + left: 2 * Units.gridUnit + bottom: 2 * Units.gridUnit + right: 2 * Units.gridUnit + top: 2 * Units.gridUnit } width: parent.width - leftMargin - rightMargin height: parent.height - topMargin - bottomMargin modal: true focus: true title: i18n("Manage Profiles") closePolicy: PopupDialog.CloseOnEscape padding: 0 contentItem: ProfileSelector { id: profileSelector onProfileChosen: { root.profile = profile manageProfileDialog.close() } } onOpened: { if (profileComboBox.profile) { var index = profileDataAccess.indexOfProfile(profileComboBox.profile) profileSelector.selectProfile(index) } } } } diff --git a/src/qml/homescreen/ProfileDetailsItem.qml b/src/qml/homescreen/ProfileDetailsItem.qml index 3432b9d..689fd4c 100644 --- a/src/qml/homescreen/ProfileDetailsItem.qml +++ b/src/qml/homescreen/ProfileDetailsItem.qml @@ -1,284 +1,284 @@ /* * Copyright 2012 Sebastian Gottfried * Copyright 2015 Sebastian Gottfried * Copyright 2017 Sebastian Gottfried * * 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) any later version. * * 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.9 import QtQuick.Layouts 1.3 import org.kde.charts 0.1 as Charts import org.kde.kcoreaddons 1.0 import ktouch 1.0 import "../common" Item { id: root property Profile profile function update() { if (profile) { var isNewProfile = root.profile.id === -1 profileForm.name = profile.name profileForm.skillLevel = profile.skillLevel profileForm.skillLevelSelectionEnabled = isNewProfile deleteConfirmationLabel.name = profile.name state = isNewProfile? "editor": "info" } } signal deletionRequest(); onProfileChanged: update() SystemPalette { id: activePalette colorGroup: SystemPalette.Active } KColorScheme { id: colorScheme colorGroup: KColorScheme.Active colorSet: KColorScheme.View } Item { id: infoContainer width: parent.width height: childrenRect.height anchors.centerIn: parent Column { width: parent.width height: childrenRect.height - spacing: 40 + spacing: 2 * Units.gridUnit LearningProgressModel { id: learningProgressModel profile: root.profile } Rectangle { anchors.horizontalCenter: parent.horizontalCenter width: parent.width - height: 250 + height: 14 * Units.gridUnit color: activePalette.base border { - width: 1 + width: Units.borderWidth color: activePalette.text } Column { id: column anchors { fill: parent topMargin: column.spacing + spacing + legend.height leftMargin: column.spacing rightMargin: column.spacing bottomMargin: column.spacing } - spacing: 20 + spacing: Units.gridUnit width: parent.width height: parent.height - legend.height - parent.spacing LearningProgressChart { id: learningProgressChart anchors.horizontalCenter: parent.horizontalCenter width: parent.width height: parent.height - legend.height - parent.spacing backgroundColor: colorScheme.normalBackground textColor: colorScheme.normalText model: learningProgressModel } Row { id: legend anchors.horizontalCenter: parent.horizontalCenter - spacing: 20 + spacing: Units.gridUnit Charts.LegendItem { dimension: learningProgressChart.accuracy textColor: colorScheme.normalText } Charts.LegendItem { dimension: learningProgressChart.charactersPerMinute textColor: colorScheme.normalText } } } } InformationTable { id: profileInfoTable width: parent.width property int trainedLessonCount: profile && profile.id !== -1? profileDataAccess.lessonsTrained(profile): 0 property list infoModel: [ InfoItem { title: i18n("Lessons trained:") text: profile && profile.id !== -1? profileInfoTable.trainedLessonCount: "" }, InfoItem { title: i18n("Total training time:") text: profile && profile.id !== -1? Format.formatDuration(profileDataAccess.totalTrainingTime(profile)): "" }, InfoItem { title: i18n("Last trained:") text: profile && profile.id !== -1 && profileInfoTable.trainedLessonCount > 0? profileDataAccess.lastTrainingSession(profile).toLocaleDateString(): i18n("Never") } ] model: infoModel } } InlineToolbar { anchors { top: parent.top horizontalCenter: parent.horizontalCenter - topMargin: 5 + topMargin: Units.largeSpacing } content: [ IconToolButton { iconName: "document-edit" text: i18n("Edit") onClicked: root.state = "editor" }, IconToolButton { iconName: "edit-delete" text: i18n("Delete") enabled: profileDataAccess.profileCount > 1 onClicked: root.state = "deleteConfirmation" } ] } } Item { id: editorContainer width: parent.width height: childrenRect.height anchors.centerIn: parent ProfileForm { id: profileForm width: parent.width showWelcomeLabel: false onDone: { root.profile.name = profileForm.name root.profile.skillLevel = profileForm.skillLevel if (root.profile.id === -1) { profileDataAccess.addProfile(profile) } else { profileDataAccess.updateProfile(profileDataAccess.indexOfProfile(root.profile)) } root.update() root.state = "info" } } } Item { id: deleteConfirmationContainer width: parent.width height: childrenRect.height anchors.centerIn: parent Column { width: parent.width height: childrenRect.height - spacing: 15 + spacing: Units.gridUnit Label { property string name id: deleteConfirmationLabel width: parent.width text: i18n("Do you really want to delete the profile \"%1\"?", name) wrapMode: Text.Wrap horizontalAlignment: Text.AlignHCenter } Row { - spacing: 10 + spacing: Units.largeSpacing anchors.horizontalCenter: parent.horizontalCenter width: childrenRect.width height: childrenRect.height IconButton { iconName: "edit-delete" text: i18n("Delete") bgColor: colorScheme.negativeBackground onClicked: root.deletionRequest() } IconButton { text: i18n("Cancel") onClicked: root.state = "info" } } } } states: [ State { name: "info" PropertyChanges { target: infoContainer visible: true } PropertyChanges { target: editorContainer visible: false } PropertyChanges { target: deleteConfirmationContainer visible: false } }, State { name: "editor" PropertyChanges { target: infoContainer visible: false } PropertyChanges { target: editorContainer visible: true } PropertyChanges { target: deleteConfirmationContainer visible: false } }, State { name: "deleteConfirmation" PropertyChanges { target: infoContainer visible: false } PropertyChanges { target: editorContainer visible: false } PropertyChanges { target: deleteConfirmationContainer visible: true } } ] } diff --git a/src/qml/homescreen/ProfileForm.qml b/src/qml/homescreen/ProfileForm.qml index 1a89256..6bff127 100644 --- a/src/qml/homescreen/ProfileForm.qml +++ b/src/qml/homescreen/ProfileForm.qml @@ -1,115 +1,115 @@ /* * Copyright 2012 Sebastian Gottfried * Copyright 2015 Sebastian Gottfried * * 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) any later version. * * 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.9 import QtQuick.Layouts 1.3 import ktouch 1.0 import "../common" ColumnLayout { id: root property alias name: nameTextField.text property int skillLevel: 0 property bool skillLevelSelectionEnabled: true property alias showWelcomeLabel: welcomeLabel.visible property alias doneButtonIconSource: doneBtn.iconName property alias doneButtonText: doneBtn.text signal done() onSkillLevelChanged: { beginnerRadioButton.checked = skillLevel == Profile.Beginner advancedRadioButton.checked = skillLevel == Profile.Advanced } - spacing: 15 + spacing: Units.gridUnit Label { id: welcomeLabel Layout.fillWidth: true - Layout.preferredWidth: 500 + Layout.preferredWidth: 24 * Units.gridUnit text: i18n("Before you start training, please introduce yourself:") } TextField { id: nameTextField Layout.fillWidth: true placeholderText: i18n("Name") } RadioButton { id: beginnerRadioButton Layout.maximumWidth: parent.width enabled: root.skillLevelSelectionEnabled text: i18n("I have no or only very little experience in machine typing") label.wrapMode: Text.Wrap onCheckedChanged: { if (checked) { root.skillLevel = Profile.Beginner } } } Label { text: i18n("Lessons are unlocked as your typing skills improve over time.") wrapMode: Text.Wrap Layout.maximumWidth: parent.width - leftPadding: font.pixelSize * 2 + leftPadding: font.pixelSize + Units.largeSpacing font.italic: true enabled: root.skillLevelSelectionEnabled } RadioButton { id: advancedRadioButton Layout.maximumWidth: parent.width enabled: root.skillLevelSelectionEnabled text: i18n("I am an experienced machine typist and want to improve my skills") label.wrapMode: Text.Wrap onCheckedChanged: { if (checked) { root.skillLevel = Profile.Advanced } } } Label { text: i18n("All lessons are unlocked immediately.") wrapMode: Text.Wrap Layout.maximumWidth: parent.width leftPadding: font.pixelSize * 2 font.italic: true enabled: root.skillLevelSelectionEnabled } Item { Layout.fillWidth: true Layout.preferredHeight: doneBtn.implicitHeight IconButton { id: doneBtn anchors.horizontalCenter: parent.horizontalCenter bgColor: colorScheme.positiveBackground text: i18n("Done") enabled: nameTextField.text !== "" && (beginnerRadioButton.checked || advancedRadioButton.checked) iconName: "dialog-ok" onClicked: done() } } } diff --git a/src/qml/homescreen/ProfileSelector.qml b/src/qml/homescreen/ProfileSelector.qml index 3cb3295..18a5621 100644 --- a/src/qml/homescreen/ProfileSelector.qml +++ b/src/qml/homescreen/ProfileSelector.qml @@ -1,125 +1,125 @@ /* * Copyright 2012 Sebastian Gottfried * Copyright 2015 Sebastian Gottfried * Copyright 2017 Sebastian Gottfried * * 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) any later version. * * 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.9 import QtQuick.Layouts 1.3 import ktouch 1.0 import "../common" FocusScope { id: root signal profileChosen(variant profile) function createNewProfile() { var profile = profileDataAccess.createProfile() profileForm.profile = profile } function selectProfile(index) { list.currentIndex = index profileForm.profile = profileDataAccess.profile(index) } KColorScheme { id: listColorScheme colorGroup: KColorScheme.Active colorSet: KColorScheme.View } ColumnLayout { anchors.fill: parent - anchors.margins: 20 - spacing: 20 + anchors.margins: Units.gridUnit + spacing: Units.gridUnit RowLayout { Layout.fillWidth: true Layout.fillHeight: true - spacing: 20 + spacing: Units.gridUnit ListView { id: list Layout.fillWidth: true Layout.fillHeight: true model: profileDataAccess.profileCount + 1 clip: true focus: true delegate: ListItem { property bool isNewButton: index >= profileDataAccess.profileCount width: list.width text: isNewButton? i18n("Create New Profile"): index < profileDataAccess.profileCount? profileDataAccess.profile(index).name: null label.font.italic: isNewButton iconName: isNewButton? "list-add": "user-identity" highlighted: ListView.isCurrentItem onClicked: { list.forceActiveFocus() list.currentIndex = index if (isNewButton) { createNewProfile() } else { selectProfile(index) } } onDoubleClicked: { if (!isNewButton) { root.profileChosen(profileDataAccess.profile(list.currentIndex)) } } } onCurrentItemChanged: { if (currentItem != null) { if (currentItem.isNewButton) { createNewProfile() } else { selectProfile(currentIndex) } } } } ProfileDetailsItem { id: profileForm Layout.fillWidth: true Layout.fillHeight: true onDeletionRequest: { var index = profileDataAccess.indexOfProfile(profileForm.profile) profileForm.profile = null profileDataAccess.removeProfile(index) selectProfile(Math.max(0, list.currentIndex - 1)) } } } IconButton { id: selectButton Layout.fillWidth: true iconName: "go-next-view" text: i18n("Use Selected Profile") enabled: list.currentIndex !== -1 && list.currentIndex < profileDataAccess.profileCount bgColor: colorScheme.positiveBackground onClicked: root.profileChosen(profileDataAccess.profile(list.currentIndex)) } } } diff --git a/src/qml/homescreen/StatPopupDialog.qml b/src/qml/homescreen/StatPopupDialog.qml index f5a18c5..2833705 100644 --- a/src/qml/homescreen/StatPopupDialog.qml +++ b/src/qml/homescreen/StatPopupDialog.qml @@ -1,93 +1,93 @@ /* * Copyright 2017 Sebastian Gottfried * * 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) any later version. * * 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.9 import QtQuick.Layouts 1.3 import ktouch 1.0 import org.kde.charts 0.1 as Charts import '../common' PopupDialog { id: root property alias course: learningProgressModel.courseFilter property alias lesson: learningProgressModel.lessonFilter property alias profile: learningProgressModel.profile title: i18n("Lesson training statistics") modal: true margins: { - left: 40 - bottom: 40 - right: 40 - top: 40 + left: 2 * Units.gridUnit + bottom: 2 * Units.gridUnit + right: 2 * Units.gridUnit + top: 2 * Units.gridUnit } width: parent.width - leftMargin - rightMargin height: parent.height - topMargin - bottomMargin padding: 0 contentItem: Rectangle { color: colorScheme.normalBackground ColumnLayout { anchors.fill: parent - anchors.margins: titleLabel.font.pixelSize - spacing: titleLabel.font.pixelSize + anchors.margins: Units.gridUnit + spacing: Units.gridUnit Label { id: titleLabel text: lesson? lesson.title: "" font.bold: true } LearningProgressChart { id: learningProgressChart Layout.fillHeight: true Layout.fillWidth: true backgroundColor: colorScheme.normalBackground textColor: colorScheme.normalText model: LearningProgressModel { id: learningProgressModel } } Row { - spacing: 2 * titleLabel.font.pixelSize + spacing: 2 * Units.gridUnit Layout.alignment: Qt.AlignTop | Qt.AlignHCenter Charts.LegendItem { id: accuracyLegend dimension: learningProgressChart.accuracy textColor: colorScheme.normalText } Charts.LegendItem { id: charactersPerMinuteLegend dimension: learningProgressChart.charactersPerMinute textColor: colorScheme.normalText } } } KColorScheme { id: colorScheme colorGroup: KColorScheme.Active colorSet: KColorScheme.View } } } diff --git a/src/qml/main.qml b/src/qml/main.qml index 37155fd..0caa6fb 100644 --- a/src/qml/main.qml +++ b/src/qml/main.qml @@ -1,239 +1,239 @@ /* * Copyright 2012 Sebastian Gottfried * Copyright 2015 Sebastian Gottfried * * 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) any later version. * * 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.9 import ktouch 1.0 import "./common" import "./meters" import "./homescreen" import "./trainingscreen" import "./scorescreen" Rectangle { id: main color: activePallete.normalBackground property Item appContent: appContentItem function switchScreen(from, to) { switchScreenAnimation.from = from switchScreenAnimation.to = to switchScreenAnimation.start() } KColorScheme { id: activePallete colorGroup: KColorScheme.Active colorSet: KColorScheme.Window } DataAccess { id: dataAccess } QtObject { id: helper property string name: ktouch.keyboardLayoutName property int keyboardLayoutCount: ktouch.globalDataIndex.keyboardLayoutCount property int courseCount: ktouch.globalDataIndex.courseCount onNameChanged: { activeKeyboardLayout.update() } onKeyboardLayoutCountChanged: { if (ktouch.globalDataIndex.isValid) activeKeyboardLayout.update() } } ResourceModel { id: resourceModel dataIndex: ktouch.globalDataIndex } ProfileDataAccess { id: profileDataAccess } Preferences { id: preferences } KeyboardLayout { id: activeKeyboardLayout Component.onCompleted: { if (ktouch.globalDataIndex.isValid) { activeKeyboardLayout.update() } } function update() { isValid = false var name = ktouch.keyboardLayoutName; // first pass - exact match for (var i = 0; i < ktouch.globalDataIndex.keyboardLayoutCount; i++) { var dataIndexLayout = ktouch.globalDataIndex.keyboardLayout(i) if (dataIndexLayout.name === name) { dataAccess.loadKeyboardLayout(dataIndexLayout, activeKeyboardLayout) return } } // second pass - substring match for (var i = 0; i < ktouch.globalDataIndex.keyboardLayoutCount; i++) { var dataIndexLayout = ktouch.globalDataIndex.keyboardLayout(i) if (name.search(dataIndexLayout.name) === 0) { dataAccess.loadKeyboardLayout(dataIndexLayout, activeKeyboardLayout) return } } } } Course { id: selectedCourse property Lesson selectedLesson } Lesson { id: customLessonCopy } Item { anchors.fill: parent id: appContentItem layer.enabled: true HomeScreen { id: homeScreen anchors.fill: parent activeKeyboardLayoutName: activeKeyboardLayout.isValid? activeKeyboardLayout.name: helper.name visible: false focus: true onLessonSelected: { trainingScreen.profile = profile var lessonIndex = -1; for (var i = 0; i < course.lessonCount; i++) { if (lesson === course.lesson(i)) { lessonIndex = i break } } selectedCourse.copyFrom(course) if (lessonIndex !== -1) { selectedCourse.selectedLesson = selectedCourse.lesson(lessonIndex) } else { customLessonCopy.copyFrom(lesson) selectedCourse.selectedLesson = customLessonCopy } main.switchScreen(homeScreen, trainingScreen) } Component.onCompleted: { homeScreen.reset() homeScreen.visible = true homeScreen.start() } } TrainingScreen { id: trainingScreen anchors.fill: parent visible: false keyboardLayout: homeScreen.selectedKeyboardLayout course: selectedCourse lesson: selectedCourse.selectedLesson onRestartRequested: main.switchScreen(trainingScreen, trainingScreen) onAbortRequested: main.switchScreen(trainingScreen, homeScreen) onFinished: main.switchScreen(trainingScreen, scoreScreen) } ScoreScreen { id: scoreScreen anchors.fill: parent visible: false course: trainingScreen.course lesson: trainingScreen.lesson stats: trainingScreen.stats profile: trainingScreen.profile referenceStats: trainingScreen.referenceStats onHomeScreenRequested: main.switchScreen(scoreScreen, homeScreen) onLessonRepetionRequested: main.switchScreen(scoreScreen, trainingScreen) onNextLessonRequested: { selectedCourse.selectedLesson = lesson main.switchScreen(scoreScreen, trainingScreen) } } } Rectangle { id: curtain anchors.fill: parent color: "#000" opacity: 0 } SequentialAnimation { id: switchScreenAnimation property Item from property Item to NumberAnimation { target: curtain property: "opacity" to: 1 - duration: switchScreenAnimation.to == homeScreen? 250: 750 + duration: switchScreenAnimation.to == homeScreen? Units.longDuration: 3 * Units.longDuration easing.type: Easing.OutQuad } PropertyAction { target: switchScreenAnimation.from property: "visible" value: false } ScriptAction { script: switchScreenAnimation.to.reset() } PropertyAction { target: switchScreenAnimation.to property: "visible" value: true } ScriptAction { script: { switchScreenAnimation.to.start() switchScreenAnimation.to.forceActiveFocus() } } NumberAnimation { target: curtain property: "opacity" to: 0 - duration: switchScreenAnimation.to == homeScreen? 250: 750 + duration: switchScreenAnimation.to == homeScreen? Units.longDuration: 3 * Units.longDuration easing.type: Easing.InQuad } } } diff --git a/src/qml/qml.qrc b/src/qml/qml.qrc index 1f7cd9c..43bd4db 100644 --- a/src/qml/qml.qrc +++ b/src/qml/qml.qrc @@ -1,67 +1,69 @@ + common/qmldir common/AutoTriggerButton.qml common/Balloon.qml common/Collapsable.qml common/ComboBox.qml common/FocusBar.qml common/GridView.qml common/Icon.qml common/IconButton.qml common/IconLabel.qml common/IconToolButton.qml common/InfoItem.qml common/InformationTable.qml common/InlineToolbar.qml common/Label.qml common/LearningProgressChart.qml common/ListItem.qml common/ListView.qml common/MessageBox.qml common/MonochromeIcon.qml common/PopupDialog.qml common/RadioButton.qml common/ScrollBar.qml common/ScrollView.qml common/SelectionGrip.qml common/SelectionRectangle.qml common/TextArea.qml common/TextField.qml common/ToolBar.qml common/ToolSeparator.qml common/ToolTip.qml + common/Units.qml homescreen/CourseDescriptionItem.qml homescreen/CourseSelector.qml homescreen/CourseSelectorKeyboardLayoutItem.qml homescreen/CourseSelectorKeyboardLayoutList.qml homescreen/HomeScreen.qml homescreen/InitialProfileDialog.qml homescreen/KeyboardLayoutMismatchMessage.qml homescreen/LessonDeletedMessage.qml homescreen/LessonEditorDialog.qml homescreen/LessonLockedNotice.qml homescreen/LessonSelector.qml homescreen/LessonSelectorItem.qml homescreen/ProfileComboBox.qml homescreen/ProfileDetailsItem.qml homescreen/ProfileForm.qml homescreen/ProfileSelector.qml homescreen/StatPopupDialog.qml keyboard/KeyItem.qml keyboard/KeyLabel.qml keyboard/Keyboard.qml keyboard/KeyboardLayoutEditor.qml main.qml meters/AccuracyMeter.qml meters/CharactersPerMinuteMeter.qml meters/ElapsedTimeMeter.qml meters/Meter.qml meters/StatBox.qml scorescreen/ScoreScreen.qml trainingscreen/KeyboardUnavailableNotice.qml trainingscreen/TrainingScreen.qml trainingscreen/TrainingScreenMenu.qml trainingscreen/TrainingScreenToolbar.qml trainingscreen/TrainingWidget.qml diff --git a/src/qml/scorescreen/ScoreScreen.qml b/src/qml/scorescreen/ScoreScreen.qml index 1952b17..1c75bd1 100644 --- a/src/qml/scorescreen/ScoreScreen.qml +++ b/src/qml/scorescreen/ScoreScreen.qml @@ -1,419 +1,419 @@ /* * Copyright 2018 Sebastian Gottfried * * 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) any later version. * * 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.9 import QtQuick.Layouts 1.3 import ktouch 1.0 import org.kde.charts 0.1 as Charts import "../common" import "../meters" FocusScope { id: screen function start() {} function reset() { internal.lessonPassed = Math.round(1000 * stats.accuracy) >= Math.round(10 * preferences.requiredAccuracy) && stats.charactersPerMinute >= preferences.requiredStrokesPerMinute var lessonIndex = 0; for (var i = 0; i < course.lessonCount; i++) { if (lesson === course.lesson(i)) { lessonIndex = i break } } var lastUnlockedLessonIndex = 0 if (profile.skillLevel === Profile.Advanced) { lastUnlockedLessonIndex = course.lessonCount - 1; internal.nextLessonUnlocked = false } else { var lastUnlockedLessonId = profileDataAccess.courseProgress(profile, course.id, ProfileDataAccess.LastUnlockedLesson); if (lastUnlockedLessonId !== "") { for (var index = 0; index < course.lessonCount; index++) { if (course.lesson(index).id === lastUnlockedLessonId) { lastUnlockedLessonIndex = index; break; } } } internal.nextLessonUnlocked = internal.lessonPassed && lessonIndex === lastUnlockedLessonIndex && lessonIndex + 1 < course.lessonCount if (internal.nextLessonUnlocked) { lastUnlockedLessonIndex++ } } internal.nextLesson = null; if (lessonIndex + 1 < course.lessonCount && lessonIndex + 1 <= lastUnlockedLessonIndex) { internal.nextLesson = course.lesson(lessonIndex + 1) } if (internal.nextLessonUnlocked) { profileDataAccess.saveCourseProgress(internal.nextLesson.id, profile, course.id, ProfileDataAccess.LastUnlockedLesson) } learningProgressModel.update() } function forceActiveFocus() { if (internal.lessonPassed && internal.nextLesson) { nextLessonButton.forceActiveFocus() } else { repeatLessonButton.forceActiveFocus() } } property Profile profile property Lesson lesson property Course course property TrainingStats stats property TrainingStats referenceStats signal homeScreenRequested signal nextLessonRequested(variant lesson) signal lessonRepetionRequested QtObject { id: internal property bool lessonPassed: false property bool nextLessonUnlocked: false property Lesson nextLesson: null } KColorScheme { id: colorScheme colorGroup: KColorScheme.Active colorSet: KColorScheme.View } LearningProgressModel { property bool filterByLesson: learningProgressFilterComboBox.currentIndex == 1 id: learningProgressModel profile: screen.visible? screen.profile: null courseFilter: screen.visible? screen.course: null lessonFilter: screen.visible && filterByLesson? screen.lesson: null } ErrorsModel { id: errorsModel trainingStats: screen.visible? screen.stats: null } Balloon { id: errorsTooltip visualParent: parent property int row: -1 InformationTable { property list infoModel: [ InfoItem { title: i18n("Character:") text: errorsTooltip.row !== -1? errorsModel.character(errorsTooltip.row): "" }, InfoItem { title: i18n("Errors:") text: errorsTooltip.row !== -1? errorsModel.errors(errorsTooltip.row): "" } ] width: 250 model: infoModel } } ColumnLayout { anchors.fill: parent spacing: 0 ToolBar { id: header Layout.fillWidth: true RowLayout { anchors.fill: parent - anchors.leftMargin: 20 - anchors.rightMargin: 20 + anchors.leftMargin: Units.gridUnit + anchors.rightMargin: Units.gridUnit spacing: anchors.leftMargin IconToolButton { id: homeScreenButton text: i18n("Return to Home Screen") iconName: "go-home" colorScheme.colorSet: KColorScheme.Complementary onClicked: screen.homeScreenRequested() } Item { Layout.fillHeight: true Layout.fillWidth: true } IconToolButton { id: repeatLessonButton iconName: "view-refresh" text: i18n("Repeat Lesson") colorScheme.colorSet: KColorScheme.Complementary onClicked: screen.lessonRepetionRequested() } IconToolButton { id: nextLessonButton iconName: "go-next-view" text: i18n("Next Lesson") enabled: internal.nextLesson colorScheme.colorSet: KColorScheme.Complementary onClicked: screen.nextLessonRequested(internal.nextLesson) } } } Rectangle { Layout.fillHeight: true Layout.fillWidth: true color: colorScheme.normalBackground ColumnLayout { id: content anchors.fill: parent - anchors.margins: 20 - spacing: 15 + anchors.margins: Units.gridUnit + spacing: Units.gridUnit Rectangle { id: caption Layout.fillWidth: true - Layout.preferredHeight: captionContent.height + 30 - radius: 15 + Layout.preferredHeight: captionContent.height + 2 * Units.gridUnit + radius: Units.gridUnit color: "#eee4be" Item { id: captionContent anchors.centerIn: parent width: Math.max(mainCaption.width, subCaption.width) - height: subCaption.visible? mainCaption.height + subCaption.height + 5: mainCaption.height + height: subCaption.visible? mainCaption.height + subCaption.height + Units.smallSpacing: mainCaption.height Item { id: mainCaption anchors.top: parent.top anchors.horizontalCenter: parent.horizontalCenter - width: captionIcon.width + captionLabel.width + 7 + width: captionIcon.width + captionLabel.width + Units.largeSpacing height: Math.max(captionIcon.height, captionLabel.height) Icon { id: captionIcon anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter icon: internal.lessonPassed? "dialog-ok-apply": "dialog-cancel" width: height height: captionLabel.height } Label { id: captionLabel anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter text: internal.lessonPassed? i18n("Congratulations! You have passed the lesson."): i18n("You have not passed the lesson.") font.pointSize: 1.5 * Qt.font({'family': 'sansserif'}).pointSize color: "#000000" } } Label { id: subCaption anchors.bottom: parent.bottom anchors.horizontalCenter: parent.horizontalCenter visible: text !== "" text: { if (!internal.lessonPassed) return i18n("Repeat the lesson. Your skills will improve automatically over time.") if (internal.nextLessonUnlocked) return i18n("Next lesson unlocked") return "" } color: "#000000" } } } Rectangle { id: statsBox Layout.fillWidth: true Layout.preferredHeight: 140 radius: 15 color: "#cccccc" StatBox { anchors.centerIn: parent width: parent.width - 26 stats: screen.stats referenceStats: screen.referenceStats } } Item { id: contentSpacer - Layout.preferredHeight: 10 + Layout.preferredHeight: Units.gridUnit Layout.fillWidth: true } RowLayout { id: chartControls spacing: 5 Layout.fillWidth: true Label { id: showLabel Layout.alignment: Qt.AlignVCenter text: i18nc("Show a specific type of statistic", "Show") opacity: 0.7 } ComboBox { id: chartTypeComboBox Layout.alignment: Qt.AlignVCenter model: ListModel { Component.onCompleted: { append({"text": i18n("Progress"), "icon": "office-chart-area"}); append({"text": i18n("Errors"), "icon": "office-chart-bar"}); } } textRole: "text" currentIndex: 0 } Label { id: overLabel Layout.alignment: Qt.AlignVCenter text: i18nc("Show a statistic over one or more lessons", "Over") opacity: tabGroup.currentItem === learningProgressTab? 0.7: 0 Behavior on opacity { - NumberAnimation {duration: 150} + NumberAnimation {duration: Units.shortDuration} } } ComboBox { id: learningProgressFilterComboBox Layout.alignment: Qt.AlignVCenter model: ListModel { Component.onCompleted: { append({"text": i18n("All Lessons"), "icon": "view-filter"}); append({"text": i18n("This Lesson"), "icon": "view-filter"}); } } textRole: "text" currentIndex: 0 opacity: tabGroup.currentItem === learningProgressTab? 1: 0 Behavior on opacity { - NumberAnimation {duration: 150} + NumberAnimation {duration: Units.shortDuration} } } Item { Layout.fillWidth: true } Charts.LegendItem { id: accuracyLegend textColor: colorScheme.normalText Layout.alignment: Qt.AlignVCenter opacity: tabGroup.currentItem === learningProgressTab? 1: 0 Behavior on opacity { - NumberAnimation {duration: 150} + NumberAnimation {duration: Units.shortDuration} } } Charts.LegendItem { id: charactersPerMinuteLegend textColor: colorScheme.normalText Layout.alignment: Qt.AlignVCenter opacity: tabGroup.currentItem === learningProgressTab? 1: 0 Behavior on opacity { - NumberAnimation {duration: 150} + NumberAnimation {duration: Units.shortDuration} } } } Item { Layout.fillHeight: true Layout.fillWidth: true StackLayout { anchors.fill: parent id: tabGroup currentIndex: chartTypeComboBox.currentIndex property Item currentItem: currentIndex != -1? children[currentIndex]: null LearningProgressChart { id: learningProgressTab property string title: i18n("Progress") property string iconName: "office-chart-area" model: learningProgressModel backgroundColor: colorScheme.normalBackground textColor: colorScheme.normalText Component.onCompleted: { accuracyLegend.dimension = learningProgressTab.accuracy charactersPerMinuteLegend.dimension = learningProgressTab.charactersPerMinute } Component.onDestruction: { accuracyLegend.dimension = null charactersPerMinuteLegend.dimension = null } } Charts.BarChart{ id: errorsTab property string title: i18n("Errors") property string iconName: "office-chart-bar" model: errorsModel - pitch: 60 + pitch: 3 * Units.gridUnit textRole: 3 // Qt::ToolTipRole backgroundColor: colorScheme.normalBackground dimensions: [ Charts.Dimension { dataColumn: 0 color: "#ffb12d" maximumValue: Math.max(4, Math.ceil(errorsModel.maximumErrorCount / 4) * 4) label: i18n("Errors") } ] onElemEntered: { errorsTooltip.visualParent = elem; errorsTooltip.row = row errorsTooltip.open() } onElemExited: { errorsTooltip.close() } } } } } } } } diff --git a/src/qml/trainingscreen/TrainingScreen.qml b/src/qml/trainingscreen/TrainingScreen.qml index 97ce081..4a9c965 100644 --- a/src/qml/trainingscreen/TrainingScreen.qml +++ b/src/qml/trainingscreen/TrainingScreen.qml @@ -1,333 +1,333 @@ /* * Copyright 2018 Sebastian Gottfried * * 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) any later version. * * 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.9 import QtQuick.Layouts 1.3 import ktouch 1.0 import "../common" import "../keyboard" import "../meters" FocusScope { id: screen property KeyboardLayout keyboardLayout property Profile profile property Course course property Lesson lesson property alias stats: stats property alias referenceStats: referenceStats signal restartRequested() signal abortRequested() signal finished() property bool trainingStarted: false property bool trainingFinished: true property bool isActive: Qt.application.active function setLessonKeys() { if (!lesson) return; var chars = lesson.characters; var keyItems = keyboard.keyItems() var modifierItems = [] var usedModifiers = {} for (var i = 0; i < keyItems.length; i++) { var key = keyItems[i].key if (key.keyType() == "key") { keyItems[i].enabled = false; for (var j = 0; j < key.keyCharCount; j++) { var keyChar = key.keyChar(j) if (chars.indexOf(keyChar.value) != -1) { keyItems[i].enabled = true; if (keyChar.modifier !== "") { usedModifiers[keyChar.modifier] = true } } } } else { var type = keyItems[i].key.type if (type != SpecialKey.Return && type != SpecialKey.Backspace && type != SpecialKey.Space) { modifierItems.push(keyItems[i]) keyItems[i].enabled = false } else if (type == SpecialKey.Return && preferences.nextLineWithSpace) { keyItems[i].enabled = false } } } for (i = 0; i < modifierItems.length; i++) { var modifierItem = modifierItems[i] modifierItem.enabled = !!usedModifiers[modifierItem.key.modifierId] } } function reset() { toolbar.reset() trainingWidget.reset() screen.trainingStarted = false screen.trainingFinished = true profileDataAccess.loadReferenceTrainingStats(referenceStats, screen.profile, screen.course.id, screen.lesson.id) profileDataAccess.saveCourseProgress(lesson.id, profile, course.id, ProfileDataAccess.LastSelectedLesson) } function start() { screen.trainingFinished = false screen.trainingStarted = true } function forceActiveFocus() { trainingWidget.forceActiveFocus() } onLessonChanged: setLessonKeys() onIsActiveChanged: { if (!screen.isActive) { stats.stopTraining() } } KColorScheme { id: colorScheme colorGroup: KColorScheme.Active colorSet: KColorScheme.Complementary } TrainingStats { id: stats onTimeIsRunningChanged: { if (timeIsRunning) { screen.trainingStarted = false } } } TrainingStats { id: referenceStats } Shortcut { sequence: "Escape" enabled: screen.visible onActivated: { if (!menu.visible) { menu.open() } } } ColumnLayout { id: screenContent anchors.fill: parent spacing: 0 BorderImage { Layout.fillWidth: true - Layout.preferredHeight: 41 + Layout.preferredHeight: toolbar.implicitHeight + 1 border { top: 1 bottom: 1 } cache: false source: "qrc:///ktouch/images/trainingscreen-toolbar.png" horizontalTileMode: BorderImage.Repeat verticalTileMode: BorderImage.Repeat TrainingScreenToolbar { id: toolbar anchors.fill: parent trainingStarted: screen.trainingStarted trainingFinished: screen.trainingFinished stats: stats menu: menu } } BorderImage { id: header Layout.fillWidth: true - Layout.preferredHeight: visible? 130: 0 + Layout.preferredHeight: visible? statBox.height + 2 * Units.gridUnit: 0 visible: preferences.showStatistics border { top: 1 bottom: 1 } source: "qrc:///ktouch/images/trainingscreen-header.png" cache: false StatBox { + id: statBox anchors.centerIn: parent - width: parent.width - 60 + width: parent.width - 2 * Units.gridUnit stats: stats referenceStats: referenceStats } } BorderImage { id: body Layout.fillWidth: true Layout.fillHeight: true border { top: 1 bottom: 1 } source: "qrc:///ktouch/images/trainingscreen-viewport.png" cache: false TrainingWidget { id: trainingWidget anchors.fill: parent lesson: screen.lesson keyboardLayout: screen.keyboardLayout trainingStats: stats overlayContainer: trainingOverlayContainer onKeyPressed: keyboard.handleKeyPress(event) onKeyReleased: keyboard.handleKeyRelease(event) onNextCharChanged: keyboard.updateKeyHighlighting() onIsCorrectChanged: keyboard.updateKeyHighlighting() onFinished: { profileDataAccess.saveTrainingStats(stats, screen.profile, screen.course.id, screen.lesson.id) screen.finished(stats) screen.trainingFinished = true } } BorderImage { anchors.fill: parent border { top: 3 bottom: 3 } source: "qrc:///ktouch/images/trainingscreen-viewport-shadow.png" cache: false - } } BorderImage { id: footer visible: preferences.showKeyboard Layout.fillWidth: true Layout.preferredHeight: visible? screen.keyboardLayout.isValid? Math.round(Math.min((parent.height - toolbar.height - header.height) / 2, parent.width / keyboard.aspectRatio)): keyboardUnavailableNotice.height: 0 border { top: 1 bottom: 1 } source: "qrc:///ktouch/images/trainingscreen-footer.png" cache: false Keyboard { id: keyboard property variant highlightedKeys: [] function highlightKey(which) { for (var i = 0; i < highlightedKeys.length; i++) highlightedKeys[i].isHighlighted = false var keys = findKeyItems(which) var newHighlightedKeys = [] for (var index = 0; index < keys.length ; index++) { var key = keys[index] if (key) { key.isHighlighted = true newHighlightedKeys.push(key) if (typeof which == "string") { for (var i = 0; i < key.key.keyCharCount; i++) { var keyChar = key.key.keyChar(i) if (keyChar.value == which && keyChar.modifier != "") { var modifier = findModifierKeyItem(keyChar.modifier) if (modifier) { modifier.isHighlighted = true newHighlightedKeys.push(modifier) } break } } } } } highlightedKeys = newHighlightedKeys } function updateKeyHighlighting() { if (!visible) return; if (trainingWidget.isCorrect) { if (trainingWidget.nextChar !== "") { highlightKey(trainingWidget.nextChar) } else { highlightKey(preferences.nextLineWithSpace? Qt.Key_Space: Qt.Key_Return) } } else { highlightKey(Qt.Key_Backspace) } } keyboardLayout: screen.keyboardLayout anchors { fill: parent - leftMargin: 30 - rightMargin: 30 - topMargin: 10 - bottomMargin: 10 + leftMargin: Units.gridUnit + rightMargin: Units.gridUnit + topMargin: Units.largeSpacing + bottomMargin: Units.largeSpacing } onKeyboardUpdate: { setLessonKeys() highlightedKeys = [] updateKeyHighlighting() } } KeyboardUnavailableNotice { id: keyboardUnavailableNotice colorScheme: colorScheme visible: !screen.keyboardLayout.isValid width: parent.width } } } Item { id: trainingOverlayContainer anchors.fill: parent } TrainingScreenMenu { id: menu onClosed: trainingWidget.forceActiveFocus() onRestartRequested: screen.restartRequested() onAbortRequested: screen.abortRequested() } } diff --git a/src/qml/trainingscreen/TrainingScreenToolbar.qml b/src/qml/trainingscreen/TrainingScreenToolbar.qml index 757e004..3e29641 100644 --- a/src/qml/trainingscreen/TrainingScreenToolbar.qml +++ b/src/qml/trainingscreen/TrainingScreenToolbar.qml @@ -1,80 +1,80 @@ /* * Copyright 2018 Sebastian Gottfried * * 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) any later version. * * 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.9 import QtQuick.Layouts 1.3 import ktouch 1.0 import "../common" ToolBar { id: root property TrainingStats stats property bool trainingStarted: false property bool trainingFinished: true property PopupDialog menu dimFactor: 1.8 function setMessage() { if (!stats.timeIsRunning && !trainingFinished) { var msg = trainingStarted? i18n("Training session started. Time begins running with the first keystroke."): i18n("Training session interrupted. Time begins running again with next keystroke.") messageBox.showMessage(msg, "media-playback-pause") } else { messageBox.clearMessage() } } function reset() { messageBox.clearMessageImmediately() } onTrainingStartedChanged: setMessage() onTrainingFinishedChanged: setMessage() Connections { target: stats onTimeIsRunningChanged: setMessage() } Row { anchors { verticalCenter: parent.verticalCenter left: parent.left right: parent.right - leftMargin: 30 - rightMargin: 30 + leftMargin: Units.gridUnit + rightMargin: Units.gridUnit } - spacing: 5 + spacing: Units.largeSpacing IconToolButton { id: menuButton iconName: "go-home" onClicked: root.menu.open() colorScheme: root.colorScheme } MessageBox { id: messageBox } } } diff --git a/src/qml/trainingscreen/TrainingWidget.qml b/src/qml/trainingscreen/TrainingWidget.qml index 4cce2b8..dae0e11 100644 --- a/src/qml/trainingscreen/TrainingWidget.qml +++ b/src/qml/trainingscreen/TrainingWidget.qml @@ -1,244 +1,244 @@ /* * Copyright 2018 Sebastian Gottfried * * 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) any later version. * * 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.9 import ktouch 1.0 import "../keyboard" import "../common" FocusScope { id: trainingWidget property Lesson lesson property KeyboardLayout keyboardLayout property TrainingStats trainingStats property Item overlayContainer property alias nextChar: trainingLine.nextCharacter property alias isCorrect: trainingLine.isCorrect property int position: -1 signal finished signal keyPressed(variant event) signal keyReleased(variant event) function reset() { stats.reset() lessonPainter.reset() sheetFlick.scrollToCurrentLine() trainingLine.active = true } function forceActiveFocus() { trainingLine.forceActiveFocus() } Timer { id: stopTimer interval: 5000 onTriggered: { stats.stopTraining() } } MouseArea { anchors.fill: parent onClicked: trainingLine.forceActiveFocus() } ScrollView { anchors.fill: parent Flickable { id: sheetFlick anchors.fill: parent contentWidth: parent.width - contentHeight: sheet.height + 60 + contentHeight: sheet.height + 2 * Units.gridUnit clip: true flickableDirection: Flickable.VerticalFlick function currentLineY() { var cursorRect = lessonPainter.cursorRectangle var y = cursorRect.y + sheet.y + (cursorRect.height / 2) return Math.max(Math.min((y - (height / 2)), contentHeight - height), 0) } function scrollToCurrentLine() { scrollAnimation.to = currentLineY() scrollAnimation.start() } onHeightChanged: { contentY = currentLineY() } NumberAnimation { target: sheetFlick id: scrollAnimation - duration: 150 + duration: Units.shortDuration property: "contentY" } Rectangle { id: sheet color: "#fff" - x: 30 - y: 30 - width: trainingWidget.width - 60 + x: Units.gridUnit + y: Units.gridUnit + width: trainingWidget.width - 2 * Units.gridUnit height: lessonPainter.height border { - width: 1 + width: Units.borderWidth color: "#000" } LessonPainter { id: lessonPainter anchors.centerIn: sheet lesson: trainingWidget.lesson maximumWidth: parent.width trainingLineCore: trainingLine onDone: { trainingLine.active = false trainingWidget.finished(); stats.stopTraining(); } TrainingLineCore { id: trainingLine anchors.fill: parent focus: true trainingStats: stats cursorItem: cursor onActiveFocusChanged: { if (!trainingLine.activeFocus) { trainingStats.stopTraining() } } Keys.onPressed: { if (!trainingLine.active) return cursorAnimation.restart() trainingStats.startTraining() stopTimer.restart() if (!event.isAutoRepeat) { trainingWidget.keyPressed(event) } } Keys.onReleased: { if (!trainingLine.active) return if (!event.isAutoRepeat) { trainingWidget.keyReleased(event) } } KeyNavigation.backtab: trainingLine KeyNavigation.tab: trainingLine } Rectangle { id: cursor color: "#000" x: Math.floor(lessonPainter.cursorRectangle.x) y: lessonPainter.cursorRectangle.y width: lessonPainter.cursorRectangle.width height: lessonPainter.cursorRectangle.height onYChanged: sheetFlick.scrollToCurrentLine() SequentialAnimation { id: cursorAnimation running: trainingLine.active && trainingLine.activeFocus && Qt.application.active loops: Animation.Infinite PropertyAction { target: cursor property: "opacity" value: 1 } PauseAnimation { duration: 500 } PropertyAction { target: cursor property: "opacity" value: 0 } PauseAnimation { duration: 500 } } } } } } } KeyItem { id: hintKey parent: trainingWidget.overlayContainer anchors.horizontalCenter: parent.horizontalCenter y: Math.max(height, Math.min(parent.height - 2 * height, sheetFlick.mapToItem(parent, 0, cursor.y + 3 * cursor.height - sheetFlick.contentY).y)) property Key defaultKey: Key {} property KeyboardLayout defaultKeyboardLayout: KeyboardLayout {} key: { var specialKeyType switch (trainingLine.hintKey) { case Qt.Key_Return: specialKeyType = SpecialKey.Return break case Qt.Key_Backspace: specialKeyType = SpecialKey.Backspace break case Qt.Key_Space: specialKeyType = SpecialKey.Space break default: specialKeyType = -1 } for (var i = 0; i < keyboardLayout.keyCount; i++) { var key = keyboardLayout.key(i) if (key.keyType() === "specialKey" && key.type === specialKeyType) { return key; } } return defaultKey } opacity: trainingLine.hintKey !== -1? 1: 0 isHighlighted: opacity == 1 keyboardLayout: screen.keyboardLayout || defaultKeyboardLayout Behavior on opacity { - NumberAnimation { duration: 150 } + NumberAnimation { duration: Units.shortDuration } } } }