#include #include #include #include #include #include "kb.h" #include "kbmanager.h" // All active devices static QSet activeDevices; // Active notification node paths static QSet notifyPaths; static QMutex notifyPathMutex; int Kb::_frameRate = 30, Kb::_scrollSpeed = 0; bool Kb::_dither = false, Kb::_mouseAccel = true; static inline Kb::pollrate_t stringToPollrate(const QString& str){ if(str == QLatin1String("8 ms")) return Kb::POLLRATE_8MS; else if(str == QLatin1String("4 ms")) return Kb::POLLRATE_4MS; else if(str == QLatin1String("2 ms")) return Kb::POLLRATE_2MS; else if(str == QLatin1String("1 ms")) return Kb::POLLRATE_1MS; else if(str == QLatin1String("0.5 ms")) return Kb::POLLRATE_05MS; else if(str == QLatin1String("0.25 ms")) return Kb::POLLRATE_025MS; else if(str == QLatin1String("0.1 ms")) return Kb::POLLRATE_01MS; else return Kb::POLLRATE_UNKNOWN; } Kb::Kb(QObject *parent, const QString& path) : QThread(parent), features(QStringList()), pollrate(POLLRATE_UNKNOWN), maxpollrate(POLLRATE_UNKNOWN), monochrome(false), hwload(false), adjrate(false), firmware(), batteryTimer(nullptr), batteryIcon(nullptr), showBatteryIndicator(false), devpath(path), cmdpath(path + "/cmd"), notifyPath(path + "/notify1"), macroPath(path + "/notify2"), _currentProfile(nullptr), _currentMode(nullptr), _model(KeyMap::NO_MODEL), batteryLevel(0), batteryStatus(BatteryStatus::BATT_STATUS_UNKNOWN), _hwProfile(nullptr), prevProfile(nullptr), prevMode(nullptr), cmd(cmdpath), notifyNumber(1), macroNumber(2), _needsSave(false), _layout(KeyMap::NO_LAYOUT), _maxDpi(0), deviceIdleTimer() { memset(iState, 0, sizeof(iState)); memset(hwLoading, 0, sizeof(hwLoading)); // Get the features, model, serial number, FW version (if available), poll rate (if available), and layout from /dev nodes QFile ftpath(path + "/features"), mpath(path + "/model"), spath(path + "/serial"), fwpath(path + "/fwversion"), ppath(path + "/pollrate"), prodpath(path + "/productid"), hwlayoutPath(path + "/layout"), dpiPath(path + "/dpi"); if (ftpath.open(QIODevice::ReadOnly)){ QString featurestr = ftpath.read(1000).trimmed(); ftpath.close(); // Read model from features (first word: vendor, second word: product) features = featurestr.split(" "); if(features.length() < 2) return; _model = KeyMap::getModel(features[1]); if(_model == KeyMap::NO_MODEL) { qDebug() << "could not find valid model information:" << features[1] << "produced" << _model; return; } } else { // Bail if features aren't readable qDebug() << "Could not open" << ftpath.fileName(); return; } if (features.contains("monochrome")) monochrome = true; if (features.contains("hwload")) hwload = true; if (mpath.open(QIODevice::ReadOnly)){ usbModel = mpath.read(100); usbModel = usbModel.remove("Corsair", Qt::CaseInsensitive).remove("Gaming").remove("Keyboard").remove("Mouse").remove("Bootloader").remove("Mechanical").replace("LOW PROFILE", "LP").trimmed(); mpath.close(); } if (usbModel == "") usbModel = "Keyboard"; if (spath.open(QIODevice::ReadOnly)){ usbSerial = spath.read(100); usbSerial = usbSerial.trimmed().toUpper(); spath.close(); } if (usbSerial == "") usbSerial = "Unknown-" + usbModel; if (fwpath.open(QIODevice::ReadOnly)) { firmware.parse(fwpath.read(100)); fwpath.close(); if (prodpath.open(QIODevice::ReadOnly)) { productID = prodpath.read(4).toUShort(nullptr, 16); // qInfo() << "ProductID of device is" << productID; } else { qCritical() << "could not open" << prodpath.fileName(); } } if (ppath.open(QIODevice::ReadOnly)){ QStringList sl = QString(ppath.read(100)).trimmed().split(QLatin1Char('\n')); ppath.close(); if(sl.length() > 0) pollrate = stringToPollrate(sl.at(0)); if(sl.length() > 1) maxpollrate = stringToPollrate(sl.at(1)); if(pollrate > maxpollrate) maxpollrate = POLLRATE_UNKNOWN; if(features.contains("adjrate")) adjrate = true; } if(hwlayoutPath.open(QIODevice::ReadOnly)){ hwlayout = hwlayoutPath.read(10); hwlayout = hwlayout.trimmed(); hwlayoutPath.close(); } if(dpiPath.open(QIODevice::ReadOnly)){ _maxDpi = dpiPath.read(6).trimmed().toUShort(); dpiPath.close(); } if(!_maxDpi) _maxDpi = 12000; prefsPath = "Devices/" + usbSerial; hwModeCount = (_model == KeyMap::K95) ? 3 : 1; // Open cmd in non-blocking mode so that it doesn't lock up if nothing is reading // (e.g. if the daemon crashed and didn't clean up the node) int fd = open(cmdpath.toLatin1().constData(), O_WRONLY | O_NONBLOCK); if(!cmd.open(fd, QIODevice::WriteOnly, QFileDevice::AutoCloseHandle)) return; // Find an available notification node (if none is found, take notify1) { QMutexLocker locker(¬ifyPathMutex); for(int i = 1; i < 10; i++){ QString notify = QString(path + "/notify%1").arg(i); if(!QFile::exists(notify) && !notifyPaths.contains(notify)){ notifyNumber = i; notifyPath = notify; break; } } notifyPaths.insert(notifyPath); } cmd.write(QString("notifyon %1\n").arg(notifyNumber).toLatin1()); cmd.flush(); if(features.contains("battery")){ batteryIcon = new BatteryStatusTrayIcon(usbModel, this); updateBattery(); batteryTimer = new QTimer(this); connect(batteryTimer, &QTimer::timeout, this, &Kb::updateBattery); connect(this, &Kb::batteryChanged, batteryIcon, &BatteryStatusTrayIcon::setBattery); if (this->currentPerf()) connect(this, &Kb::batteryChangedLed, this->currentPerf(), &KbPerf::setBattery); if(showBatteryIndicator) batteryIcon->show(); batteryTimer->setInterval(10000); batteryTimer->start(); } // Again, find an available notification node for macro definition // (if none is found, take notify2) { QMutexLocker locker(¬ifyPathMutex); for(int i = 1; i < 10; i++){ QString notify = QString(path + "/notify%1").arg(i); if(!QFile::exists(notify) && !notifyPaths.contains(notify)){ macroNumber = i; macroPath = notify; break; } } notifyPaths.insert(notifyPath); ///< \todo Is adding notify2 to the notifypaths neccessary? } // Activate device, apply settings, and ask for hardware profile cmd.write(QString("fps %1\n").arg(_frameRate).toLatin1()); cmd.write(QString("dither %1\n").arg(static_cast(_dither)).toLatin1()); #ifdef Q_OS_MACOS // Write ANSI/ISO flag to daemon (OSX only) cmd.write("layout "); cmd.write(KeyMap::isISO(_layout) ? "iso" : "ansi"); // Also OSX only: scroll speed and mouse acceleration cmd.write(QString("accel %1\n").arg(QString(_mouseAccel ? "on" : "off")).toLatin1()); cmd.write(QString("scrollspeed %1\n").arg(_scrollSpeed).toLatin1()); #endif cmd.write(QString("\nactive\n@%1 get :hwprofileid").arg(notifyNumber).toLatin1()); hwLoading[0] = true; for(int i = 0; i < hwModeCount; i++){ cmd.write(QString(" mode %1 get :hwid").arg(i + 1).toLatin1()); hwLoading[i + 1] = true; } // Ask for current indicator and key state cmd.write(" get :i :keys\n"); cmd.flush(); emit infoUpdated(); activeDevices.insert(this); // Start a separate thread to read from the notification node start(); } Kb::~Kb(){ // Save settings first save(); delete batteryTimer; delete batteryIcon; // remove the notify channel from the list of notifyPaths. ///< \todo I don't think, that notifypaths is used somewhere. So why do we have it? /// If we do not need it, searching for an ununsed notify channel can easy be refactored to a private member function. notifyPaths.remove(macroPath); // Kill notification thread and remove node activeDevices.remove(this); // FIXME: https://github.com/ckb-next/ckb-next/pull/1011 if(QFile::exists(cmdpath) && cmd.isOpen() && notifyNumber > 0){ cmd.write(QString("idle\nnotifyoff %1\n").arg(notifyNumber).toLatin1()); // Manually flush so that the daemon closes the notify pipe and the thread can gracefully stop cmd.flush(); } if(!wait(1000)){ terminate(); qDebug() << "Second wait returned" << wait(1000); } // This does nothing if cmd isn't open. cmd.close(); } void Kb::frameRate(int newFrameRate){ KbManager::fps(newFrameRate); // If the rate has changed, send to all devices if(newFrameRate == _frameRate) return; _frameRate = newFrameRate; foreach(Kb* kb, activeDevices){ kb->cmd.write(QString("fps %1\n").arg(newFrameRate).toLatin1()); kb->cmd.flush(); } } void Kb::layout(KeyMap::Layout newLayout, bool stop){ if(newLayout == KeyMap::NO_LAYOUT || newLayout == _layout) return; _layout = newLayout; // Update the current device this->updateLayout(stop); } void Kb::updateLayout(bool stop){ #ifdef Q_OS_MACOS // Write ANSI/ISO flag to daemon (OSX only) cmd.write("layout "); cmd.write(KeyMap::isISO(_layout) ? "iso" : "ansi"); cmd.write("\n"); cmd.flush(); #endif foreach(KbProfile* profile, _profiles) profile->keyMap(getKeyMap()); if(_hwProfile && !_profiles.contains(_hwProfile)) _hwProfile->keyMap(getKeyMap()); // Stop all animations as they'll need to be restarted, only if requested if(stop){ foreach(KbMode* mode, _currentProfile->modes()) mode->light()->close(); } emit infoUpdated(); } void Kb::updateBattery(){ cmd.write(QString("@%1 get :battery\n").arg(notifyNumber).toLatin1()); cmd.flush(); } void Kb::dither(bool newDither){ if(newDither == _dither) return; _dither = newDither; // Update all devices foreach(Kb* kb, activeDevices){ kb->cmd.write(QString("dither %1\n").arg(static_cast(newDither)).toLatin1()); kb->cmd.flush(); } } void Kb::mouseAccel(bool newAccel){ if(newAccel == _mouseAccel) return; _mouseAccel = newAccel; #ifdef Q_OS_MACOS // Update all devices foreach(Kb* kb, activeDevices){ kb->cmd.write(QString("accel %1\n").arg(QString(newAccel ? "on" : "off")).toLatin1()); kb->cmd.flush(); } #endif } void Kb::scrollSpeed(int newSpeed){ if(newSpeed == _scrollSpeed) return; _scrollSpeed = newSpeed; #ifdef Q_OS_MACOS // Update all devices foreach(Kb* kb, activeDevices){ kb->cmd.write(QString("scrollspeed %1\n").arg(newSpeed).toLatin1()); kb->cmd.flush(); } #endif } void Kb::load(){ if(prefsPath.isEmpty()) return; _needsSave = false; CkbSettings settings(prefsPath); // Read profiles KbProfile* newCurrentProfile = nullptr; QString current = settings.value("CurrentProfile").toString().trimmed().toUpper(); foreach(QString guid, settings.value("Profiles").toString().split(" ")){ guid = guid.trimmed().toUpper(); if(guid != ""){ KbProfile* profile = new KbProfile(this, getKeyMap(), settings, guid); _profiles.append(profile); if(guid == current || !newCurrentProfile) newCurrentProfile = profile; } } showBatteryIndicator = settings.value("batteryIndicator", true).toBool(); if(newCurrentProfile) setCurrentProfile(newCurrentProfile); else { KeyMap map = getKeyMap(); // If nothing was loaded, load the appropriate demo profile for each device QString demoProfile(":/txt/demoprofile.conf"); if(map.model() == KeyMap::M95) demoProfile = ":/txt/demoprofile_m95.ini"; else if(map.model() == KeyMap::K55) demoProfile = ":/txt/demoprofile_k55.ini"; else if(map.model() == KeyMap::POLARIS) demoProfile = ":/txt/demoprofile_polaris.ini"; else if(map.model() == KeyMap::ST100) demoProfile = ":/txt/demoprofile_st100.ini"; else if(map.model() == KeyMap::NIGHTSWORD) demoProfile = ":/txt/demoprofile_nightsword.ini"; else if(map.model() == KeyMap::K55PRO) demoProfile = ":/txt/demoprofile_k55pro.ini"; else if(map.model() == KeyMap::MM700) demoProfile = ":/txt/demoprofile_mm700.ini"; else if(map.model() == KeyMap::K55_CORE) demoProfile = ":/txt/demoprofile_k55_core.ini"; QSettings demoSettings(demoProfile, QSettings::IniFormat, this); CkbDemoSettings cSettings(demoSettings); KbProfile* demo = new KbProfile(this, map, cSettings, "{BA7FC152-2D51-4C26-A7A6-A036CC93D924}"); _profiles.append(demo); setCurrentProfile(demo); } emit infoUpdated(); emit profileAdded(); } void Kb::save(){ if(prefsPath.isEmpty()) return; _needsSave = false; CkbSettings settings(prefsPath, true); QString guids, currentGuid; foreach(KbProfile* profile, _profiles){ guids.append(" " + profile->id().guidString()); if(profile == _currentProfile) currentGuid = profile->id().guidString(); profile->save(settings); } settings.setValue("CurrentProfile", currentGuid); settings.setValue("Profiles", guids.trimmed()); settings.setValue("hwLayout", KeyMap::getLayout(_layout)); settings.setValue("batteryIndicator", showBatteryIndicator); } void Kb::autoSave(){ if(needsSave() && !CkbSettings::isBusy()) save(); } void Kb::hwSave(){ if(!_currentProfile) return; // Close active lighting (if any) if(prevMode){ prevMode->light()->close(); deletePrevious(); } hwProfile(_currentProfile); _hwProfile->id().hwModified = _hwProfile->id().modified; _hwProfile->setNeedsSave(); // Re-send the current profile from scratch to ensure consistency writeProfileHeader(); // Make sure there are enough modes while(_currentProfile->modeCount() < hwModeCount) _currentProfile->append(new KbMode(this, getKeyMap())); // Write only the base colors of each mode, no animations for(int i = 0; i < hwModeCount; i++){ KbMode* mode = _currentProfile->modes()[i]; cmd.write(QString("\nmode %1").arg(i + 1).toLatin1()); KbLight* light = mode->light(); KbPerf* perf = mode->perf(); if(mode == _currentMode) cmd.write(" switch"); // Write the mode name and ID cmd.write(" name "); cmd.write(QUrl::toPercentEncoding(mode->name())); cmd.write(" id "); cmd.write(mode->id().guidString().toLatin1()); cmd.write(" "); cmd.write(mode->id().modifiedString().toLatin1()); cmd.write(" "); // Write lighting and performance light->base(cmd, true, monochrome); cmd.write(" "); perf->update(cmd, notifyNumber, true, false); // Update mode ID mode->id().hwModified = mode->id().modified; mode->setNeedsSave(); } cmd.write("\n"); // Save the profile to memory cmd.write("hwsave\n"); cmd.flush(); } bool Kb::needsSave() const { if(_needsSave) return true; foreach(const KbProfile* profile, _profiles){ if(profile->needsSave()) return true; } return false; } void Kb::writeProfileHeader(){ cmd.write("eraseprofile"); // Write the profile name and ID cmd.write(" profilename "); cmd.write(QUrl::toPercentEncoding(_currentProfile->name())); cmd.write(" profileid "); cmd.write(_currentProfile->id().guidString().toLatin1()); cmd.write(" "); cmd.write(_currentProfile->id().modifiedString().toLatin1()); } void Kb::fwUpdate(const QString& path){ fwUpdPath = path; // Write the active command to ensure it's not ignored cmd.write("active"); cmd.write(QString(" @%1 ").arg(notifyNumber).toLatin1()); cmd.write("fwupdate "); cmd.write(path.toLatin1()); cmd.write("\n"); } void Kb::frameUpdate(){ // Advance animation frame if(!_currentMode) return; KbLight* light = _currentMode->light(); KbBind* bind = _currentMode->bind(); KbPerf* perf = _currentMode->perf(); if(!light->isStarted()){ // Don't do anything until the animations are started light->open(); return; } // Stop animations on the previously active mode (if any) bool changed = false; if(prevMode != _currentMode){ if(prevMode){ prevMode->light()->close(); disconnect(prevMode, SIGNAL(destroyed()), this, SLOT(deletePrevious())); } prevMode = _currentMode; connect(prevMode, SIGNAL(destroyed()), this, SLOT(deletePrevious())); changed = true; } // If the profile has changed, update it if(prevProfile != _currentProfile){ writeProfileHeader(); cmd.write(" "); prevProfile = _currentProfile; } // Update current mode int index = _currentProfile->indexOf(_currentMode); // ckb-daemon only has 6 modes: 3 hardware, 3 non-hardware. Beyond mode six, switch back to four. // e.g. 1, 2, 3, 4, 5, 6, 4, 5, 6, 4, 5, 6 ... if(index >= 6) index = 3 + index % 3; // Send lighting/binding to driver if(prevMode != _currentMode || changed) cmd.write(QString("mode %1 switch ").arg(index + 1).toLatin1()); perf->applyIndicators(index, iState); light->frameUpdate(cmd, monochrome); bind->update(cmd, notifyNumber, changed); perf->update(cmd, notifyNumber, changed, true); cmd.flush(); } void Kb::deletePrevious(){ disconnect(prevMode, SIGNAL(destroyed()), this, SLOT(deletePrevious())); prevMode = nullptr; } void Kb::hwProfile(KbProfile* newHwProfile){ if(_hwProfile == newHwProfile) return; if(_hwProfile) disconnect(_hwProfile, SIGNAL(destroyed()), this, SLOT(deleteHw())); _hwProfile = newHwProfile; if(_hwProfile) connect(_hwProfile, SIGNAL(destroyed()), this, SLOT(deleteHw())); } void Kb::deleteHw(){ disconnect(_hwProfile, SIGNAL(destroyed()), this, SLOT(deleteHw())); _hwProfile = nullptr; } void Kb::run(){ QFile notify(notifyPath); // Wait a small amount of time for the node to open (100ms) QThread::usleep(100000); if(!notify.open(QIODevice::ReadOnly)){ // If it's still not open, try again before giving up (1s at a time, 10s total) QThread::usleep(900000); for(int i = 1; i < 10; i++){ if(notify.open(QIODevice::ReadOnly)) break; QThread::sleep(1); } if(!notify.isOpen()) return; } // Read data from notification node QByteArray line; while(notify.isOpen() && (line = notify.readLine()).length() > 0){ QString text = QString::fromUtf8(line); metaObject()->invokeMethod(this, "readNotify", Qt::QueuedConnection, Q_ARG(QString, text)); } QMutexLocker locker(¬ifyPathMutex); notifyPaths.remove(notifyPath); qDebug() << "Notify thread returning. Read" << line.length() << "isOpen()" << notify.isOpen(); } void Kb::readNotify(const QString& line){ QStringList components = line.trimmed().split(" "); if(components.count() < 2) return; if(components[0] == "key"){ // Key event QString key = components[1]; if(key.length() < 2) return; QString keyName = key.mid(1); bool keyPressed = (key[0] == '+'); KbMode* mode = _currentMode; if(mode){ mode->light()->animKeypress(keyName, keyPressed); mode->bind()->keyEvent(keyName, keyPressed); } deviceIdleTimer.start(); } else if (components[0] == "battery"){ QStringList bComponents = components[1].split(':'); if(bComponents.length() != 2) return; // Convert battery values into human readable text bool ok, ok2; uint newBatteryLevel = bComponents[0].toUInt(&ok), newBatteryStatus = bComponents[1].toUInt(&ok2); if(!ok || !ok2 || newBatteryStatus >= BatteryStatus::BATT_STATUS_INVALID || (batteryLevel == newBatteryLevel && batteryStatus == newBatteryStatus)) return; batteryLevel = newBatteryLevel; batteryStatus = static_cast(newBatteryStatus); emit batteryChanged(batteryLevel, batteryStatus); emit batteryChangedLed(batteryLevel, batteryStatus); } else if(components[0] == "i"){ // Indicator event QString i = components[1]; if(i.length() < 2) return; QString iName = i.mid(1); bool on = (i[0] == '+'); if(iName == "num") iState[0] = on; else if(iName == "caps") iState[1] = on; else if(iName == "scroll") iState[2] = on; } else if(components[0] == "hwprofileid"){ // Hardware profile ID if(components.count() < 3) return; // Find the hardware profile in the list of profiles QString guid = components[1]; QString modified = components[2]; KbProfile* newProfile = nullptr; foreach(KbProfile* profile, _profiles){ if(profile->id().guid == QUuid::fromString(guid)){ newProfile = profile; break; } } // If it wasn't found, create it if(!newProfile){ newProfile = new KbProfile(this, getKeyMap(), guid, modified); hwLoading[0] = true; cmd.write(QString("@%1 get :hwprofilename\n").arg(notifyNumber).toLatin1()); cmd.flush(); } else { // If it's been updated, fetch its name if(newProfile->id().hwModifiedString() != modified){ newProfile->id().modifiedString(modified); newProfile->id().hwModifiedString(modified); newProfile->setNeedsSave(); if(hwLoading[0]){ cmd.write(QString("@%1 get :hwprofilename\n").arg(notifyNumber).toLatin1()); cmd.flush(); } } else { hwLoading[0] = false; } } hwProfile(newProfile); emit profileAdded(); } else if(components[0] == "hwprofilename"){ // Hardware profile name QString name = QUrl::fromPercentEncoding(components[1].toUtf8()); if(!_hwProfile || !hwLoading[0]) return; QString oldName = _hwProfile->name(); if(!(oldName.length() >= name.length() && oldName.left(name.length()) == name)){ // Don't change the name if it's a truncated version of what we already have _hwProfile->name(name); emit profileRenamed(); } } else if(components[0] == "mode"){ // Mode-specific data if(components.count() < 4) return; int mode = components[1].toInt() - 1; if(components[2] == "hwid"){ if(components.count() < 5 || mode >= HWMODE_MAX || !_hwProfile) return; // Hardware mode ID QString guid = components[3]; QString modified = components[4]; // Look for this mode in the hardware profile KbMode* hwMode = nullptr; bool isUpdated = false; foreach(KbMode* kbMode, _hwProfile->modes()){ if(kbMode->id().guid == QUuid::fromString(guid)){ hwMode = kbMode; if(kbMode->id().hwModifiedString() != modified){ // Update modification time hwMode->id().modifiedString(modified); hwMode->id().hwModifiedString(modified); hwMode->setNeedsSave(); isUpdated = true; } else { hwLoading[mode + 1] = false; } break; } } // If it wasn't found, add it if(!hwMode){ isUpdated = true; hwMode = new KbMode(this, getKeyMap(), guid, modified); _hwProfile->append(hwMode); // If the hardware profile now contains enough modes to be added to the list, do so if(!_profiles.contains(_hwProfile) && _hwProfile->modeCount() >= hwModeCount){ _profiles.append(_hwProfile); _needsSave = true; emit profileAdded(); if(!_currentProfile) setCurrentProfile(_hwProfile); } } if(hwLoading[mode + 1] && isUpdated){ // If the mode isn't in the right place, move it int index = _hwProfile->indexOf(hwMode); if(mode < _hwProfile->modeCount() && index != mode) _hwProfile->move(index, mode); // Fetch the updated data cmd.write(QString("@%1 mode %2 get :hwname :hwrgb").arg(notifyNumber).arg(mode + 1).toLatin1()); if(isMouse()) cmd.write(" :hwdpi :hwdpisel :hwlift :hwsnap"); cmd.write("\n"); cmd.flush(); } } else if(components[2] == "hwname"){ // Mode name - update list if(!_hwProfile || _hwProfile->modeCount() <= mode || mode >= HWMODE_MAX || !hwLoading[mode + 1]) return; KbMode* hwMode = _hwProfile->modes()[mode]; QString name = QUrl::fromPercentEncoding(components[3].toUtf8()); QString oldName = hwMode->name(); if(!(oldName.length() >= name.length() && oldName.left(name.length()) == name)){ // Don't change the name if it's a truncated version of what we already have hwMode->name(name); } } else if(components[2] == "hwrgb"){ // RGB - set mode lighting if(!_hwProfile || _hwProfile->modeCount() <= mode || mode >= HWMODE_MAX || !hwLoading[mode + 1]) return; KbMode* kbmode = _hwProfile->modes()[mode]; KbLight* light = kbmode->light(); // Scan the input for colors QColor lightColor = QColor(); for(int i = 3; i < components.count(); i++){ QString comp = components[i]; if(comp.indexOf(":") < 0){ // No ":" - single hex constant bool ok; int rgb = comp.toInt(&ok, 16); if(ok) light->color(QColor::fromRgb((QRgb)rgb)); } else { // List of keys ("a,b:xxxxxx"). Parse color first QStringList set = comp.split(":"); bool ok; int rgb = set[1].toInt(&ok, 16); if(ok){ QColor color = QColor::fromRgb((QRgb)rgb); // Parse keys QStringList keys = set[0].split(","); foreach(QString key, keys){ if(key == "light") // Extrapolate the Light key to the M-keys and Lock key, since those will be set to black on hwsave lightColor = color; if(key.startsWith("dpi") && key.length() > 3){ // DPI levels go to the KbPerf object instead of KbLight int index = key.mid(3).toInt(&ok); if(ok) kbmode->perf()->dpiColor(index, color); continue; } light->color(key, color); } } } } if(lightColor.isValid()){ light->color("mr", lightColor); light->color("m1", lightColor); light->color("m2", lightColor); if(!(this->model() == KeyMap::K70MK2 || this->model() == KeyMap::STRAFE_MK2)) light->color("m3", lightColor); light->color("lock", lightColor); } } else if(components[2] == "hwdpi"){ // DPI settings if(!_hwProfile || _hwProfile->modeCount() <= mode || mode >= HWMODE_MAX || !hwLoading[mode + 1]) return; KbPerf* perf = _hwProfile->modes()[mode]->perf(); // Read the rest of the line as stage:x,y foreach(QString comp, components.mid(3)){ QStringList dpi = comp.split(':'); if(dpi.length() != 2) continue; QStringList xy = dpi[1].split(','); int x = 0, y = 0; bool off = false; if(xy.length() < 2){ // If the right side only has one parameter, set both X and Y if(xy[0] == "off") off = true; else x = y = xy[0].toInt(); } else { x = xy[0].toInt(); y = xy[1].toInt(); } // Set DPI for this stage int index = dpi[0].toInt(); if(off){ perf->dpiEnabled(index, false); // If all DPIs have been disabled, turn them back on bool allOff = true; for(int i = 1; i < KbPerf::DPI_COUNT; i++){ if(perf->dpiEnabled(i)){ allOff = false; break; } } if(allOff){ for(int i = 1; i < KbPerf::DPI_COUNT; i++) perf->dpiEnabled(i, true); } } else { perf->dpiEnabled(index, true); perf->dpi(index, QPoint(x, y)); } } } else if(components[2] == "hwdpisel"){ // Hardware DPI selection (0...5) if(!_hwProfile || _hwProfile->modeCount() <= mode || mode >= HWMODE_MAX || !hwLoading[mode + 1]) return; KbPerf* perf = _hwProfile->modes()[mode]->perf(); int idx = components[3].toInt(); if(idx < 1) idx = 1; if(idx >= KbPerf::DPI_COUNT) idx = KbPerf::DPI_COUNT - 1; perf->baseDpiIdx(idx); } else if(components[2] == "hwlift"){ // Mouse lift height (1...5) if(!_hwProfile || _hwProfile->modeCount() <= mode || mode >= HWMODE_MAX || !hwLoading[mode + 1]) return; KbPerf* perf = _hwProfile->modes()[mode]->perf(); perf->liftHeight((KbPerf::height)components[3].toInt()); } else if(components[3] == "hwsnap"){ // Mouse angle snapping ("on" or "off") if(!_hwProfile || _hwProfile->modeCount() <= mode || mode >= HWMODE_MAX || !hwLoading[mode + 1]) return; KbPerf* perf = _hwProfile->modes()[mode]->perf(); perf->angleSnap(components[3] == "on"); } } else if(components[0] == "fwupdate"){ // Firmware update progress if(components.count() < 3) return; // Make sure path is the same if(components[1] != fwUpdPath) return; QString res = components[2]; if(res == "invalid" || res == "fail") emit fwUpdateFinished(false); else if(res == "ok") emit fwUpdateFinished(true); else { // "xx/yy" indicates progress if(!res.contains("/")) return; QStringList numbers = res.split("/"); emit fwUpdateProgress(numbers[0].toInt(), numbers[1].toInt()); } } } KeyMap Kb::getKeyMap(){ return KeyMap(_model, _layout); } void Kb::setCurrentProfile(KbProfile* profile){ while(profile->modeCount() < hwModeCount) profile->append(new KbMode(this, getKeyMap())); emit profileAboutToChange(); _currentProfile = profile; emit profileChanged(); // Hack to prevent crash when switching to HW mode on first start with no config file. // It happens when called by KbWidget::on_profileBox_activated(). // The KbWidget event will re-call this after the currentMode has properly been set. if(profile->currentMode()) setCurrentMode(profile->currentMode()); } void Kb::setCurrentMode(KbMode* mode){ _currentProfile->currentMode(_currentMode = mode); _needsSave = true; if(features.contains("battery") && this->currentPerf()) connect(this, &Kb::batteryChangedLed, this->currentPerf(), &KbPerf::setBattery); emit modeChanged(); mode->light()->forceFrameUpdate(); } KbProfile* Kb::newProfileWithBlankMode(){ KbProfile* p = new KbProfile(this, getKeyMap()); KbMode* m = newMode(); p->append(m); p->currentMode(m); return p; } KeyMap::Layout Kb::getCurrentLayout(){ return _layout; } void Kb::setPollRate(const QString& poll){ cmd.write(QString("\npollrate %1\n").arg(poll).toLatin1()); }