Commit 106d3bbe authored by Sebastian Gottfried's avatar Sebastian Gottfried
Browse files

Merge Branch 'homescreen-ng'

This replaces the home screen to solve KTouch's most egrigous problem:
content discovery. Previously, KTouch displayed only the courses for the
the currently active keyboard layout. If KTouch has no course for this
layout, the windows was basically empty showing only the option to
create a new custom lesson. Being forced to set up first the correct
keyboad layout for your computer has proven to be unintuitive. The
new home screen shows all available training content at a glance.

Improvements in more detail:

 * There is a new sidebar allowing the user to select from all available
   keyboard layouts with their associated courses. Users still have to
   set up the correct layout matching the selected course, though, and
   they are told how to do so with an passive notification. (BUG 395264,
   BUG 385425, BUG 331368, BUG 398773, BUG 391968, BUG 364011)

 * Training statistics are now available from the home screen for each
   lesson. Previously they have been only shown on the score screen
   after completing a lesson. (BUG 401359, BUG 317174)

 * The lesson selector has been redesign to show previews of each lesson
   as selectable items in a grid view.

  * Tab focus works now as expected and allows training without using the
    mouse at all. (BUG 316058)

  * Support for HiDPI rendering has been added.

  * KTouch now uses Qt Quick Controls 2 instead of version 1.

BUG: 385425
BUG: 401359
BUG: 331368
BUG: 398773
BUG: 391968
BUG: 364011
BUG: 316058
BUG: 395264
BUG: 317174
FIXED-IN: 19.08.0
Differential Revision: https://phabricator.kde.org/D20953
parents ae2b5d63 8b465992
......@@ -4,3 +4,7 @@
/*.txt.user
/*.kdev4
# Default build and install locations
/build
/install
Sebastian Gottfried <sebastiangottfried@web.de
Sebastian Gottfried <sebastian.gottfried@posteo.de>
Håvard Frøiland <haavard@solveien.net>
Andreas Nicolai <Andreas.Nicolai@gmx.net>
......@@ -23,14 +23,13 @@ include(ECMOptionalAddSubdirectory)
include(ECMSetupVersion)
include(FeatureSummary)
find_package(Qt5 5.5 REQUIRED COMPONENTS
find_package(Qt5 5.9 REQUIRED COMPONENTS
Gui
Qml
Quick
QuickWidgets
Script
QuickControls2
Sql
Test
Widgets
X11Extras
Xml
......@@ -53,6 +52,7 @@ find_package(KF5 REQUIRED COMPONENTS
WidgetsAddons
WindowSystem
XmlGui
IconThemes
)
option(COMPILE_QML "Precompile QML code" ON)
......@@ -65,7 +65,6 @@ ecm_optional_add_subdirectory(data)
ecm_optional_add_subdirectory(doc)
ecm_optional_add_subdirectory(src)
# ecm_optional_add_subdirectory(sounds)
ecm_optional_add_subdirectory(images)
ecm_optional_add_subdirectory(icons)
# files to install in the ktouch project root directory
......
KTouch
======
# KTouch
[![Build Status](https://build.kde.org/view/Applications/job/Applications%20ktouch%20kf5-qt5%20SUSEQt5.9/badge/icon)](https://build.kde.org/view/Applications/job/Applications%20ktouch%20kf5-qt5%20SUSEQt5.9/)
......@@ -10,44 +9,93 @@ training levels where additional keys are introduced. Because you no
longer need to search for the keys on the keyboard you will be able
to type quickly and accurately.
Building
========
# Development Setup
## Building
KTouch has the following build dependencies:
* CMake >= 2.8.12
* CMake Extra Modules >= 1.0.0
* Qt >= 5.5
* KDE Frameworks:
* Config
* ConfigWidgets
* CoreAddons
* Declarative
* DocTools
* I18n
* ItemViews
* KCMUtils
* KIO
* NewStuff
* TextEditor
* WidgetsAddons
* WindowSystem
* XmlGui
* libxkbfile (optional, for keyboard layout auto-detection)
* libxcb (optional, for keyboard layout auto-detection)
If the build requirements are met, KTouch can be built and installed with:
* [CMake][cmake] ≥ 3.0.0
* [Extra CMake Modules][ecm] ≥ 1.0.0
* [Qt][qt] ≥ 5.9 with the following modules
* Qt GUI
* Qt QML
* Qt Quick
* Qt Quick Widgets
* Qt Quick Controls 2
* Qt SQL
* Qt Test
* Qt X11 Extras
* Qt XML
* Qt XML Patterns
* [KDE Frameworks 5][kf5] with:
* Completion
* Config
* ConfigWidgets
* CoreAddons
* Declarative
* DocTools
* I18n
* ItemViews
* KCMUtils
* KIO
* TextWidgets
* WidgetsAddons
* WindowSystem
* XmlGui
* IconThemes
* [libxkbfile][libxkbfile] (optional, for keyboard layout auto-detection)
* [libxcb][libxcb] (optional, for keyboard layout auto-detection)
[cmake]: https://cmake.org/
[ecm]: https://cgit.kde.org/extra-cmake-modules.git/
[qt]: https://www.qt.io/
[kf5]: https://api.kde.org/frameworks/
[libxkbfile]: https://cgit.freedesktop.org/xorg/lib/libxkbfile
[libxcb]: https://xcb.freedesktop.org/
Collecting and installing all dependencies by hand can be a challenge.
Far easier is to use the dependency resolution of your package
manager. Usually they can be instructed to install all dependencies
automatically by using the information of the packaged version of
KTouch:
apt-get build-dep ktouch # Ubuntu, Debian, ...
zypper source-install --build-deps-only # openSUSE
If the build requirements are met, execute the following commands in
the root directory of your working copy to built and install KTouch:
mkdir build
cd build/
cmake ..
cd build
cmake .. -DCMAKE_INSTALL_PREFIX=../install
make
sudo make install
make install
These commands install KTouch to the directory `install` in the root of
your working copy. You can install KTouch to any directory of your
choice by passing a different path to `-DCMAKE_INSTALL_PREFIX`.
## Running
At runtime KTouch needs the following additional software packages to be
installed:
* [Breeze icon set][breeze]
* [kqtquickcharts][kqtquickcharts] ≥ 16.12
* [plasma-desktop][plasma-desktop] (optional, for keyboard layout
configuration inside the application)
[breeze]: https://phabricator.kde.org/source/breeze-icons/
[kqtquickcharts]: https://phabricator.kde.org/source/kqtquickcharts/
[plasma-desktop]: https://phabricator.kde.org/source/plasma-desktop/
KTouch needs the following environment variables to be set in order
to find all its installed resources:
Runtime dependencies
====================
export KDEDIRS=<full-path-to-install-dir>:$KDEDIRS
export XDG_DATA_DIRS=<full-path-to-install-dir>/share:$XDG_DATA_DIRS
At runtime KTouch needs the following software packages to be present:
* kqtquickcharts >= 16.12 (git://anongit.kde.org/kqtquickcharts)
* plasma-desktop (optional, for keyboard layout configuration
from within the application)
After this, KTouch can finally be launched with:
<path-to-install-dir>/bin/ktouch
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright 2017 Sebastian Gottfried <sebastian.gottfried@posteo.de>
#
# 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 <http://www.gnu.org/licenses/>.
#
import argparse
import re
from pathlib import Path
from collections import namedtuple
Import = namedtuple('Import', ['name', 'version', 'alias'])
ALLOWED_IMPORTS = {
'common': [
Import('QtQuick.Controls', '2.2', None),
Import('QtQuick.Controls', '2.2', 'Controls'),
Import('org.kde.kquickcontrolsaddons', '2.0', 'Addons'),
],
'*': [
Import('QtQuick', '2.9', alias=None),
Import('QtQuick.Layouts', '1.3', alias=None),
Import('ktouch', '1.0', alias=None),
Import('QtGraphicalEffects', '1.0', alias=None),
Import('org.kde.charts', '0.1', alias='Charts'),
],
}
IMPORT_RE = re.compile(r'^import ([\w\.]+) ([\d\.]+)( as ([\w\.]+))?$')
LOCAL_IMPORT = re.compile('^import ("|\')[\w\./]+("|\')')
def dir_arg(path_str):
path = Path(path_str)
if not path.exists():
msg = "'{}' doesn't exist".format(path)
raise argparse.ArgumentTypeError(msg)
if not path.is_dir():
msg = "'{}' has to be a directory".format(path)
raise argparse.ArgumentTypeError(msg)
return path
def check_imports(file, module, local_path):
for line in file:
line = line.strip()
if not line.startswith('import'):
continue
if LOCAL_IMPORT.match(line) is not None:
continue
try:
check_package_import(line, module, local_path)
except ValueError:
print(f"{local_path}: unrecognized import statement: '{line}'")
def check_package_import(line, module, local_path):
matches = IMPORT_RE.match(line)
if matches is None:
raise ValueError('unrecognized import')
current_import = Import(*matches.group(1, 2, 4))
for allowed_module, allowed_imports in ALLOWED_IMPORTS.items():
if allowed_module == '*' or allowed_module == module:
for allowed_import in allowed_imports:
if current_import == allowed_import:
return
print(f"{local_path}: import not allowed here: '{line}'")
if __name__ == '__main__':
parser = argparse.ArgumentParser(
description='check QML imports for KTouch'
)
parser.add_argument('path',
type=dir_arg,
metavar="PATH",
default=""
)
args = parser.parse_args()
for qml_file_path in args.path.glob('**/*.qml'):
local_path = qml_file_path.relative_to(args.path)
if len(local_path.parts) > 1:
module = local_path.parts[0]
else:
module = None
check_imports(qml_file_path.open(mode='r'), module, local_path)
set(image_files
accuracymeter-background.png
accuracymeter-scale.png
accuracymeter-hand.png
balloontip.svgz
charactersperminutemeter-background.png
charactersperminutemeter-scale.png
charactersperminutemeter-hand.png
elapsedtimemeter-background.png
elapsedtimemeter-minute-hand.png
elapsedtimemeter-second-hand.png
meterbox-left.png
meterbox-right.png
trainingscreen-footer.png
trainingscreen-header.png
trainingscreen-toolbar.png
trainingscreen-viewport.png
trainingscreen-viewport-shadow.png
)
install(FILES ${image_files} DESTINATION ${DATA_INSTALL_DIR}/ktouch/images)
......@@ -31,7 +31,9 @@ set(ktouch_SRCS
bindings/utils.cpp
bindings/stringformatter.cpp
declarativeitems/griditem.cpp
declarativeitems/kcolorschemeproxy.cpp
declarativeitems/lessonpainter.cpp
declarativeitems/lessontexthighlighteritem.cpp
declarativeitems/preferencesproxy.cpp
declarativeitems/scalebackgrounditem.cpp
declarativeitems/traininglinecore.cpp
......@@ -83,6 +85,8 @@ set(ktouch_SRCS
ktouchcontext.cpp
)
qt5_add_resources(ktouch_imgs_SRCS images/images.qrc)
if (Qt5QuickCompiler_FOUND AND COMPILE_QML)
qtquick_compiler_add_resources(ktouch_qml_SRCS qml/qml.qrc)
else ()
......@@ -117,7 +121,7 @@ endif (KTOUCH_BUILD_WITH_X11)
kconfig_add_kcfg_files(ktouch_SRCS preferences.kcfgc)
add_executable(ktouch ${ktouch_SRCS} ${ktouch_qml_SRCS})
add_executable(ktouch ${ktouch_SRCS} ${ktouch_imgs_SRCS} ${ktouch_qml_SRCS})
set(ktouch_X11_DEPS "")
......@@ -132,7 +136,7 @@ target_link_libraries(ktouch
Qt5::Qml
Qt5::Quick
Qt5::QuickWidgets
Qt5::Script
Qt5::QuickControls2
Qt5::Sql
Qt5::XmlPatterns
Qt5::X11Extras
......@@ -146,6 +150,7 @@ target_link_libraries(ktouch
KF5::TextWidgets
KF5::WindowSystem
KF5::CoreAddons
KF5::IconThemes
${ktouch_X11_DEPS}
)
......
......@@ -18,9 +18,11 @@
#include "application.h"
#include <QDir>
#include <QIcon>
#include <QFile>
#include <QQmlEngine>
#include <QQmlContext>
#include <QQuickStyle>
#include <QStandardPaths>
#include <Kdelibs4ConfigMigrator>
......@@ -30,7 +32,9 @@
#include "bindings/utils.h"
#include "bindings/stringformatter.h"
#include "declarativeitems/griditem.h"
#include "declarativeitems/kcolorschemeproxy.h"
#include "declarativeitems/lessonpainter.h"
#include "declarativeitems/lessontexthighlighteritem.h"
#include "declarativeitems/preferencesproxy.h"
#include "declarativeitems/scalebackgrounditem.h"
#include "declarativeitems/traininglinecore.h"
......@@ -59,6 +63,9 @@ Application::Application(int& argc, char** argv, int flags):
registerQmlTypes();
migrateKde4Files();
QIcon::setThemeName("breeze");
QQuickStyle::setStyle("Default");
DataAccess dataAccess;
dataAccess.loadDataIndex(m_dataIndex);
}
......@@ -81,7 +88,8 @@ void Application::setupDeclarativeBindings(QQmlEngine* qmlEngine)
{
KDeclarative::KDeclarative kDeclarative;
kDeclarative.setDeclarativeEngine(qmlEngine);
kDeclarative.setupBindings();
kDeclarative.setupContext();
kDeclarative.setupEngine(qmlEngine);
Application* app = static_cast<Application*>(Application::instance());
foreach (const QString& path, app->m_qmlImportPaths)
......@@ -124,10 +132,12 @@ void Application::registerQmlTypes()
qmlRegisterType<LearningProgressModel>("ktouch", 1, 0, "LearningProgressModel");
qmlRegisterType<ErrorsModel>("ktouch", 1, 0, "ErrorsModel");
qmlRegisterType<GridItem>("ktouch", 1, 0 , "Grid");
qmlRegisterType<GridItem>("ktouch", 1, 0 , "LineGrid");
qmlRegisterType<ScaleBackgroundItem>("ktouch", 1, 0, "ScaleBackgroundItem");
qmlRegisterType<LessonPainter>("ktouch", 1, 0, "LessonPainter");
qmlRegisterType<LessonTextHighlighterItem>("ktouch", 1, 0, "LessonTextHighlighter");
qmlRegisterType<TrainingLineCore>("ktouch", 1, 0, "TrainingLineCore");
qmlRegisterType<KColorSchemeProxy>("ktouch", 1, 0, "KColorScheme");
}
void Application::migrateKde4Files()
......
......@@ -17,6 +17,7 @@
#include "utils.h"
#include <QColor>
#include <QTime>
#include <QUuid>
#include <QtDebug>
......@@ -27,20 +28,6 @@ Utils::Utils(QObject* parent):
{
}
QUrl Utils::findImage(const QString &name)
{
const QString relPath = QStringLiteral("images/") + name;
const QString path = QStandardPaths::locate(QStandardPaths::DataLocation, relPath);
if (path.isNull())
{
qWarning() << "can't find image resource:" << name;
return QUrl();
}
return QUrl::fromLocalFile(path);
}
int Utils::getMinutesOfQTime(const QTime& time)
{
if (!time.isValid())
......@@ -67,3 +54,10 @@ QString Utils::uuid()
{
return QUuid::createUuid().toString();
}
QColor Utils::alpha(const QColor& color, float alpha)
{
auto result = QColor(color);
result.setAlphaF(alpha * result.alphaF());
return result;
}
......@@ -21,16 +21,17 @@
#include <QObject>
#include <QTime>
#include <QUrl>
#include <QColor>
class Utils : public QObject
{
Q_OBJECT
public:
explicit Utils(QObject* parent = 0);
Q_INVOKABLE QUrl findImage(const QString &name);
explicit Utils(QObject* parent = nullptr);
Q_INVOKABLE int getMinutesOfQTime(const QTime& time);
Q_INVOKABLE int getSecondsOfQTime(const QTime& time);
Q_INVOKABLE QString uuid();
Q_INVOKABLE QColor alpha(const QColor& color, float alpha);
};
......
......@@ -25,7 +25,7 @@
Course::Course(QObject *parent) :
CourseBase(parent),
m_associatedDataIndexCourse(0),
m_doSyncLessonCharacters(true),
m_kind(Course::SequentialCourse),
m_signalMapper(new QSignalMapper(this))
{
connect(m_signalMapper, SIGNAL(mapped(int)), SLOT(updateLessonCharacters(int)));
......@@ -45,17 +45,17 @@ void Course::setAssociatedDataIndexCourse(DataIndexCourse* dataIndexCourse)
}
}
bool Course::doSyncLessonCharacters() const
Course::Kind Course::kind() const
{
return m_doSyncLessonCharacters;
return m_kind;
}
void Course::setDoSyncLessonCharacters(bool doSync)
void Course::setKind(Kind kind)
{
if (doSync != m_doSyncLessonCharacters)
if (kind != m_kind)
{
m_doSyncLessonCharacters = doSync;
emit doSyncLessonCharactersChanged();
m_kind = kind;
emit kindChanged();
}
}
......@@ -148,6 +148,11 @@ void Course::removeLesson(int index)
emit lessonsRemoved();
}
int Course::indexOfLesson(Lesson* lesson)
{
return m_lessons.indexOf(lesson);
}
void Course::clearLessons()
{
if (m_lessons.count() == 0)
......@@ -167,7 +172,7 @@ void Course::copyFrom(Course* source)
setTitle(source->title());
setDescription(source->description());
setKeyboardLayoutName(source->keyboardLayoutName());
setDoSyncLessonCharacters(source->doSyncLessonCharacters());
setKind(source->kind());
clearLessons();
for (int i = 0; i < source->lessonCount(); i++)
{
......@@ -180,8 +185,10 @@ void Course::copyFrom(Course* source)
void Course::updateLessonCharacters(int firstIndex)
{
if (!m_doSyncLessonCharacters)
if (m_kind == Course::LessonCollection)
{
return;
}
QString characters = firstIndex > 0? lesson(firstIndex - 1)->characters(): QLatin1String("");
......
......@@ -32,15 +32,21 @@ class Course : public CourseBase
Q_OBJECT
Q_PROPERTY(DataIndexCourse* associatedDataIndexCourse READ associatedDataIndexCourse WRITE setAssociatedDataIndexCourse NOTIFY associatedDataIndexCourseChanged)
Q_PROPERTY(int lessonCount READ lessonCount NOTIFY lessonCountChanged)
Q_PROPERTY(bool doSyncLessonCharacters READ doSyncLessonCharacters WRITE setDoSyncLessonCharacters NOTIFY doSyncLessonCharactersChanged)
Q_PROPERTY(Kind kind READ kind WRITE setKind NOTIFY kindChanged)
public:
enum Kind {
SequentialCourse,
LessonCollection
};
Q_ENUM(Kind)
explicit Course(QObject *parent = 0);
DataIndexCourse* associatedDataIndexCourse() const;
void setAssociatedDataIndexCourse(DataIndexCourse* dataIndexCourse);
int lessonCount() const;
bool doSyncLessonCharacters() const;
void setDoSyncLessonCharacters(bool doSync);
Kind kind() const;
void setKind(Kind kind);
void setId(const QString& id);
void setTitle(const QString& title);
void setDescription(const QString& description);
......@@ -49,13 +55,14 @@ public:
Q_INVOKABLE void addLesson(Lesson* lesson);
Q_INVOKABLE void insertLesson(int index, Lesson* lesson);
Q_INVOKABLE void removeLesson(int index);
Q_INVOKABLE int indexOfLesson(Lesson* lesson);
Q_INVOKABLE void clearLessons();
Q_INVOKABLE void copyFrom(Course* source);
signals:
void associatedDataIndexCourseChanged();
void lessonCountChanged();
void doSyncLessonCharactersChanged();
void kindChanged();
void lessonAboutToBeAdded(Lesson* lesson, int index);
void lessonAdded();
void lessonsAboutToBeRemoved(int first, int last);
......@@ -67,7 +74,7 @@ private slots:
private:
Q_DISABLE_COPY(Course)
DataIndexCourse* m_associatedDataIndexCourse;
bool m_doSyncLessonCharacters;
Kind m_kind;
QList<Lesson*> m_lessons;
QSignalMapper* m_signalMapper;
};
......
......@@ -61,6 +61,11 @@ void Key::setHasHapticMarker(bool hasHapticMarker)
}
}
const QList<KeyChar*>& Key::keyChars() const
{
return m_keyChars;
}
int Key::keyCharCount() const
{
return m_keyChars.count();
......
......@@ -38,6 +38,7 @@ public:
void setFingerIndex(int finger);
bool hasHapticMarker() const;
void setHasHapticMarker(bool hasHapticMarker);
const QList<KeyChar*>& keyChars() const;
int keyCharCount() const;
Q_INVOKABLE KeyChar* keyChar(int index) const;
Q_INVOKABLE void addKeyChar(KeyChar* keyChar);
......
......@@ -29,6 +29,7 @@
#include "abstractkey.h"
#include "key.h"
#include "keychar.h"
#include "specialkey.h"
#include "dataindex.h"
......@@ -162,6 +163,23 @@ void KeyboardLayout::copyFrom(KeyboardLayout* source)
setIsValid(true);
}
QString KeyboardLayout::allCharacters() const
{
QString result;
for (const auto abstractKey: m_keys)
{
Key* const key = qobject_cast<Key*>(abstractKey);
if (key)
{
for (const auto keyChar : key->keyChars())
{
result.append(keyChar->value());
}
}
}
return result;
}
AbstractKey* KeyboardLayout::key(int index) const