1 /*
2 Copyright (C) 2016-2018 Chih-Hsuan Yen <yan12125@gmail.com>
3
4 This program is free software; you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or
7 (at your option) any later version.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License along
15 with this program; if not, write to the Free Software Foundation, Inc.,
16 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17 */
18
19 #include "touchpaddevice.h"
20
21 #include <cmath>
22 #include <QDebug>
23 #include <QX11Info>
24 #include <QUrl>
25 #include <libudev.h>
26 #include <LXQt/Settings>
27 #include <X11/Xatom.h>
28 #include <X11/extensions/XInput2.h>
29 #include <libinput-properties.h>
30
xi2_get_device_property(int deviceid,const char * prop)31 static QList<QVariant> xi2_get_device_property(int deviceid, const char* prop)
32 {
33 QList<QVariant> ret;
34
35 Display* dpy = QX11Info::display();
36
37 Atom act_type;
38 int act_format;
39 unsigned long nitems, bytes_after;
40 unsigned char *data, *ptr;
41 Atom prop_atom = XInternAtom(dpy, prop, False);
42 XIGetProperty(dpy, deviceid, prop_atom, 0, 1000, False, AnyPropertyType,
43 &act_type, &act_format, &nitems, &bytes_after, &data);
44 ptr = data;
45 for (unsigned long i = 0; i < nitems; i++)
46 {
47 switch (act_type)
48 {
49 case XA_INTEGER:
50 switch (act_format)
51 {
52 case 8:
53 ret << *reinterpret_cast<quint8*>(ptr);
54 break;
55 case 16:
56 ret << *reinterpret_cast<quint16*>(ptr);
57 break;
58 case 32:
59 ret << *reinterpret_cast<quint32*>(ptr);
60 break;
61 default:
62 Q_ASSERT(0);
63 }
64 ptr += act_format / 8;
65 break;
66 case XA_STRING:
67 {
68 Q_ASSERT(act_format == 8);
69 QString s(QString::fromUtf8(reinterpret_cast<char*>(ptr)));
70 ptr += s.size() + 1; // including '\0'
71 ret << s;
72 break;
73 }
74 default:
75 if (act_type == XInternAtom(dpy, "FLOAT", False)) {
76 Q_ASSERT(act_format == 32);
77 ret << *reinterpret_cast<float*>(ptr);
78 ptr += 4;
79 } else {
80 qWarning() << "Unrecognized type" << act_type << "for property" << prop;
81 }
82 }
83 }
84
85 XFree(data);
86
87 return ret;
88 }
89
xi2_set_device_property(int deviceid,const char * prop,QList<QVariant> values)90 static bool xi2_set_device_property(int deviceid, const char* prop, QList<QVariant> values)
91 {
92 Display* dpy = QX11Info::display();
93
94 Atom prop_atom = XInternAtom(dpy, prop, False);
95
96 Atom act_type;
97 int act_format;
98 unsigned long nitems, bytes_after;
99 unsigned char *data;
100 float* float_data = nullptr;
101 // get the property first to determine its type and format
102 XIGetProperty(dpy, deviceid, prop_atom, 0, 1000, False, AnyPropertyType,
103 &act_type, &act_format, &nitems, &bytes_after, &data);
104 if (!nitems)
105 {
106 return false;
107 }
108
109 XFree(data);
110
111 auto dataType = static_cast<QMetaType::Type>(values[0].type());
112 switch (dataType)
113 {
114 case QMetaType::Int:
115 Q_ASSERT(act_type == XA_INTEGER);
116
117 data = new unsigned char[values.size() * act_format / 8];
118 for (int i = 0; i < values.size(); i++)
119 {
120 switch (act_format)
121 {
122 case 8:
123 *reinterpret_cast<int8_t*>(data + i) = values[i].toInt();
124 break;
125 case 16:
126 *reinterpret_cast<int16_t*>(data + i) = values[i].toInt();
127 break;
128 case 32:
129 *reinterpret_cast<int32_t*>(data + i) = values[i].toInt();
130 break;
131 default:
132 Q_ASSERT(0);
133 }
134 }
135 break;
136 case QMetaType::Float:
137 Q_ASSERT(act_type == XInternAtom(dpy, "FLOAT", False));
138 Q_ASSERT(act_format == 32);
139 float_data = new float[values.size()];
140 for (int i = 0; i < values.size(); i++)
141 {
142 float_data[i] = values[i].toFloat();
143 }
144 data = reinterpret_cast<unsigned char*>(float_data);
145 break;
146 default:
147 qWarning() << "Unsupported data type" << dataType;
148 }
149 if (!data)
150 {
151 return false;
152 }
153 XIChangeProperty(dpy, deviceid, prop_atom, act_type, act_format,
154 XIPropModeReplace, data, values.size());
155 // XXX How to catch errors?
156
157 delete [] float_data;
158
159 return true;
160 }
161
get_xi2_property(const char * prop) const162 QList<QVariant> TouchpadDevice::get_xi2_property(const char* prop) const
163 {
164 Q_ASSERT(deviceid);
165
166 return xi2_get_device_property(deviceid, prop);
167 }
168
set_xi2_property(const char * prop,QList<QVariant> values) const169 bool TouchpadDevice::set_xi2_property(const char* prop, QList<QVariant> values) const
170 {
171 Q_ASSERT(deviceid);
172
173 return xi2_set_device_property(deviceid, prop, values);
174 }
175
enumerate_from_udev()176 QList<TouchpadDevice> TouchpadDevice::enumerate_from_udev()
177 {
178 QList<TouchpadDevice> ret;
179 udev *ud = udev_new();
180
181 if (!ud)
182 {
183 qWarning() << "Fails to initialize udev";
184 return ret;
185 }
186
187 udev_enumerate *enumerate = udev_enumerate_new(ud);
188 udev_enumerate_add_match_property(enumerate, "ID_INPUT_TOUCHPAD", "1");
189 udev_enumerate_add_match_property(enumerate, "ID_INPUT_MOUSE", "1");
190 udev_enumerate_scan_devices(enumerate);
191 udev_list_entry *devices = udev_enumerate_get_list_entry(enumerate);
192 for (udev_list_entry *entry = devices; entry; entry = udev_list_entry_get_next(entry)) {
193 const char *path = udev_list_entry_get_name(entry);
194 udev_device *dev = udev_device_new_from_syspath(ud, path);
195
196 const char* devnode = udev_device_get_devnode(dev);
197 if (devnode)
198 {
199 TouchpadDevice dev;
200 dev.devnode = QString::fromUtf8(devnode);
201 if(dev.find_xi2_device())
202 {
203 qDebug() << "Detected" << dev.m_name << "on" << dev.devnode;
204 qDebug() << "xinput driver:" << dev.xinputDriver();
205 dev.m_oldTappingEnabled = dev.tappingEnabled();
206 dev.m_oldNaturalScrollingEnabled = dev.naturalScrollingEnabled();
207 dev.m_oldTapToDragEnabled = dev.tapToDragEnabled();
208 dev.m_oldAccelSpeed = dev.accelSpeed();
209 dev.m_oldScrollingMethodEnabled = dev.scrollingMethodEnabled();
210 ret << dev;
211 }
212 }
213
214 udev_device_unref(dev);
215 }
216 udev_enumerate_unref(enumerate);
217 udev_unref(ud);
218
219 return ret;
220 }
221
222
find_xi2_device()223 bool TouchpadDevice::find_xi2_device()
224 {
225 Display* dpy = QX11Info::display();
226
227 int ndevices;
228 bool found = false;
229 XIDeviceInfo *info = XIQueryDevice(dpy, XIAllDevices, &ndevices);
230
231 for(int i = 0; i < ndevices ; i++)
232 {
233 QList<QVariant> devnode_prop = xi2_get_device_property(info[i].deviceid, "Device Node");
234 if (devnode_prop.size() && devnode_prop[0].toString() == devnode)
235 {
236 m_name = QString::fromUtf8(info[i].name);
237 deviceid = info[i].deviceid;
238
239 // init xinput driver name
240 auto xi_driver_prop = xi2_get_device_property(deviceid, "Synaptics Capabilities");
241 if (xi_driver_prop.size()) {
242 // lxqt does not support xf86-input-synaptics properties
243 m_xinputDriver = QStringLiteral("synaptics");
244 } else {
245 // otherweise we assumge libinput and don't test on libinput properties
246 m_xinputDriver = QStringLiteral("libinput");
247 }
248
249 found = true;
250 break;
251 }
252 }
253
254 XIFreeDeviceInfo(info);
255
256 return found;
257 }
258
escapedName() const259 QString TouchpadDevice::escapedName() const
260 {
261 // device names may contain '/' or '\\' so it should be escaped first
262 // XXX: special characters are double escaped (see writeIniFile in qsettings.cpp)
263 return QString::fromUtf8(QUrl::toPercentEncoding(name(), QByteArray(), QByteArray("/\\")));
264 }
265
loadSettings(LXQt::Settings * settings)266 void TouchpadDevice::loadSettings(LXQt::Settings* settings)
267 {
268 const QList<TouchpadDevice> devices = enumerate_from_udev();
269 settings->beginGroup(QStringLiteral("Touchpad"));
270 for (const TouchpadDevice& device : devices) {
271 qDebug() << "Load settings for" << device.name();
272
273 settings->beginGroup(device.escapedName());
274 if (settings->contains(QLatin1String(TAPPING_ENABLED))) {
275 device.setTappingEnabled(settings->value(QLatin1String(TAPPING_ENABLED)).toBool());
276 }
277 if (settings->contains(QLatin1String(NATURAL_SCROLLING_ENABLED))) {
278 device.setNaturalScrollingEnabled(settings->value(QLatin1String(NATURAL_SCROLLING_ENABLED)).toBool());
279 }
280 if (settings->contains(QLatin1String(TAP_TO_DRAG_ENABLED))) {
281 device.setTapToDragEnabled(settings->value(QLatin1String(TAP_TO_DRAG_ENABLED)).toBool());
282 }
283 if (settings->contains(QLatin1String(ACCELERATION_SPEED))) {
284 device.setAccelSpeed(settings->value(QLatin1String(ACCELERATION_SPEED)).toFloat());
285 }
286 if (settings->contains(QLatin1String(SCROLLING_METHOD_ENABLED))) {
287 device.setScrollingMethodEnabled(
288 static_cast<ScrollingMethod>(settings->value(QLatin1String(SCROLLING_METHOD_ENABLED)).toInt()));
289 }
290 settings->endGroup();
291 }
292 settings->endGroup();
293 }
294
saveSettings(LXQt::Settings * settings) const295 void TouchpadDevice::saveSettings(LXQt::Settings* settings) const
296 {
297 settings->beginGroup(QStringLiteral("Touchpad"));
298
299 settings->beginGroup(escapedName());
300 settings->setValue(QLatin1String(TAPPING_ENABLED), tappingEnabled());
301 settings->setValue(QLatin1String(NATURAL_SCROLLING_ENABLED), naturalScrollingEnabled());
302 settings->setValue(QLatin1String(TAP_TO_DRAG_ENABLED), tapToDragEnabled());
303 settings->setValue(QLatin1String(ACCELERATION_SPEED), accelSpeed());
304 settings->setValue(QLatin1String(SCROLLING_METHOD_ENABLED), scrollingMethodEnabled());
305 settings->endGroup(); // device name
306
307 settings->endGroup(); // "Touchpad"
308 }
309
featureEnabled(const char * prop) const310 int TouchpadDevice::featureEnabled(const char* prop) const
311 {
312 QList<QVariant> propVal = get_xi2_property(prop);
313 if (propVal.size())
314 {
315 return propVal[0].toInt();
316 }
317 else
318 {
319 return -1;
320 }
321 }
322
tappingEnabled() const323 int TouchpadDevice::tappingEnabled() const
324 {
325 return featureEnabled(LIBINPUT_PROP_TAP);
326 }
327
naturalScrollingEnabled() const328 int TouchpadDevice::naturalScrollingEnabled() const
329 {
330 return featureEnabled(LIBINPUT_PROP_NATURAL_SCROLL);
331 }
332
tapToDragEnabled() const333 int TouchpadDevice::tapToDragEnabled() const
334 {
335 return featureEnabled(LIBINPUT_PROP_TAP_DRAG);
336 }
337
accelSpeed() const338 float TouchpadDevice::accelSpeed() const
339 {
340 QList<QVariant> propVal = get_xi2_property(LIBINPUT_PROP_ACCEL);
341 if (propVal.size())
342 {
343 return propVal[0].toFloat();
344 }
345 else
346 {
347 return std::nanf("");
348 }
349 }
350
setTappingEnabled(bool enabled) const351 bool TouchpadDevice::setTappingEnabled(bool enabled) const
352 {
353 return set_xi2_property(LIBINPUT_PROP_TAP, QList<QVariant>({enabled ? 1 : 0}));
354 }
355
setNaturalScrollingEnabled(bool enabled) const356 bool TouchpadDevice::setNaturalScrollingEnabled(bool enabled) const
357 {
358 return set_xi2_property(LIBINPUT_PROP_NATURAL_SCROLL, QList<QVariant>({enabled ? 1 : 0}));
359 }
360
setTapToDragEnabled(bool enabled) const361 bool TouchpadDevice::setTapToDragEnabled(bool enabled) const
362 {
363 return set_xi2_property(LIBINPUT_PROP_TAP_DRAG, QList<QVariant>({enabled ? 1 : 0}));
364 }
365
setAccelSpeed(float speed) const366 bool TouchpadDevice::setAccelSpeed(float speed) const
367 {
368 return set_xi2_property(LIBINPUT_PROP_ACCEL, QList<QVariant>({speed}));
369 }
370
scrollMethodsAvailable() const371 int TouchpadDevice::scrollMethodsAvailable() const
372 {
373 QList<QVariant> values = get_xi2_property(LIBINPUT_PROP_SCROLL_METHODS_AVAILABLE);
374
375 if (!values.size()) {
376 return 0;
377 }
378
379 Q_ASSERT(values.size() == 3);
380
381 return (values[0].toInt() ? TWO_FINGER : 0) |
382 (values[1].toInt() ? EDGE : 0) |
383 (values[2].toInt() ? BUTTON : 0);
384 }
385
scrollingMethodEnabled() const386 ScrollingMethod TouchpadDevice::scrollingMethodEnabled() const
387 {
388 QList<QVariant> values = get_xi2_property(LIBINPUT_PROP_SCROLL_METHOD_ENABLED);
389
390 if (!values.size()) {
391 return NONE;
392 }
393
394 Q_ASSERT(values.size() == 3);
395
396 // those methods are mutually exclusive
397 if (values[0].toInt())
398 return TWO_FINGER;
399 else if (values[1].toInt())
400 return EDGE;
401 else if (values[2].toInt())
402 return BUTTON;
403 else
404 return NONE;
405 }
406
setScrollingMethodEnabled(ScrollingMethod method) const407 bool TouchpadDevice::setScrollingMethodEnabled(ScrollingMethod method) const
408 {
409 QList<QVariant> values;
410
411 values << ((method == TWO_FINGER) ? 1 : 0);
412 values << ((method == EDGE) ? 1 : 0);
413 values << ((method == BUTTON) ? 1 : 0);
414
415 return set_xi2_property(LIBINPUT_PROP_SCROLL_METHOD_ENABLED, values);
416 }
417