├── .gitignore ├── .gitmodules ├── LICENSE ├── Makefile ├── README.md ├── examples ├── 8BitDo Zero 2 gamepad ├── 8bitDo Micro gamepad ├── D01 ├── D01 Pro ├── LIFT └── T01 └── src ├── btpt.cc ├── btpt.h ├── eventcodes.cc └── eventcodes.h /.gitignore: -------------------------------------------------------------------------------- 1 | # make gitignore 2 | .kdev4/ 3 | *.kdev4 4 | .kateconfig 5 | .vscode/ 6 | .idea/ 7 | .clangd/ 8 | .cache/ 9 | compile_commands.json 10 | *.tgz 11 | *.so 12 | *.moc 13 | *.o 14 | src/eventcodes_init.h 15 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "NickelHook"] 2 | path = NickelHook 3 | url = https://github.com/pgaskin/NickelHook 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 Thomas Sowell 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | include NickelHook/NickelHook.mk 2 | 3 | override LIBRARY := libbtpt.so 4 | override SOURCES += src/btpt.cc src/eventcodes.cc 5 | override CFLAGS += -Wall -Wextra -Werror 6 | override CXXFLAGS += -Wall -Wextra -Werror -Wno-missing-field-initializers 7 | override LDFLAGS += -lQt5Core 8 | override PKGCONF += Qt5Widgets 9 | 10 | override MOCS += src/btpt.h 11 | 12 | override GENERATED += src/eventcodes_init.h 13 | 14 | src/eventcodes.cc: src/eventcodes_init.h 15 | 16 | src/eventcodes_init.h: 17 | sed '/^ *#define *[A-Za-z]/!d; s/^ *#define \([A-Za-z0-9_]*\).*/MAP(\1);/;' \ 18 | < /usr/include/linux/input-event-codes.h > $@ 19 | 20 | include NickelHook/NickelHook.mk 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bluetooth page turner for Kobo 2 | 3 | This plugin adds support to Kobo eReaders for turning pages using buttons on 4 | connected Bluetooth devices. It has only been tested on a Kobo Libra 2, but I 5 | don't see why it wouldn't work on other Nickel devices. 6 | 7 | ## Installation 8 | 9 | Copy `KoboRoot.tgz` into the `.kobo` folder of your eReader and eject it. 10 | 11 | After it installs, you'll see a `.btpt` folder on the device. Add 12 | configuration files for your Bluetooth devices there, and connect to them using 13 | the standard Kobo Bluetooth settings menu. You can now use the configured 14 | buttons to change pages in the reading view. 15 | 16 | To uninstall it, create a file in `.btpt` called `uninstall` and restart the 17 | device. 18 | 19 | ### Configuration 20 | 21 | You need two pieces of information about a Bluetooth device to configure it: 22 | 23 | 1. The Bluetooth device name which you can find in the Kobo Bluetooth settings 24 | menu. 25 | 26 | 2. The [Linux Input Subsystem event codes][0] that represent the input events 27 | you want to use to turn pages. You can find these using the [evtest][1] 28 | tool, for example. 29 | 30 | Once you have those, create a configuration file under `.btpt`. The filename 31 | should be the exact Bluetooth device name. 32 | 33 | The file should have a line for each input mapping in the format: 34 | 35 | ``` 36 | METHOD TYPE CODE VALUE 37 | ``` 38 | 39 | `METHOD` is the name of the ReadingView method to invoke. `prevPage` and 40 | `nextPage` turn the pages. `prevChapter` and `nextChapter` are also valid 41 | choices, but I'm not sure if any other methods make sense here. 42 | 43 | `TYPE`, `CODE`, and `VALUE` specify the input event values to match. C-style 44 | integers in decimal, octal, or hexadecimal are accepted, and so are #defines 45 | from ``. 46 | 47 | [0]: https://www.kernel.org/doc/html/v4.14/input/event-codes.html 48 | [1]: https://cgit.freedesktop.org/evtest/ 49 | 50 | #### Example configuration 51 | 52 | I use an 8BitDo Zero 2 in XInput mode. I want it to work sideways in either 53 | orientation, so I want Down (65535 on the Y-axis) and the X button to go to the 54 | previous page, and Up (0 on the Y-axis) and the B button to go to the next 55 | page. 56 | 57 | In XInput mode, the device identifies itself as "8BitDo Zero 2 gamepad", so 58 | the configuration file is `.btpt/8BitDo Zero 2 gamepad` with the following 59 | contents: 60 | 61 | ``` 62 | prevPage EV_ABS ABS_Y 65535 63 | nextPage EV_ABS ABS_Y 0 64 | prevPage EV_KEY BTN_NORTH 0 65 | nextPage EV_KEY BTN_SOUTH 0 66 | ``` 67 | 68 | This file is also present in the `examples` directory of this repository. 69 | 70 | #### Advanced configuration 71 | 72 | You can also name configuration files after a device's 48-bit Bluetooth address 73 | (often represented like `11:22:33:44:FF:EE`, though the filename should have no 74 | semicolons, like `11223344FFEE`). When present, these files take precedence 75 | over files named after the device name, so you can override default behavior 76 | for specific devices. 77 | 78 | The addresses are case insensitive, but behavior is undefined if there are 79 | collisions. 80 | 81 | ## Building from source 82 | 83 | `make` builds the shared library, `libbtpt.so`, which can be installed in 84 | /usr/local/Kobo/imageformats. 85 | 86 | `make koboroot` builds `KoboRoot.tgz` which can be installed over USB. 87 | 88 | Or get pre-built `KoboRoot.tgz` from the release section. 89 | -------------------------------------------------------------------------------- /examples/8BitDo Zero 2 gamepad: -------------------------------------------------------------------------------- 1 | prevPage EV_ABS ABS_Y 65535 2 | nextPage EV_ABS ABS_Y 0 3 | prevPage EV_KEY BTN_NORTH 0 4 | nextPage EV_KEY BTN_SOUTH 0 5 | -------------------------------------------------------------------------------- /examples/8bitDo Micro gamepad: -------------------------------------------------------------------------------- 1 | prevPage EV_KEY KEY_I 0 2 | nextPage EV_KEY KEY_G 0 3 | prevPage EV_KEY KEY_F 0 4 | nextPage EV_KEY KEY_E 0 5 | -------------------------------------------------------------------------------- /examples/D01: -------------------------------------------------------------------------------- 1 | prevPage EV_ABS ABS_Y 1012 2 | nextPage EV_ABS ABS_Y 500 -------------------------------------------------------------------------------- /examples/D01 Pro: -------------------------------------------------------------------------------- 1 | nextPage EV_KEY KEY_NEXTSONG 0 2 | prevPage EV_KEY KEY_PREVIOUSSONG 0 -------------------------------------------------------------------------------- /examples/LIFT: -------------------------------------------------------------------------------- 1 | prevPage EV_KEY BTN_RIGHT 1 2 | nextPage EV_KEY BTN_LEFT 1 3 | -------------------------------------------------------------------------------- /examples/T01: -------------------------------------------------------------------------------- 1 | prevPage EV_ABS ABS_X 3400 2 | prevPage EV_ABS ABS_Y 800 3 | nextPage EV_ABS ABS_X 650 4 | nextPage EV_ABS ABS_Y 2900 5 | -------------------------------------------------------------------------------- /src/btpt.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | 13 | #include 14 | 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | 23 | #include "btpt.h" 24 | #include "eventcodes.h" 25 | 26 | /* Directory in which to look for device configuration */ 27 | #define BTPT_DIR "/mnt/onboard/.btpt/" 28 | 29 | /* Stop Bluetooth heartbeat after this many seconds of inactivity */ 30 | #define BLUETOOTH_TIMEOUT (10*60) 31 | 32 | static void *(*BluetoothHeartbeat)(void *, long long); 33 | static void (*BluetoothHeartbeat_beat)(void *); 34 | 35 | static void *(*MainWindowController_sharedInstance)(); 36 | static QWidget *(*MainWindowController_currentView)(void *); 37 | 38 | static QObject *(*PowerManager_sharedInstance)(); 39 | static int (*PowerManager_filter)(QObject *, QObject *, QEvent *); 40 | static QEvent::Type (*TimeEvent_eventType)(); 41 | 42 | static void invokeMainWindowController(const char *method) 43 | { 44 | QString name = QString(); 45 | void *mwc = MainWindowController_sharedInstance(); 46 | if (!mwc) { 47 | nh_log("invalid MainWindowController"); 48 | return; 49 | } 50 | QWidget *cv = MainWindowController_currentView(mwc); 51 | if (!cv) { 52 | nh_log("invalid View"); 53 | return; 54 | } 55 | name = cv->objectName(); 56 | if (name == "ReadingView") { 57 | nh_log(method); 58 | QMetaObject::invokeMethod(cv, method, Qt::QueuedConnection); 59 | } 60 | else { 61 | nh_log("not reading view"); 62 | } 63 | } 64 | 65 | bool BluetoothPageTurner::addDevice( 66 | const QString &name, const QString &uniq, const QString &handler) 67 | { 68 | int fd; 69 | 70 | if (devices.contains(uniq)) { 71 | return false; 72 | } 73 | 74 | /* Fall back to device name */ 75 | QFile cfg(BTPT_DIR + name); 76 | 77 | /* Look for case-insensitive Bluetooth address in BTPT_DIR */ 78 | QStringList list = QDir(BTPT_DIR).entryList(); 79 | for (int i = 0; i < list.size(); ++i) { 80 | QString filename = list.at(i); 81 | if (filename.toLower() == uniq.toLower()) { 82 | cfg.setFileName(BTPT_DIR + filename); 83 | } 84 | } 85 | 86 | if (!cfg.open(QIODevice::ReadOnly | QIODevice::Text)) { 87 | nh_log("unable to open %s%s", 88 | BTPT_DIR, cfg.fileName().toStdString().c_str()); 89 | return false; 90 | } 91 | 92 | QString path = "/dev/input/" + handler; 93 | fd = open(path.toStdString().c_str(), O_RDONLY | O_NONBLOCK); 94 | if (fd < 0) { 95 | nh_log("error opening /dev/input/%s", 96 | path.toStdString().c_str()); 97 | return false; 98 | } 99 | 100 | /* Found a config file and a /dev/input handler, so configure device */ 101 | Device &device = devices[uniq]; 102 | device.fd = fd; 103 | device.cfg.clear(); 104 | 105 | QByteArray text = cfg.readAll(); 106 | cfg.close(); 107 | QTextStream in(&text); 108 | 109 | while (!in.atEnd()) { 110 | /* Configuration file format is, one per line: 111 | * 112 | * METHOD TYPE CODE VALUE 113 | * 114 | * Invoke METHOD on MainWindowController when input event 115 | * matches TYPE, CODE, and VALUE. 116 | * 117 | * TYPE, CODE, and VALUE can be #defines from 118 | * 119 | */ 120 | 121 | QString line = in.readLine(); 122 | QList parts = line.split(" ", QString::SkipEmptyParts); 123 | if (parts.size() != 4) { 124 | nh_log("invalid config line: %s", 125 | line.toStdString().c_str()); 126 | devices.remove(uniq); 127 | return false; 128 | } 129 | 130 | struct input_event event = { 0 }; 131 | bool ok; 132 | 133 | event.type = parseEventCode(&ok, parts[1]); 134 | if (!ok) { 135 | nh_log("invalid type: %s", 136 | parts[1].toStdString().c_str()); 137 | devices.remove(uniq); 138 | return false; 139 | } 140 | 141 | event.code = parseEventCode(&ok, parts[2]); 142 | if (!ok) { 143 | nh_log("invalid code: %s", 144 | parts[2].toStdString().c_str()); 145 | devices.remove(uniq); 146 | return false; 147 | } 148 | 149 | event.value = parseEventCode(&ok, parts[3]); 150 | if (!ok) { 151 | nh_log("invalid value: %s", 152 | parts[3].toStdString().c_str()); 153 | devices.remove(uniq); 154 | return false; 155 | } 156 | 157 | QPair map(event, parts[0]); 158 | device.cfg.append(map); 159 | } 160 | 161 | nh_log("acquired device %s: %s", 162 | handler.toStdString().c_str(), 163 | uniq.toStdString().c_str()); 164 | 165 | return true; 166 | } 167 | 168 | void BluetoothPageTurner::directoryChanged(const QString &path) 169 | { 170 | (void)path; 171 | mutex.lock(); 172 | deviceChanges++; 173 | newDevice.wakeAll(); 174 | mutex.unlock(); 175 | } 176 | 177 | bool BluetoothPageTurner::scanDevices() 178 | { 179 | bool devicesAdded = false; 180 | 181 | QFile file("/proc/bus/input/devices"); 182 | if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { 183 | nh_log("error opening /proc/bus/input/devices"); 184 | return devicesAdded; 185 | } 186 | 187 | nh_log("checking /proc/bus/input/devices for new devices"); 188 | 189 | QByteArray text = file.readAll(); 190 | 191 | file.close(); 192 | 193 | QTextStream in(&text); 194 | QString i; 195 | QString u; 196 | QString n; 197 | while (!in.atEnd()) { 198 | QString line = in.readLine(); 199 | QString type = line.section(": ", 0, 0); 200 | if (type == "I") { 201 | i = line.section(": ", 1); 202 | } 203 | else if (type == "U") { 204 | /* Bluetooth address without the ':'s */ 205 | u = line.section(": Uniq=", 1); 206 | u.remove(':'); 207 | } 208 | else if (type == "N") { 209 | /* Bluetooth name without surrounding double-quotes */ 210 | n = line.section(": Name=", 1); 211 | n.chop(1); 212 | n.remove(0, 1); 213 | } 214 | else if (type == "H") { 215 | /* Skip if not Bluetooth device */ 216 | if (!i.startsWith("Bus=0005 ")) { 217 | nh_log("skipping %s", i.toStdString().c_str()); 218 | continue; 219 | } 220 | 221 | /* Configure the device */ 222 | nh_log("found %s", i.toStdString().c_str()); 223 | QList handlers = 224 | line.section("Handlers=", 1).split(" "); 225 | foreach(const QString &handler, handlers) { 226 | if (handler.startsWith("event")) { 227 | devicesAdded |= 228 | addDevice(n, u, handler); 229 | } 230 | } 231 | } 232 | } 233 | 234 | nh_log("devices scanned"); 235 | 236 | return devicesAdded; 237 | } 238 | 239 | void BluetoothPageTurner::run() 240 | { 241 | void *bluetoothHeartbeat = NULL; 242 | fd_set rfds; 243 | struct timeval tv; 244 | struct timespec last_event = { 0 }; 245 | struct timespec now = { 0 }; 246 | 247 | nh_log("starting"); 248 | 249 | /* FileSystemWatcher will increment devicesChanges and wake us up when 250 | * /dev/input changes */ 251 | QObject::connect( 252 | &watcher, &QFileSystemWatcher::directoryChanged, 253 | this, &BluetoothPageTurner::directoryChanged); 254 | watcher.addPath("/dev/input"); 255 | 256 | while (1) { 257 | struct input_event e; 258 | int ret; 259 | 260 | /* Scan input devices for new devices */ 261 | mutex.lock(); 262 | if (deviceChanges == 0 && devices.empty()) { 263 | nh_log("waiting for input devices"); 264 | newDevice.wait(&mutex); 265 | deviceChanges = 0; 266 | mutex.unlock(); 267 | if (!scanDevices()) { 268 | continue; 269 | } 270 | } 271 | else if (deviceChanges > 0) { 272 | deviceChanges = 0; 273 | mutex.unlock(); 274 | if (!scanDevices()) { 275 | continue; 276 | } 277 | } 278 | else { 279 | mutex.unlock(); 280 | } 281 | 282 | /* Bluetooth heartbeat to prevent Bluetooth from turning off */ 283 | clock_gettime(CLOCK_MONOTONIC, &now); 284 | if (last_event.tv_sec > 0 && 285 | now.tv_sec - last_event.tv_sec < BLUETOOTH_TIMEOUT) { 286 | if (bluetoothHeartbeat == NULL) { 287 | bluetoothHeartbeat = alloca(1024); 288 | new(bluetoothHeartbeat) QObject(); 289 | BluetoothHeartbeat(bluetoothHeartbeat, 0); 290 | } 291 | BluetoothHeartbeat_beat(bluetoothHeartbeat); 292 | } 293 | 294 | /* Poll devices for input events */ 295 | tv.tv_sec = 1; 296 | tv.tv_usec = 0; 297 | 298 | FD_ZERO(&rfds); 299 | 300 | foreach(const Device &device, devices.values()) { 301 | FD_SET(device.fd, &rfds); 302 | } 303 | 304 | ret = select(FD_SETSIZE, &rfds, NULL, NULL, &tv); 305 | if (ret <= 0) { 306 | continue; 307 | } 308 | 309 | /* Process events for each device */ 310 | for (auto it = devices.begin(); it != devices.end();) { 311 | Device &device = it.value(); 312 | 313 | if (!FD_ISSET(device.fd, &rfds)) { 314 | it++; 315 | continue; 316 | } 317 | 318 | ret = read(device.fd, &e, sizeof(e)); 319 | if (ret < (int)sizeof(e)) { 320 | nh_log("lost device"); 321 | it = devices.erase(it); 322 | continue; 323 | } 324 | 325 | if (e.type == 0x00 && 326 | e.code == 0x00 && 327 | e.value == 0x0001) { 328 | nh_log("lost device"); 329 | it = devices.erase(it); 330 | continue; 331 | } 332 | 333 | for (int i = 0; i < device.cfg.size(); i++) { 334 | auto &pair = device.cfg[i]; 335 | struct input_event test = pair.first; 336 | if (e.type == test.type && 337 | e.code == test.code && 338 | e.value == test.value) { 339 | /* Update Bluetooth heartbeat time */ 340 | clock_gettime(CLOCK_MONOTONIC, 341 | &last_event); 342 | 343 | /* Update PowerManager::timeLastUsed */ 344 | emit notify(); 345 | 346 | /* Invoke the configured method */ 347 | const char *method = pair.second 348 | .toStdString().c_str(); 349 | invokeMainWindowController(method); 350 | } 351 | } 352 | 353 | it++; 354 | } 355 | } 356 | } 357 | 358 | void TimeLastUsedUpdater::notify() 359 | { 360 | /* Update PowerManager::timeLastUsed to prevent sleep */ 361 | QEvent timeEvent(TimeEvent_eventType()); 362 | 363 | QObject *pm = PowerManager_sharedInstance(); 364 | if (pm == NULL) { 365 | nh_log("invalid PowerManager"); 366 | } 367 | 368 | PowerManager_filter(pm, this, &timeEvent); 369 | } 370 | 371 | static int btpt_init() 372 | { 373 | mkdir(BTPT_DIR, 0755); 374 | TimeLastUsedUpdater *timeLastUsedUpdater = new TimeLastUsedUpdater(); 375 | BluetoothPageTurner *btpt = new BluetoothPageTurner(); 376 | QObject::connect( 377 | btpt, &BluetoothPageTurner::notify, 378 | timeLastUsedUpdater, &TimeLastUsedUpdater::notify, 379 | Qt::QueuedConnection); 380 | QObject::connect( 381 | btpt, &QThread::finished, 382 | btpt, &QObject::deleteLater); 383 | QObject::connect( 384 | btpt, &QThread::finished, 385 | timeLastUsedUpdater, &QObject::deleteLater); 386 | btpt->start(); 387 | 388 | return 0; 389 | } 390 | 391 | static struct nh_info btpt_info = { 392 | .name = "BluetoothPageTurner", 393 | .desc = "Turn pages with Bluetooth device", 394 | .uninstall_flag = BTPT_DIR "/uninstall", 395 | }; 396 | 397 | static struct nh_hook btpt_hook[] = { 398 | {0}, 399 | }; 400 | 401 | static struct nh_dlsym btpt_dlsym[] = { 402 | { 403 | .name = "_ZN18BluetoothHeartbeatC1Ex", 404 | .out = nh_symoutptr(BluetoothHeartbeat) 405 | }, 406 | { 407 | .name = "_ZN18BluetoothHeartbeat4beatEv", 408 | .out = nh_symoutptr(BluetoothHeartbeat_beat) 409 | }, 410 | { 411 | .name = "_ZN20MainWindowController14sharedInstanceEv", 412 | .out = nh_symoutptr(MainWindowController_sharedInstance) 413 | }, 414 | { 415 | .name = "_ZNK20MainWindowController11currentViewEv", 416 | .out = nh_symoutptr(MainWindowController_currentView) 417 | }, 418 | { 419 | .name = "_ZN9TimeEvent9eventTypeEv", 420 | .out = nh_symoutptr(TimeEvent_eventType) 421 | }, 422 | { 423 | .name = "_ZN12PowerManager14sharedInstanceEv", 424 | .out = nh_symoutptr(PowerManager_sharedInstance) 425 | }, 426 | { 427 | .name = "_ZN12PowerManager6filterEP7QObjectP6QEvent", 428 | .out = nh_symoutptr(PowerManager_filter) 429 | }, 430 | {0}, 431 | }; 432 | 433 | NickelHook( 434 | .init = btpt_init, 435 | .info = &btpt_info, 436 | .hook = btpt_hook, 437 | .dlsym = btpt_dlsym, 438 | ) 439 | -------------------------------------------------------------------------------- /src/btpt.h: -------------------------------------------------------------------------------- 1 | #ifndef BTPT_H 2 | #define BTPT_H 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | class Device 16 | { 17 | public: 18 | int fd; 19 | QList> cfg; 20 | }; 21 | 22 | class BluetoothPageTurner : public QThread 23 | { 24 | Q_OBJECT 25 | 26 | void run() override; 27 | 28 | private: 29 | bool addDevice( 30 | const QString &name, 31 | const QString &uniq, 32 | const QString &handler); 33 | bool scanDevices(); 34 | void learn(Device &device); 35 | QFileSystemWatcher watcher; 36 | QMap devices; 37 | QMutex mutex; 38 | QWaitCondition newDevice; 39 | int deviceChanges = 0; 40 | 41 | public slots: 42 | void directoryChanged(const QString &path); 43 | 44 | signals: 45 | void notify(); 46 | }; 47 | 48 | class TimeLastUsedUpdater : public QObject 49 | { 50 | Q_OBJECT 51 | 52 | public slots: 53 | void notify(); 54 | }; 55 | 56 | #endif /* BTPT_H */ 57 | -------------------------------------------------------------------------------- /src/eventcodes.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | 6 | #include 7 | 8 | #define VALUE(code) code 9 | #define MAP(code) map[#code] = VALUE(code) 10 | 11 | static QMap init() 12 | { 13 | QMap map; 14 | #include "eventcodes_init.h" 15 | return map; 16 | } 17 | 18 | static QMap codes = init(); 19 | 20 | /* Parse C-style integer or #define from */ 21 | int parseEventCode(bool *ok, const QString &s) 22 | { 23 | int value = s.toInt(ok, 0); 24 | if (*ok) { 25 | return value; 26 | } 27 | else if (codes.contains(s)) { 28 | *ok = true; 29 | return codes[s]; 30 | } 31 | 32 | *ok = false; 33 | return -1; 34 | } 35 | -------------------------------------------------------------------------------- /src/eventcodes.h: -------------------------------------------------------------------------------- 1 | #ifndef EVENTCODES_H 2 | #define EVENTCODES_H 3 | 4 | #include 5 | #include 6 | 7 | int parseEventCode(bool *ok, const QString &s); 8 | 9 | #endif /* EVENTCODES_H */ 10 | --------------------------------------------------------------------------------