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