SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "calibrationtool.h"
#include "logging.h"
#include <KConfigGroup>
#include <KSharedConfig>
#include <canberra.h>
CalibrationTool::CalibrationTool()
{
m_resetTimer.setInterval(1000);
connect(&m_resetTimer, &QTimer::timeout, this, [this] {
m_resetCountdown -= 1;
Q_EMIT resetSecondsLeftChanged();
if (m_resetCountdown > 0) {
m_resetTimer.start();
} else {
reset();
m_resetTimer.stop();
Q_EMIT resetFromSaved();
}
});
}
CalibrationTool::~CalibrationTool()
{
ca_context_destroy(m_canberraContext);
}
void CalibrationTool::setWidth(const float width)
{
if (m_width != width) {
m_width = width;
Q_EMIT widthChanged();
}
}
float CalibrationTool::width() const
{
return m_width;
}
void CalibrationTool::setHeight(const float height)
{
if (m_height != height) {
m_height = height;
Q_EMIT heightChanged();
}
}
float CalibrationTool::height()
{
return m_height;
}
CalibrationTool::State CalibrationTool::state() const
{
return m_state;
}
int CalibrationTool::currentTarget() const
{
return m_calibratedTargets;
}
int CalibrationTool::resetSecondsLeft() const
{
return m_resetCountdown;
}
QString CalibrationTool::lastCalibrationSummary() const
{
return m_lastCalibrationSummary;
}
QMatrix4x4 CalibrationTool::matrixWithCenterCorrection(const QMatrix4x4 &matrix, const double residualX, const double residualY) const
{
Q_ASSERT(m_width > 0.0f);
Q_ASSERT(m_height > 0.0f);
QMatrix4x4 corrected = matrix;
corrected(0, 2) -= residualX / m_width;
corrected(1, 2) -= residualY / m_height;
return corrected;
}
void CalibrationTool::updateCalibrationSummary(const QMatrix4x4 &matrix)
{
m_lastCalibrationSummary = QStringLiteral("a=%1 b=%2 c=%3 d=%4 e=%5 f=%6")
.arg(matrix(0, 0), 0, 'f', 6)
.arg(matrix(0, 1), 0, 'f', 6)
.arg(matrix(0, 2), 0, 'f', 6)
.arg(matrix(1, 0), 0, 'f', 6)
.arg(matrix(1, 1), 0, 'f', 6)
.arg(matrix(1, 2), 0, 'f', 6);
Q_EMIT lastCalibrationSummaryChanged();
}
void CalibrationTool::calibrate(const double touchX, const double touchY, const double screenX, const double screenY)
{
Q_ASSERT(m_calibratedTargets >= 0 && m_calibratedTargets < 4);
if (m_calibratedTargets < 0 || m_calibratedTargets >= 4) {
return;
}
Q_ASSERT(m_state != State::Testing);
if (m_state == State::Testing) {
return;
}
if (m_state == State::Calibrating) {
m_screenPoints[m_calibratedTargets] = {screenX, screenY};
m_touchPoints[m_calibratedTargets] = {touchX, touchY};
}
if (m_state == State::Confirming) {
m_resetCountdown = actionResetSeconds;
m_resetTimer.start();
Q_EMIT resetSecondsLeftChanged();
}
playSound(QStringLiteral("completion-partial"));
m_calibratedTargets++;
Q_EMIT currentTargetChanged();
checkIfFinished();
}
void CalibrationTool::setCalibrationMatrix(KWinDevices::InputDevice *device, const QMatrix4x4 &matrix)
{
Q_ASSERT(device);
device->setCalibrationMatrix(device->calibrationMatrix() * matrix);
}
void CalibrationTool::restoreDefaults(KWinDevices::InputDevice *device)
{
Q_ASSERT(device);
device->setCalibrationMatrix(device->defaultCalibrationMatrix());
playSound(QStringLiteral("dialog-information"));
}
void CalibrationTool::reset()
{
m_calibratedTargets = 0;
Q_EMIT currentTargetChanged();
m_state = State::Calibrating;
Q_EMIT stateChanged();
}
void CalibrationTool::applyCenterCorrection(KWinDevices::InputDevice *device, const double residualX, const double residualY)
{
Q_ASSERT(device);
Q_ASSERT(m_width > 0.0f);
Q_ASSERT(m_height > 0.0f);
if (!device || m_width <= 0.0f || m_height <= 0.0f) {
return;
}
const QMatrix4x4 matrix = matrixWithCenterCorrection(device->calibrationMatrix(), residualX, residualY);
device->setCalibrationMatrix(matrix);
updateCalibrationSummary(matrix);
}
void ca_finish_callback(ca_context *c, uint32_t id, int error_code, void *userdata)
{
Q_UNUSED(c);
Q_UNUSED(id);
Q_UNUSED(error_code);
Q_UNUSED(userdata);
}
QMatrix3x3 invert(const QMatrix3x3 &m)
{
* from https://stackoverflow.com/questions/983999/simple-3x3-matrix-inverse-code-c
* with some simplification
*/
const float m4857 = m.data()[4] * m.data()[8] - m.data()[5] * m.data()[7];
const float m3746 = m.data()[3] * m.data()[7] - m.data()[4] * m.data()[6];
const float m5638 = m.data()[5] * m.data()[6] - m.data()[3] * m.data()[8];
const float det = m.data()[0] * (m4857) + m.data()[1] * (m5638) + m.data()[2] * (m3746);
const float invdet = 1 / det;
QMatrix3x3 minv;
minv.data()[0] = (m4857)*invdet;
minv.data()[1] = (m.data()[2] * m.data()[7] - m.data()[1] * m.data()[8]) * invdet;
minv.data()[2] = (m.data()[1] * m.data()[5] - m.data()[2] * m.data()[4]) * invdet;
minv.data()[3] = (m5638)*invdet;
minv.data()[4] = (m.data()[0] * m.data()[8] - m.data()[2] * m.data()[6]) * invdet;
minv.data()[5] = (m.data()[2] * m.data()[3] - m.data()[0] * m.data()[5]) * invdet;
minv.data()[6] = (m3746)*invdet;
minv.data()[7] = (m.data()[1] * m.data()[6] - m.data()[0] * m.data()[7]) * invdet;
minv.data()[8] = (m.data()[0] * m.data()[4] - m.data()[1] * m.data()[3]) * invdet;
return minv;
}
void CalibrationTool::checkIfFinished()
{
if (m_state == State::Calibrating) {
if (m_calibratedTargets >= 4) {
Q_ASSERT(m_width > 0.0f);
Q_ASSERT(m_height > 0.0f);
enum {
UpperLeft = 0,
UpperRight = 1,
LowerLeft = 2,
LowerRight = 3,
};
const std::array permutations{
std::array{UpperLeft, UpperRight, LowerLeft},
std::array{LowerRight, UpperRight, LowerLeft},
std::array{LowerRight, UpperLeft, LowerLeft},
std::array{LowerRight, UpperLeft, UpperRight},
};
QMatrix3x3 sum;
sum.fill(0.0f);
for (int i = 0; i < 4; i++) {
const QPointF screenA = m_screenPoints[permutations[i][0]];
const QPointF screenB = m_screenPoints[permutations[i][1]];
const QPointF screenC = m_screenPoints[permutations[i][2]];
const std::array screenMatrix
{
static_cast<float>(screenA.x()), static_cast<float>(screenB.x()), static_cast<float>(screenC.x()),
static_cast<float>(screenA.y()), static_cast<float>(screenB.y()), static_cast<float>(screenC.y()),
1.0f, 1.0f, 1.0f,
};
const QPointF touchA = m_touchPoints[permutations[i][0]];
const QPointF touchB = m_touchPoints[permutations[i][1]];
const QPointF touchC = m_touchPoints[permutations[i][2]];
const std::array touchMatrix{
static_cast<float>(touchA.x()), static_cast<float>(touchB.x()), static_cast<float>(touchC.x()),
static_cast<float>(touchA.y()), static_cast<float>(touchB.y()), static_cast<float>(touchC.y()),
1.0f, 1.0f, 1.0f,
};
sum += QMatrix3x3(screenMatrix.data()) * invert(QMatrix3x3(touchMatrix.data()));
}
QMatrix3x3 average = sum / 4.0f;
average(0, 1) *= m_height / m_width;
average(0, 2) *= 1.0f / m_width;
average(1, 0) *= m_width / m_height;
average(1, 2) *= 1.0f / m_height;
average(2, 0) = 0.0f;
average(2, 1) = 0.0f;
average(2, 2) = 1.0f;
updateCalibrationSummary(QMatrix4x4(average));
m_state = State::Confirming;
Q_EMIT stateChanged();
Q_EMIT calibrationCreated(QMatrix4x4(average));
m_calibratedTargets = 0;
Q_EMIT currentTargetChanged();
m_resetTimer.start();
m_resetCountdown = actionResetSeconds;
Q_EMIT resetSecondsLeftChanged();
playSound(QStringLiteral("completion-success"));
}
} else if (m_state == State::Confirming) {
if (m_calibratedTargets >= 4) {
m_resetTimer.stop();
m_state = State::Testing;
Q_EMIT stateChanged();
playSound(QStringLiteral("completion-success"));
}
}
}
void CalibrationTool::playSound(const QString &soundName)
{
const auto soundThemeConfig = KSharedConfig::openConfig(QStringLiteral("kdeglobals"));
const KConfigGroup soundGroup = soundThemeConfig->group(QStringLiteral("Sounds"));
const QString soundTheme = soundGroup.readEntry("Theme", QStringLiteral("ocean"));
ca_proplist *props = nullptr;
ca_proplist_create(&props);
ca_proplist_sets(props, CA_PROP_CANBERRA_XDG_THEME_NAME, qUtf8Printable(soundTheme));
ca_proplist_sets(props, CA_PROP_CANBERRA_CACHE_CONTROL, "permanent");
ca_proplist_sets(props, CA_PROP_EVENT_ID, soundName.toLatin1().constData());
ca_context_play_full(canberraContext(), 0, props, &ca_finish_callback, this);
ca_proplist_destroy(props);
}
ca_context *CalibrationTool::canberraContext()
{
if (!m_canberraContext) {
int ret = ca_context_create(&m_canberraContext);
if (ret != CA_SUCCESS) {
qCWarning(KCM_TABLET) << "Failed to initialize canberra context:" << ca_strerror(ret);
m_canberraContext = nullptr;
return nullptr;
}
ca_context_change_props(m_canberraContext,
CA_PROP_APPLICATION_NAME, "Tablet KCM",
CA_PROP_APPLICATION_ID, "kcm_tablet",
CA_PROP_APPLICATION_ICON_NAME, "preferences-desktop-tablet",
nullptr);
}
return m_canberraContext;
}
#include "moc_calibrationtool.cpp"