diff --git a/README.md b/README.md index 10de341..354ca9c 100644 --- a/README.md +++ b/README.md @@ -188,13 +188,18 @@ If you're using **Unity** and the tray icon doesn't appear correctly, run `sudo #### OS X -- **“ckb.pkg” can’t be opened because it is from an unidentified developer.** -- Open `System Preferences > Security & Privacy > General` and click `Open Anyway`. -- **Modifier keys (Shift, Ctrl, etc.) are not rebound correctly.** -- ckb does not recognize modifier keys rebound from System Preferences. You can rebind them again within the application. -- **`~` key prints `§±`** -- Check your keyboard layout on ckb's Settings screen. Choose the layout that matches your physical keyboard. -- **Compile problems** can usually be resolved by rebooting your computer and/or reinstalling Qt. Make sure that Xcode works on its own. If a compile fails, delete the `ckb-master` directory as well as any automatically generated `build-ckb` folders and try again from a new download. +- **“ckb.pkg” can’t be opened because it is from an unidentified developer** + Open `System Preferences > Security & Privacy > General` and click `Open Anyway`. +- **Modifier keys (Shift, Ctrl, etc.) are not rebound correctly** + ckb does not recognize modifier keys rebound from System Preferences. You can rebind them again within the application. +- **`~` key prints `§±`** + Check your keyboard layout on ckb's Settings screen. Choose the layout that matches your physical keyboard. +- **Compile problems** + Can usually be resolved by rebooting your computer and/or reinstalling Qt. Make sure that Xcode works on its own. If a compile fails, delete the `ckb-master` directory as well as any automatically generated `build-ckb` folders and try again from a new download. +- **Scroll wheel does not scroll** + As of #c3474d2 it's now possible to **disable scroll acceleration** from the GUI. You can access it under "OSX tweaks" in the "More settings" screen. Once disabled, the scroll wheel should behave consistently. + + #### General diff --git a/VERSION b/VERSION index cd048df..27b0395 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -beta-v0.2.6 +beta-v0.2.6+t04 diff --git a/qmake-auto b/qmake-auto index a5a1d32..5c91548 100755 --- a/qmake-auto +++ b/qmake-auto @@ -11,7 +11,7 @@ if [[ "$QMAKE" != "" ]]; then fi if [[ "$OSTYPE" == "darwin"* ]]; then # OSX: Look for qmake in standard install locations - QMAKE=`find ~/Qt/*/clang_64/bin ~/Qt5*/*/clang_64/bin ~/Applications/Qt/*/clang_64/bin ~/Applications/Qt5*/*/clang_64/bin /Applications/Qt/*/clang_64/bin /Applications/Qt5*/*/clang_64/bin -name qmake -print -quit 2>/dev/null` + QMAKE=`find /usr/local/opt/qt5/bin ~/Qt/*/clang_64/bin ~/Qt5*/*/clang_64/bin ~/Applications/Qt/*/clang_64/bin ~/Applications/Qt5*/*/clang_64/bin /Applications/Qt/*/clang_64/bin /Applications/Qt5*/*/clang_64/bin -name qmake -print -quit 2>/dev/null` [[ $QMAKE == "" ]] && die else # Linux: Look for system-installed Qt5 diff --git a/src/ckb-daemon/command.c b/src/ckb-daemon/command.c index 6829e28..9acffe7 100644 --- a/src/ckb-daemon/command.c +++ b/src/ckb-daemon/command.c @@ -8,6 +8,7 @@ static const char* const cmd_strings[CMD_COUNT - 1] = { // NONE is implicit + "delay", "mode", "switch", "layout", @@ -111,7 +112,7 @@ int readcmd(usbdevice* kb, const char* line){ // Reject unrecognized commands. Reject bind or notify related commands if the keyboard doesn't have the feature enabled. if(command == NONE - || ((!HAS_FEATURES(kb, FEAT_BIND) && (command == BIND || command == UNBIND || command == REBIND || command == MACRO)) + || ((!HAS_FEATURES(kb, FEAT_BIND) && (command == BIND || command == UNBIND || command == REBIND || command == MACRO || command == DELAY)) || (!HAS_FEATURES(kb, FEAT_NOTIFY) && command == NOTIFY))){ next_loop: continue; @@ -197,6 +198,9 @@ int readcmd(usbdevice* kb, const char* line){ } continue; } + case DELAY: + kb->delay = (!strcmp (word, "on")); // independendant from parameter to handle false commands like "delay off" + continue; default:; } diff --git a/src/ckb-daemon/command.h b/src/ckb-daemon/command.h index 1c2d5b2..7ca2ed7 100644 --- a/src/ckb-daemon/command.h +++ b/src/ckb-daemon/command.h @@ -6,8 +6,9 @@ // Command operations typedef enum { // Special - handled by readcmd, no device functions - NONE = -10, - MODE = -9, CMD_FIRST = MODE, + NONE = -11, + DELAY = -10, CMD_FIRST = DELAY, + MODE = -9, SWITCH = -8, LAYOUT = -7, ACCEL = -6, diff --git a/src/ckb-daemon/device_vtable.c b/src/ckb-daemon/device_vtable.c index e13a4e4..58a4239 100644 --- a/src/ckb-daemon/device_vtable.c +++ b/src/ckb-daemon/device_vtable.c @@ -95,6 +95,7 @@ const devcmd vtable_keyboard_nonrgb = { .bind = cmd_bind, .unbind = cmd_unbind, .rebind = cmd_rebind, + .macro = cmd_macro, .dpi = cmd_macro_none, .dpisel = cmd_none, @@ -140,6 +141,7 @@ const devcmd vtable_mouse = { .bind = cmd_bind, .unbind = cmd_unbind, .rebind = cmd_rebind, + .macro = cmd_macro, .dpi = cmd_dpi, .dpisel = cmd_dpisel, diff --git a/src/ckb-daemon/input.c b/src/ckb-daemon/input.c index 8d7aeb9..78a5c39 100644 --- a/src/ckb-daemon/input.c +++ b/src/ckb-daemon/input.c @@ -5,7 +5,8 @@ int macromask(const uchar* key1, const uchar* key2){ // Scan a macro against key input. Return 0 if any of them don't match for(int i = 0; i < N_KEYBYTES_INPUT; i++){ - if((key1[i] & key2[i]) != key2[i]) + // if((key1[i] & key2[i]) != key2[i]) + if(key1[i] != key2[i]) // Changed to detect G-keys + modifiers return 0; } return 1; @@ -32,8 +33,13 @@ static void inputupdate_keys(usbdevice* kb){ macroaction* action = macro->actions + a; if(action->rel_x != 0 || action->rel_y != 0) os_mousemove(kb, action->rel_x, action->rel_y); - else + else { os_keypress(kb, action->scan, action->down); + if (kb->delay) { + if (a > 200) usleep (100); + else if (a > 20) usleep(30); + } + } } } } else { @@ -130,7 +136,6 @@ void inputupdate(usbdevice* kb){ input->rel_x = input->rel_y = 0; } // Finish up - os_isync(kb); memcpy(input->prevkeys, input->keys, N_KEYBYTES_INPUT); } diff --git a/src/ckb-daemon/input.h b/src/ckb-daemon/input.h index 9544744..7625df5 100644 --- a/src/ckb-daemon/input.h +++ b/src/ckb-daemon/input.h @@ -43,8 +43,6 @@ void cmd_macro(usbdevice* kb, usbmode* mode, const int notifynumber, const char* void os_keypress(usbdevice* kb, int scancode, int down); // Generate mouse movement void os_mousemove(usbdevice* kb, int x, int y); -// Synchronize input (called after sending key presses) -void os_isync(usbdevice* kb); // Perform OS-specific setup for indicator lights. Called when the device is created. Return 0 on success. int os_setupindicators(usbdevice* kb); diff --git a/src/ckb-daemon/input_linux.c b/src/ckb-daemon/input_linux.c index 20598ba..b42347e 100644 --- a/src/ckb-daemon/input_linux.c +++ b/src/ckb-daemon/input_linux.c @@ -97,6 +97,18 @@ void os_inputclose(usbdevice* kb){ kb->uinput_mouse = 0; } +// Generate SYN reports to synchronize device +static void isync(usbdevice* kb){ + struct input_event event; + memset(&event, 0, sizeof(event)); + event.type = EV_SYN; + event.code = SYN_REPORT; + if(write(kb->uinput_kb - 1, &event, sizeof(event)) <= 0) + ckb_warn("uinput write failed: %s\n", strerror(errno)); + if(write(kb->uinput_mouse - 1, &event, sizeof(event)) <= 0) + ckb_warn("uinput write failed: %s\n", strerror(errno)); +} + void os_keypress(usbdevice* kb, int scancode, int down){ struct input_event event; memset(&event, 0, sizeof(event)); @@ -118,6 +130,8 @@ void os_keypress(usbdevice* kb, int scancode, int down){ } if(write((is_mouse ? kb->uinput_mouse : kb->uinput_kb) - 1, &event, sizeof(event)) <= 0) ckb_warn("uinput write failed: %s\n", strerror(errno)); + else + isync(kb); } void os_mousemove(usbdevice* kb, int x, int y){ @@ -129,26 +143,19 @@ void os_mousemove(usbdevice* kb, int x, int y){ event.value = x; if(write(kb->uinput_mouse - 1, &event, sizeof(event)) <= 0) ckb_warn("uinput write failed: %s\n", strerror(errno)); + else + isync(kb); } if(y != 0){ event.code = REL_Y; event.value = y; if(write(kb->uinput_mouse - 1, &event, sizeof(event)) <= 0) ckb_warn("uinput write failed: %s\n", strerror(errno)); + else + isync(kb); } } -void os_isync(usbdevice* kb){ - struct input_event event; - memset(&event, 0, sizeof(event)); - event.type = EV_SYN; - event.code = SYN_REPORT; - if(write(kb->uinput_kb - 1, &event, sizeof(event)) <= 0) - ckb_warn("uinput write failed: %s\n", strerror(errno)); - if(write(kb->uinput_mouse - 1, &event, sizeof(event)) <= 0) - ckb_warn("uinput write failed: %s\n", strerror(errno)); -} - void* _ledthread(void* ctx){ usbdevice* kb = ctx; uchar ileds = 0; diff --git a/src/ckb-daemon/input_mac.c b/src/ckb-daemon/input_mac.c index f82f1ab..91e454a 100644 --- a/src/ckb-daemon/input_mac.c +++ b/src/ckb-daemon/input_mac.c @@ -402,10 +402,6 @@ void os_mousemove(usbdevice* kb, int x, int y){ postevent_mm(kb->event, x, y, HAS_FEATURES(kb, FEAT_MOUSEACCEL), kb->mousestate); } -void os_isync(usbdevice* kb){ - // OSX doesn't have any equivalent to the SYN_ events -} - int os_setupindicators(usbdevice* kb){ // Set NumLock on permanently kb->hw_ileds = kb->hw_ileds_old = kb->ileds = 1; diff --git a/src/ckb-daemon/structures.h b/src/ckb-daemon/structures.h index 4d17731..0a284e5 100644 --- a/src/ckb-daemon/structures.h +++ b/src/ckb-daemon/structures.h @@ -246,6 +246,8 @@ typedef struct { uchar hw_ileds, hw_ileds_old, ileds; // Color dithering in use char dither; + // Flag to check, if large macros should be sent delayed + char delay; } usbdevice; #endif // STRUCTURES_H diff --git a/src/ckb-ripple/main.c b/src/ckb-ripple/main.c index 1388ad3..c61258f 100644 --- a/src/ckb-ripple/main.c +++ b/src/ckb-ripple/main.c @@ -1,5 +1,6 @@ #include "../ckb/ckb-anim.h" #include +#include void ckb_info(){ // Plugin info @@ -14,6 +15,7 @@ void ckb_info(){ CKB_PARAM_AGRADIENT("color", "Ripple color:", "", "ffffffff"); CKB_PARAM_DOUBLE("length", "Ring length:", "%", 100, 1, 100); CKB_PARAM_BOOL("symmetric", "Symmetric", 0); + CKB_PARAM_BOOL("randomize", "Randomly select from gradient", 0); // Timing/input parameters CKB_KPMODE(CKB_KP_POSITION); @@ -44,11 +46,12 @@ void ckb_info(){ float kbsize = 0.f; ckb_gradient animcolor = { 0 }; -int symmetric = 0, kprelease = 0; +int symmetric = 0, kprelease = 0, randomize = 0; double animlength = 0.; void ckb_init(ckb_runctx* context){ kbsize = sqrt(context->width * context->width / 4.f + context->height * context->height / 4.f); + srand((unsigned)time(NULL)); } void ckb_parameter(ckb_runctx* context, const char* name, const char* value){ @@ -61,6 +64,7 @@ void ckb_parameter(ckb_runctx* context, const char* name, const char* value){ } CKB_PARSE_BOOL("symmetric", &symmetric){} CKB_PARSE_BOOL("kprelease", &kprelease){} + CKB_PARSE_BOOL("randomize", &randomize){} } #define ANIM_MAX (144 * 2) @@ -69,6 +73,7 @@ struct { float x, y; float maxsize; float cursize; + float choice; } anim[ANIM_MAX] = { }; void anim_add(float x, float y, float width, float height){ @@ -82,6 +87,7 @@ void anim_add(float x, float y, float width, float height){ float sizey = fmax(y, height - y); anim[i].maxsize = sqrt(sizex * sizex + sizey * sizey) + animlength; anim[i].cursize = (symmetric) ? -animlength : 0; + anim[i].choice = (float)rand()/(float)(RAND_MAX); return; } } @@ -140,10 +146,12 @@ int ckb_frame(ckb_runctx* context){ if(distance > 1.f && distance <= 1.005f) // Round values close to 1 distance = 1.f; + // Blend color gradient according to position if(distance >= 0. && distance <= 1.f){ float a, r, g, b; - ckb_grad_color(&a, &r, &g, &b, &animcolor, distance * 100.); + float gradChoice = randomize ? anim[i].choice : distance; + ckb_grad_color(&a, &r, &g, &b, &animcolor, gradChoice * 100.); ckb_alpha_blend(key, a, r, g, b); } } diff --git a/src/ckb/ckb.pro b/src/ckb/ckb.pro index a4d339e..1a6ea11 100644 --- a/src/ckb/ckb.pro +++ b/src/ckb/ckb.pro @@ -100,7 +100,8 @@ SOURCES += main.cpp\ layoutdialog.cpp \ extrasettingswidget.cpp \ kbmanager.cpp \ - colormap.cpp + colormap.cpp \ + macroreader.cpp HEADERS += mainwindow.h \ kbwidget.h \ @@ -157,7 +158,8 @@ HEADERS += mainwindow.h \ layoutdialog.h \ extrasettingswidget.h \ kbmanager.h \ - colormap.h + colormap.h \ + macroreader.h FORMS += mainwindow.ui \ kbwidget.ui \ diff --git a/src/ckb/extrasettingswidget.cpp b/src/ckb/extrasettingswidget.cpp index b1ea88c..536142a 100644 --- a/src/ckb/extrasettingswidget.cpp +++ b/src/ckb/extrasettingswidget.cpp @@ -63,6 +63,11 @@ ExtraSettingsWidget::ExtraSettingsWidget(QWidget *parent) : // Update animation info ui->animPathLabel->setText(AnimScript::path()); on_animScanButton_clicked(); + + /// Read Macro Delay setting, update UI and Kb-internal flag + bool macroDelay = settings.value("MacroDelay").toBool(); + Kb::macroDelay(macroDelay); + ui->delayBox->setChecked(macroDelay); } ExtraSettingsWidget::~ExtraSettingsWidget(){ @@ -134,3 +139,8 @@ void ExtraSettingsWidget::on_sSpeedBox_valueChanged(int arg1){ CkbSettings::set("Program/ScrollSpeed", arg1); Kb::scrollSpeed(ui->sAccelBox->isChecked() ? arg1 : 0); } + +void ExtraSettingsWidget::on_delayBox_clicked(bool checked) { + CkbSettings::set("Program/MacroDelay", checked); + Kb::macroDelay(checked); +} diff --git a/src/ckb/extrasettingswidget.h b/src/ckb/extrasettingswidget.h index d5deba6..c6ad936 100644 --- a/src/ckb/extrasettingswidget.h +++ b/src/ckb/extrasettingswidget.h @@ -27,6 +27,7 @@ private slots: void on_mAccelBox_clicked(bool checked); void on_sAccelBox_clicked(bool checked); void on_sSpeedBox_valueChanged(int arg1); + void on_delayBox_clicked(bool checked); private: Ui::ExtraSettingsWidget *ui; diff --git a/src/ckb/extrasettingswidget.ui b/src/ckb/extrasettingswidget.ui index d437df1..b7cf93a 100644 --- a/src/ckb/extrasettingswidget.ui +++ b/src/ckb/extrasettingswidget.ui @@ -406,6 +406,16 @@ + + + + When using macros with strings longer than 25 chars, some OS may lose characters (e.g. Mint 17.2). Select to prevent that bug. + + + Use delay for very long macros + + + diff --git a/src/ckb/kb.cpp b/src/ckb/kb.cpp index 53e5719..22a8b98 100644 --- a/src/ckb/kb.cpp +++ b/src/ckb/kb.cpp @@ -14,15 +14,15 @@ static QMutex notifyPathMutex; int Kb::_frameRate = 30, Kb::_scrollSpeed = 0; KeyMap::Layout Kb::_layout = KeyMap::NO_LAYOUT; -bool Kb::_dither = false, Kb::_mouseAccel = true; +bool Kb::_dither = false, Kb::_mouseAccel = true, Kb::_delay = false; Kb::Kb(QObject *parent, const QString& path) : QThread(parent), features("N/A"), firmware("N/A"), pollrate("N/A"), monochrome(false), - devpath(path), cmdpath(path + "/cmd"), notifyPath(path + "/notify1"), + devpath(path), cmdpath(path + "/cmd"), notifyPath(path + "/notify1"), macroPath(path + "/notify2"), _currentProfile(0), _currentMode(0), _model(KeyMap::NO_MODEL), lastAutoSave(QDateTime::currentMSecsSinceEpoch()), _hwProfile(0), prevProfile(0), prevMode(0), - cmd(cmdpath), notifyNumber(1), _needsSave(false) + cmd(cmdpath), notifyNumber(1), macroNumber(2), _needsSave(false) { memset(iState, 0, sizeof(iState)); memset(hwLoading, 0, sizeof(hwLoading)); @@ -94,9 +94,25 @@ Kb::Kb(QObject *parent, const QString& path) : } cmd.write(QString("notifyon %1\n").arg(notifyNumber).toLatin1()); cmd.flush(); + + // 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()); + cmd.write(QString("\ndelay %1\n").arg(_delay? "on" : "off").toLatin1()); #ifdef Q_OS_MACX // Write ANSI/ISO flag to daemon (OSX only) cmd.write("layout "); @@ -125,6 +141,12 @@ Kb::Kb(QObject *parent, const QString& path) : Kb::~Kb(){ // Save settings first save(); + + // 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); if(!isOpen()){ @@ -757,3 +779,16 @@ void Kb::setCurrentMode(KbProfile* profile, KbMode* mode, bool spontaneous){ } } +//// +/// \brief Kb::macroDelay handles the UI-Element macroBox. +/// Sends a command to the keyboard to switch on or off the delay function on very large macros +/// \param flag true: Switch on delay function, else switch off +/// +void Kb::macroDelay(bool flag) { + _delay = flag; + + foreach(Kb* kb, activeDevices){ + kb->cmd.write(QString("\ndelay %1\n").arg(flag? "on" : "off").toLatin1()); + } +} + diff --git a/src/ckb/kb.h b/src/ckb/kb.h index 17acf14..f98f77c 100644 --- a/src/ckb/kb.h +++ b/src/ckb/kb.h @@ -32,6 +32,9 @@ class Kb : public QThread // Whether dithering is used (all devices) static inline bool dither() { return _dither; } static void dither(bool newDither); + // Macro Delay setting + static inline bool macroDelay() { return _delay; } + static void macroDelay(bool flag); // OSX: mouse acceleration toggle (all devices) static inline bool mouseAccel() { return _mouseAccel; } static void mouseAccel(bool newAccel); @@ -83,6 +86,25 @@ class Kb : public QThread void hwSave(); + ////////// + /// For usage with macro definions, these two params must only be readable. + /// So there are no setters. + /// \brief getMacroNumber returns the macroNumber, which we have saved in the constructor. + /// For usage with macro definions, this param must only be readable. + /// So there is no setter. + /// \return The Number is returned as int. + /// + inline int getMacroNumber () { return macroNumber; } + + /// + /// \brief getMacroPath returns the macroPath (e.g. /dev/input/ckb1/notify), + /// which we have saved in the constructor. + /// For usage with macro definions, this param must only be readable. + /// So there is no setter. + /// \return The absolute path as String + /// + inline QString getMacroPath () { return macroPath; } + ~Kb(); signals: @@ -124,8 +146,13 @@ private slots: inline bool isOpen() const { return cmd.isOpen(); } - // File paths - QString devpath, cmdpath, notifyPath; + ////////// + /// \brief pathVars + /// devpath is the device root path (e.g. /dev/device/ckb1), + /// cmdpath leads to the daemon input pipe for daemon commands, + /// notifyPath is the standard input monitor for general purpose, + /// macroPath added for a second thread to read macro input. + QString devpath, cmdpath, notifyPath, macroPath; // Is this the keyboard at the given serial/path? inline bool matches(const QString& path, const QString& serial) { return path.trimmed() == devpath.trimmed() && usbSerial == serial.trimmed().toUpper(); } @@ -162,8 +189,13 @@ private slots: // cmd and notify file handles QFile cmd; - // Notification number + + /// \brief notifyNumber is the trailing number in the device path. int notifyNumber; + // Macro Numer to notify macro definition events + int macroNumber; + // flag if macro delay hast to be switched on + static bool _delay; // Needs to be saved? bool _needsSave; diff --git a/src/ckb/kbbind.cpp b/src/ckb/kbbind.cpp index 6bd7d63..c1d2b1e 100644 --- a/src/ckb/kbbind.cpp +++ b/src/ckb/kbbind.cpp @@ -4,6 +4,7 @@ #include "kbbind.h" #include "kbmode.h" #include "kb.h" +#include "qdebug.h" QHash KbBind::_globalRemap; quint64 KbBind::globalRemapTime = 0; @@ -178,7 +179,12 @@ void KbBind::update(QFile& cmd, bool force){ if(!_bind.contains("ralt")) bind["ralt"] = 0; if(!_bind.contains("fn")) bind["fn"] = 0; QHashIterator i(bind); - // Write out rebound keys + + // Initialize String buffer for macro Key definitions (G-keys) + // "macro clear" is neccessary, if an older definition is unbound. + QString macros = "\nmacro clear\n"; + + // Write out rebound keys and collect infos for macro definitions while(i.hasNext()){ i.next(); QString key = i.key(); @@ -192,6 +198,13 @@ void KbBind::update(QFile& cmd, bool force){ // If the key is unbound or is a special action, unbind it cmd.write(" unbind "); cmd.write(key.toLatin1()); + // if a macro definiton for the key is given, + // add the converted string to key-buffer "macro" + if (act->isValidMacro()) { + if (act->macroContent().length() > 0) { + macros.append("macro " + key.toLatin1() + ":" + act->macroContent().toLatin1() + "\n"); + } + } } else { // Otherwise, write the binding cmd.write(" bind "); @@ -203,6 +216,45 @@ void KbBind::update(QFile& cmd, bool force){ // If win lock is enabled, unbind windows keys if(_winLock) cmd.write(" unbind lwin rwin"); + + // At last, send Macro definitions if avalilable. + // If no definitions are made, clear macro will be sent only to reset all macros, + cmd.write(macros.toLatin1()); + lastCmd = &cmd; +} + +//////// +/// \brief KbBind::getMacroNumber +/// \return number of notification channel. Use it in combination with notifyon/off-Statement +/// +int KbBind::getMacroNumber() { + return devParent()->getMacroNumber(); +} + +//////// +/// \brief KbBind::getMacroPath +/// \return Filepath of macro notification pipe. If not set, returns initial value "" +/// +QString KbBind::getMacroPath() { + return devParent()->getMacroPath(); +} + +//////// +/// \brief handleNotificationChannel sends commands to ckb-daemon for (de-) activating the notify channel. +/// Send a notify cmd to the keyboard to set or clear notification for reading macro definition. +/// The file handle for the cmd pipe is stored in lastCmd. +/// \param start If true, notification channel is opened for all keys, otherwise channel ist closed. +/// +void KbBind::handleNotificationChannel(bool start) { + if (getMacroNumber() > 0 && lastCmd) { + if (start) { + lastCmd->write (QString("\nnotifyon %1\n@%1 notify all:on\n").arg(getMacroNumber()).toLatin1()); + } else { + lastCmd->write (QString("\n@%1 notify all:off\nnotifyoff %1\n").arg(getMacroNumber()).toLatin1()); + } + lastCmd->flush(); + } else qDebug() << QString("No cmd or valid handle for notification found, macroNumber = %1, lastCmd = %2") + .arg(getMacroNumber()).arg(lastCmd? "set" : "unset"); } void KbBind::keyEvent(const QString& key, bool down){ diff --git a/src/ckb/kbbind.h b/src/ckb/kbbind.h index 6ac666d..ca10a81 100644 --- a/src/ckb/kbbind.h +++ b/src/ckb/kbbind.h @@ -37,7 +37,6 @@ class KbBind : public QObject inline bool isISO() const { return _map.isISO(); } inline bool isKeyboard() const { return _map.isKeyboard(); } inline bool isMouse() const { return _map.isMouse(); } - // Related objects KbPerf* perf(); KbLight* light(); @@ -78,6 +77,23 @@ class KbBind : public QObject void update(QFile& cmd, bool force = false); inline void setNeedsUpdate() { _needsUpdate = true; } + //////// + /// \brief KbBind::getMacroNumber + /// \return number of notification channel. Use it in combination with notifyon/off-Statement + int getMacroNumber(); + + //////// + /// \brief KbBind::getMacroPath + /// \return Filepath of macro notification pipe. If not set, returns initial value "" + QString getMacroPath(); + + //////// + /// \brief handleNotificationChannel sends commands to ckb-daemon for (de-) activating the notify channel. + /// Send a notify cmd to the keyboard to set or clear notification for reading macro definition. + /// The file handle for the cmd pipe is stored in lastCmd. + /// \param start is boolean. If true, notification channel is opened for all keys, otherwise channel ist closed. + void handleNotificationChannel(bool start); + public slots: // Callback for a keypress event. void keyEvent(const QString& key, bool down); @@ -87,7 +103,6 @@ public slots: void layoutChanged(); void updated(); - private: Kb* _devParent; inline Kb* devParent() const { return _devParent; } @@ -99,6 +114,12 @@ public slots: static quint64 globalRemapTime; quint64 lastGlobalRemapTime; + ////////// + /// \brief lastCmd is a cache-hack. + /// Because the QFile ist opened in Kb, and we need it in the macro processing functions, + /// we cache the value her in lastCmd. + QFile* lastCmd; + KeyMap _map; // Key -> action map (no entry = default action) QHash _bind; @@ -106,7 +127,6 @@ public slots: bool _winLock; bool _needsUpdate; bool _needsSave; - friend class KeyAction; }; diff --git a/src/ckb/kbbindwidget.ui b/src/ckb/kbbindwidget.ui index 6c35453..797410f 100644 --- a/src/ckb/kbbindwidget.ui +++ b/src/ckb/kbbindwidget.ui @@ -241,7 +241,6 @@ label line - verticalSpacer label_2 line_2 widget diff --git a/src/ckb/kbwidget.ui b/src/ckb/kbwidget.ui index b54fa1f..2cd9c87 100644 --- a/src/ckb/kbwidget.ui +++ b/src/ckb/kbwidget.ui @@ -131,19 +131,6 @@ - - - - - 0 - 0 - - - - Tip: Drag+drop items to reorder. Right-click for menu. - - - diff --git a/src/ckb/keyaction.cpp b/src/ckb/keyaction.cpp index 130bd18..0e3ce38 100644 --- a/src/ckb/keyaction.cpp +++ b/src/ckb/keyaction.cpp @@ -1,3 +1,4 @@ +#include #include "keyaction.h" #include "kb.h" #include "kbanim.h" @@ -42,7 +43,9 @@ KeyAction::~KeyAction(){ QString KeyAction::defaultAction(const QString& key){ // G1-G18 are unbound by default - if(key.length() >= 2 && key[0] == 'g' && key[1] >= '0' && key[1] <= '9') + if(key.length() >= 2 && key[0] == 'g' + && ((key.length() == 2 && key[1] >= '0' && key[1] <= '9') + || (key.length() == 3 && key[1] == '1' && key[2] >= '0' && key[2] <= '8'))) return ""; // So are thumbgrid buttons if(key.startsWith("thumb")) @@ -137,6 +140,8 @@ QString KeyAction::friendlyName(const KeyMap& map) const { return "Start animation"; } else if(prefix == "$program"){ return "Launch program"; + } else if(prefix == "$macro"){ + return "Send G-key macro"; } return "(Unknown)"; } @@ -419,9 +424,24 @@ void KeyAction::keyEvent(KbBind* bind, bool down){ else relProgram = process; } + } else if (prefix == "$macro") { + // Do nothing, because all work is done by the keyboard itself. + // For now, there is no reason to react on G-key press or release. + // If u find some reason, then here is the place for it. } } +/// \brief KeyAction::macroDisplay is just for debugging. +/// It shows the content of the key action and some other info. +/// +void KeyAction::macroDisplay() { + qDebug() << "isMacro returns" << (isMacro() ? "true" : "false"); + qDebug() << "isValidMacro returns" << (isValidMacro() ? "true" : "false"); + QStringList ret =_value.split(":"); + qDebug() << "Macro definition conains" << ret.count() << "elements"; + qDebug() << "Macro definition is" << _value; +} + void KeyAction::adjustDisplay(){ #ifdef USE_LIBX11 // Try to get the current display from the X server @@ -472,3 +492,15 @@ void KeyAction::adjustDisplay(){ XCloseDisplay(display); #endif } + +////////// +/// \brief KeyAction::macroAction is called when applying changes on a macro definition. +/// macroAction ist called while being in the macro pane +/// and clicking Apply with something in the Macro Text Box. +/// It tags that input with "$macro:" for further recognition. +/// \param macroDef holds the String containing parts 2-4 of a complete macro definition. +/// \return QString holding the complete G-Key macro definition (parts 1-4) +/// +QString KeyAction::macroAction(QString macroDef) { + return QString ("$macro:%1").arg(macroDef); +} diff --git a/src/ckb/keyaction.h b/src/ckb/keyaction.h index 6a77f35..82c4d82 100644 --- a/src/ckb/keyaction.h +++ b/src/ckb/keyaction.h @@ -31,6 +31,72 @@ class KeyAction : public QObject // Name to send to driver (empty string for unbind) QString driverName() const; + ////////// + /// \brief macroFullLine + /// If a macro command and a macro definition exists for the given key, + /// returns the complete string except the leading "$" + /// (the $ may confuse some caller). + /// \return QString + /// All 4 parts are returned in one QString. + /// If no definition exists, return "" + /// + inline QString macroFullLine() const { + return isMacro() ? _value.right(_value.length()-1) : ""; + } + + ////////// + /// \brief isValidMacro checks whether a keyAction contains a valid macro. + /// This is done easily: If the macro action starts with $macro: + /// and has four elements, delimited by ":", we may assume, + /// that is a structural correct macro action. + /// \return bool as true iff the macro definition contains all four elements. + /// + inline bool isValidMacro() const { + if (isMacro()) { + QStringList ret; + ret =_value.split(":"); + return (ret.count() == 4); + } else { + return false; + } + } + + ////////// + /// \brief macroLine returns all interresting content for a macro definition. + /// \return QStringList returns the Macro Key Definition, + /// Readble Macro String and + /// Readable Macro Comment as QStringList. + /// + inline QStringList macroLine() const { + if (isValidMacro()) { + QStringList ret =_value.split(":"); + ret.removeFirst(); + return ret; + } else return QStringList(); + } + + ////////// + /// \brief macroContent returns the macro key definition only + /// (the second part of the macro action). + /// \return QString macroContent + /// + inline QString macroContent() const { + return isValidMacro() ? _value.split(":")[1] : ""; + } + + ////////// + /// \brief Debug output for invalid macro Definitions + /// + /// General Info on KeyAction::_value for macros: + /// That string consists of 4 elements, all delimited by ":". + /// 1. Macro command indicator "$macro:" + /// 2. Macro Key Definition (coming from pteMacroBox): + /// This sequence will program the keyboard and is hardly readable + /// 3. Readable Macro String: This is displayed in pteMacroText + /// 4. Readable Macro Comment:This is displayed in pteMacroComment + /// + void macroDisplay(); + // Mode-switch action. // 0 for first mode, 1 for second, etc. Constants below for movement options const static int MODE_PREV = -2, MODE_NEXT = -1; @@ -53,6 +119,7 @@ class KeyAction : public QObject static QString programAction(const QString& onPress, const QString& onRelease, int stop); // Key to start an animation static QString animAction(const QUuid& guid, bool onlyOnce, bool stopOnRelease); + static QString macroAction(QString macroDef); ///< \brief well documented in cpp file // Action type enum Type { @@ -66,9 +133,10 @@ class KeyAction : public QObject inline bool isSpecial() const { return type() == SPECIAL; } // Media is a type of normal key inline bool isMedia() const { return _value == "mute" || _value == "volup" || _value == "voldn" || _value == "stop" || _value == "prev" || _value == "play" || _value == "next"; } - // Program and animation are types of special key + // Macro, program and animation are types of special key inline bool isProgram() const { return _value.startsWith("$program:"); } inline bool isAnim() const { return _value.startsWith("$anim:"); } + inline bool isMacro() const { return _value.startsWith("$macro:"); } // Mouse is some normal keys plus DPI inline bool isDPI() const { return _value.startsWith("$dpi:"); } inline bool isMouse() const { return (isNormal() && (_value.startsWith("mouse") || _value.startsWith("wheel"))) || isDPI(); } diff --git a/src/ckb/macroreader.cpp b/src/ckb/macroreader.cpp new file mode 100644 index 0000000..a2f38d2 --- /dev/null +++ b/src/ckb/macroreader.cpp @@ -0,0 +1,74 @@ +#include +#include "macroreader.h" + +////////// +/// \class MacroReader +/// + +MacroReader::MacroReader() { + qDebug() << "Calling MacroReader without params is not allowed."; +} + +MacroReader::MacroReader(int macroNumber, QString macroPath, QPlainTextEdit* macBox, QPlainTextEdit* macText) { + startWorkInAThread(macroNumber, macroPath, macBox, macText); +} +MacroReader::~MacroReader() {} + +void MacroReader::startWorkInAThread(int macroNumber, QString macroPath, QPlainTextEdit* macBox, QPlainTextEdit* macText) { + macroReaderThread = new MacroReaderThread(macroNumber, macroPath, macBox, macText); + connect(macroReaderThread, &MacroReaderThread::finished, macroReaderThread, &QObject::deleteLater); + macroReaderThread->start(); +} + +////////// +/// \class MacroReaderThread +/// +void MacroReaderThread::readMacro(QString line) { + /// \detail We want to see the keys as they appear in the macroText Widget. + /// + /// Because it is possible to change the Focus via keyboard, + /// we must set the focus on each call. + macroText->setFocus(); + QTextCursor c = macroText->textCursor(); + c.setPosition(macroText->toPlainText().length()); + macroText->setTextCursor(c); + macroBox->appendPlainText(line.left(line.size()-1)); +} + +////////// +/// \brief MacroReaderThread::run is the standard main function for a thread. +/// Tries to open a file several times +/// (in this case, it should be possible the first time. (The code was recycled from kb.cpp). +/// +/// While the file is open, read lines an signal them via metaObject() to the main thread. +/// When the file is closed by the sender, close it as reader and terminate. +/// +void MacroReaderThread::run() { + qDebug() << "MacroReader::run() started with" << macroNumber << "and" << macroPath << "and" << macroBox << "and" << macroText; + + QFile macroFile(macroPath); + // Wait a small amount of time for the node to open (100ms) (code reused from kb.cpp) + QThread::usleep(100000); + if(!macroFile.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(macroFile.open(QIODevice::ReadOnly)) + break; + QThread::sleep(1); + } + if(!macroFile.isOpen()) { + qDebug() << QString("unable to open macroFile (%1)").arg(macroPath); + return; + } + } + // Read data from notification node macroPath + QByteArray line; + while(macroFile.isOpen() && (line = macroFile.readLine()).length() > 0){ + QString text = QString::fromUtf8(line); + metaObject()->invokeMethod(this, "readMacro", Qt::QueuedConnection, Q_ARG(QString, text)); + } + qDebug() << "MacroReader::run() ends."; + macroFile.close(); + QThread::exit (); +} diff --git a/src/ckb/macroreader.h b/src/ckb/macroreader.h new file mode 100644 index 0000000..6b8299b --- /dev/null +++ b/src/ckb/macroreader.h @@ -0,0 +1,126 @@ +#ifndef MACROREADER_H +#define MACROREADER_H + +//#include +#include +#include +#include +#include + +////////// +/// \brief The MacroReaderThread class is responsible for reading Macro Key Values. +/// It is created as a separate thread (worker thread) for reading macro commands from an fresh opened notify channel. +/// Standard notify channel for macro definitions is number 2. +/// +/// While the worker Thread gets input from the keyboard, +/// the lines are sent via signalling (metaobject) to run a member function in the context of the Qt UI manager. +/// +/// When the notify channel is closed (that's normally done by pressing "Stop"-Button in the UI), +/// the worker thread closes the channelFile and leaves. +/// \sa MacroReaderThread(), ~MacroReaderThread(), readMaco(), run() +/// +class MacroReaderThread : public QThread +{ + Q_OBJECT + + ////////// + /// \brief macroNumber + /// Filenames of nofity channels have the structure /ckb1/notify + /// First part is hold in macroPath, the number is hold in macroNumber. + /// macroNumber may range from 0 to 9. + int macroNumber; + + ////////// + /// \brief macroPath holds the path for the notify channel + /// \see macroNumber + QString macroPath; + + ////////// + /// \brief macroBox will receive the Macro Key Values sent from the keyboard while defining a new macro. + QPlainTextEdit* macroBox; + + ////////// + /// \brief macroText is the other textpane used in the UI while typing new macros. + /// That variable is used for setting the focus to that textpane and to set the cursor at EOT. + QPlainTextEdit* macroText; + +public: + ////////// + /// \brief MacroReaderThread saves the four params to local vars with similar varNames. + /// \param macNum + /// \param macPath + /// \param macBox + /// \param macText + /// + MacroReaderThread(int macNum, QString macPath, QPlainTextEdit* macBox, QPlainTextEdit* macText) { + macroNumber = macNum; + macroPath = macPath; + macroBox = macBox; + macroText = macText; + } + + ////////// + /// \brief run is the notification reader main loop. + void run () Q_DECL_OVERRIDE; + +private slots: + ////////// + /// \brief readMacro is called for each line received by the worker thread. + /// The method ist called via signal (metaObject) from the worker thread, + /// which reads the keyboard input. + /// Just display the key code in the macroBox Widget without he trailing newline + /// and reposition the cursor in the macro pane. + /// + /// This is used, because the worker thread shouldn't get access to the UI elements + /// (and normally has none, because the pointers macroBox and macroText remain on the stack). + /// That mechanism guarantees, that the UI does not freeze if it happens something magic to the reading function. + /// + /// \param line holds the line just got from keyboard + + void readMacro(QString line); +}; + +////////// +/// \brief The MacroReader class creates a worker thread object. +/// It does a connect do delayed deletion of thread local variables in the case the worker thread terminates. +/// +class MacroReader : public QThread +{ + Q_OBJECT + +public: + ////////// + /// \brief MacroReader Calling MacroReader without params is not allowed. + /// + MacroReader(); + ////////// + /// \brief MacroReader This is the only allowed constructor. + /// It only calls startWorkInAThread() with the four params. + /// \param macroNumber + /// \param macroPath + /// \param macBox + /// \param macText + /// + MacroReader(int macroNumber, QString macroPath, QPlainTextEdit* macBox, QPlainTextEdit* macText); + ~MacroReader(); + + ////////// + /// \brief startWorkInAThread This member function creates the new thread object and starts it with the four params. + /// \param macroNumber + /// \param macroPath + /// \param macBox + /// \param macText + /// + void startWorkInAThread(int macroNumber, QString macroPath, QPlainTextEdit* macBox, QPlainTextEdit* macText); + +signals: + +public slots: + +private slots: + +private: + MacroReaderThread* macroReaderThread; +}; + +#endif // MACROREADER_H diff --git a/src/ckb/main.cpp b/src/ckb/main.cpp index 98cb2c1..a9febd9 100644 --- a/src/ckb/main.cpp +++ b/src/ckb/main.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -19,6 +20,74 @@ extern "C" void disableAppNap(); #define SHMEM_SIZE 128 * 1024 #define SHMEM_SIZE_V015 16 +// Command line options +enum CommandLineParseResults { + CommandLineOK, + CommandLineError, + CommandLineVersionRequested, + CommandLineHelpRequested, + CommandLineClose, + CommandLineBackground +}; + +/** + * parseCommandLine - Setup options and parse command line arguments. + * + * @param parser parser instance to use for parse the arguments + * @param errorMessage argument parse error message + * + * @return integer, representing the requested argument + */ +CommandLineParseResults parseCommandLine(QCommandLineParser &parser, QString *errorMessage) { + // setup parser to interpret -abc as -a -b -c + parser.setSingleDashWordOptionMode(QCommandLineParser::ParseAsCompactedShortOptions); + + /* add command line options */ + // add -v, --version + const QCommandLineOption versionOption = parser.addVersionOption(); + // add -h, --help (/? on windows) + const QCommandLineOption helpOption = parser.addHelpOption(); + // add -b, --background + const QCommandLineOption backgroundOption(QStringList() << "b" << "background", + "Starts in background, without displaying the main window."); + parser.addOption(backgroundOption); + // add -c, --close + const QCommandLineOption closeOption(QStringList() << "c" << "close", + "Causes already running instance (if any) to exit."); + parser.addOption(closeOption); + + /* parse arguments */ + if (!parser.parse(QCoreApplication::arguments())) { + // set error, if there already are some + *errorMessage = parser.errorText(); + return CommandLineError; + } + + /* return requested operation/setup */ + if (parser.isSet(versionOption)) { + // print version and exit + return CommandLineVersionRequested; + } + + if (parser.isSet(helpOption)) { + // print help and exit + return CommandLineHelpRequested; + } + + if (parser.isSet(backgroundOption)) { + // open application in background + return CommandLineBackground; + } + + if (parser.isSet(closeOption)) { + // close already running application instances, if any + return CommandLineClose; + } + + /* no explicit argument was passed */ + return CommandLineOK; +}; + // Scan shared memory for an active PID static bool pidActive(const QStringList& lines){ foreach(const QString& line, lines){ @@ -77,8 +146,19 @@ static bool isRunning(const char* command){ } int main(int argc, char *argv[]){ + // Setup main application QApplication a(argc, argv); + + // Setup names and versions QCoreApplication::setOrganizationName("ckb"); + QCoreApplication::setApplicationVersion(CKB_VERSION_STR); + QCoreApplication::setApplicationName("ckb"); + + // Setup argument parser + QCommandLineParser parser; + QString errorMessage; + parser.setApplicationDescription("Open Source Corsair Input Device Driver for Linux and OSX."); + bool background = 0; // Although the daemon runs as root, the GUI needn't and shouldn't be, as it has the potential to corrupt settings data. if(getuid() == 0){ @@ -96,23 +176,41 @@ int main(int argc, char *argv[]){ qApp->setAttribute(Qt::AA_UseHighDpiPixmaps); #endif - // If launched with --version, print version info and then quit - if(qApp->arguments().contains("--version")){ - printf("ckb %s\n", CKB_VERSION_STR); + // Parse arguments + switch (parseCommandLine(parser, &errorMessage)) { + case CommandLineOK: + // If launched with no argument + break; + case CommandLineError: + fputs(qPrintable(errorMessage), stderr); + fputs("\n\n", stderr); + fputs(qPrintable(parser.helpText()), stderr); + return 1; + case CommandLineVersionRequested: + // If launched with --version, print version info and then quit + printf("%s %s\n", qPrintable(QCoreApplication::applicationName()), + qPrintable(QCoreApplication::applicationVersion())); return 0; - } - - // Kill existing app when launched with --close - if(qApp->arguments().contains("--close")){ - if(isRunning("Close")) + case CommandLineHelpRequested: + // If launched with --help, print help and then quit + parser.showHelp(); + return 0; + case CommandLineClose: + // If launched with --close, kill existing app + if (isRunning("Close")) printf("Asking existing instance to close.\n"); else printf("ckb is not running.\n"); return 0; + case CommandLineBackground: + // If launched with --background, launch in background + background = 1; + break; } // Launch in background if requested, or if re-launching a previous session - bool background = qApp->arguments().contains("--background") || qApp->arguments().contains("-session") || qApp->arguments().contains("--session"); + if(qApp->isSessionRestored()) + background = 1; if(isRunning(background ? 0 : "Open")){ printf("ckb is already running. Exiting.\n"); return 0; diff --git a/src/ckb/quazip/zip.c b/src/ckb/quazip/zip.c index 9649653..4c9b01d 100644 --- a/src/ckb/quazip/zip.c +++ b/src/ckb/quazip/zip.c @@ -535,8 +535,8 @@ local ZPOS64_T zip64local_SearchCentralDir(const zlib_filefunc64_32_def* pzlib_f break; } - if (uPosFound!=0) - break; + if (uPosFound!=0) + break; } TRYFREE(buf); return uPosFound; diff --git a/src/ckb/rebindwidget.cpp b/src/ckb/rebindwidget.cpp index e9c12bc..ee4e5f1 100644 --- a/src/ckb/rebindwidget.cpp +++ b/src/ckb/rebindwidget.cpp @@ -1,6 +1,7 @@ #include #include "rebindwidget.h" #include "ui_rebindwidget.h" +#include // lae. static const int DPI_OFFSET = -KeyAction::DPI_UP + 1; static const int DPI_CUST_IDX = KeyAction::DPI_CUSTOM + DPI_OFFSET; @@ -8,7 +9,7 @@ static const int DPI_CUST_IDX = KeyAction::DPI_CUSTOM + DPI_OFFSET; RebindWidget::RebindWidget(QWidget *parent) : QWidget(parent), ui(new Ui::RebindWidget), - bind(0), profile(0) + bind(0), profile(0), macReader(0) { ui->setupUi(this); ui->lightWrapBox->hide(); @@ -187,6 +188,10 @@ void RebindWidget::setSelection(const QStringList& newSelection, bool applyPrevi ui->programKrModeBox->setCurrentIndex(0); ui->programKpModeBox->setEnabled(false); ui->programKrModeBox->setEnabled(false); + // Clear neu UI elements in MacroTab + ui->pteMacroBox->setPlainText(""); + ui->pteMacroText->setPlainText(""); + ui->pteMacroComment->setPlainText(""); // Fill in field and select tab according to action type bool mouse = act.isMouse(); if(mouse){ @@ -283,6 +288,16 @@ void RebindWidget::setSelection(const QStringList& newSelection, bool applyPrevi else // 0 -> "", 1 -> Prev, 2 -> Next, 3 -> Mode 1 ui->modeBox->setCurrentIndex(3); + } else if (sAction == "macro") { + ui->tabWidget->setCurrentIndex(TAB_MACRO); + if (act.isValidMacro()) { + ui->pteMacroBox->setPlainText(act.macroContent()); + ui->pteMacroText->setPlainText(act.macroLine()[1].replace("&das_IST_31N_col0n;", ":")); + ui->pteMacroComment->setPlainText(act.macroLine()[2].replace("&das_IST_31N_col0n;", ":")); + } else { + qDebug("RebindWidget::setSelection found invalid macro definition."); + act.macroDisplay(); + } } else ui->modeBox->setCurrentIndex(0); // Brightness control. Also check wrap @@ -354,11 +369,26 @@ void RebindWidget::applyChanges(const QStringList& keys, bool doUnbind){ krStop = KeyAction::PROGRAM_RE_KPSTOP; } bind->setAction(keys, KeyAction::programAction(ui->programKpBox->text(), ui->programKrBox->text(), kpStop | krStop)); + } else if (ui->pteMacroBox->toPlainText().length() > 0) { + // G-key macro handling: + // Set the macro definiton for all keys selected (indeed, it may be multiple keys). + // First, concat the Macro Key Definion and the Macro plain text + // after escaping possible colos in the parts for Macro Text and Macro Comment. + QString mac; + mac = ui->pteMacroComment->toPlainText().replace(":", "&das_IST_31N_col0n;"); + mac = ui->pteMacroText->toPlainText().replace(":", "&das_IST_31N_col0n;") + ":" + mac; + mac = ui->pteMacroBox->toPlainText() + ":" + mac; + bind->setAction(keys, KeyAction::macroAction(mac)); } else if(doUnbind) bind->noAction(keys); } void RebindWidget::on_applyButton_clicked(){ + // Normally, this should be done via signalling. + // Because there is no serarate thread, we have to call it directly + // (otherwise we could do Key char conversion step by step, + // but so it is more easy to change the key definition): + on_btnStopMacro_clicked(); applyChanges(selection, true); } @@ -400,6 +430,11 @@ void RebindWidget::setBox(QWidget* box){ ui->programKpButton->setChecked(false); ui->programKrButton->setChecked(false); } + // Clear macro panel + if (box != ui->pteMacroBox) { + ui->pteMacroBox->setPlainText(""); + helpStatus(1); + } } void RebindWidget::on_typingBox_currentIndexChanged(int index){ @@ -411,6 +446,12 @@ void RebindWidget::on_typingBox_currentIndexChanged(int index){ } } +void RebindWidget::on_pteMacroBox_textChanged() { + if (ui->pteMacroBox->toPlainText().length() > 0) { + setBox(ui->pteMacroBox); + } +} + void RebindWidget::on_modBox_currentIndexChanged(int index){ if(index == 0) ui->modButton->setChecked(false); @@ -649,3 +690,94 @@ void RebindWidget::on_animButton_clicked(bool checked){ if(checked && ui->animBox->currentIndex() == 0) ui->animBox->setCurrentIndex(1); } + +////////// +/// \brief RebindWidget::on_btnStartMacro_clicked starts macro recording. +/// A new notification channel and MacroReader are created to do the job. +/// +/// The UI is protected against false clicking +/// (e.g. if you type start and than Apply, the channel is closed in wrong order). +/// +/// At this time, all neccessary params like macroNumber, macroPath, cmdFile etc. had been cached. +/// +void RebindWidget::on_btnStartMacro_clicked() { + if (!macReader) { + bind->handleNotificationChannel(true); + macReader = new MacroReader(bind->getMacroNumber(), bind->getMacroPath(), ui->pteMacroBox, ui->pteMacroText); + // because of the second thread we need to disable three of the four bottom buttons. + // Clicking "Stop" will enable them again. + ui->applyButton->setEnabled(false); + ui->resetButton->setEnabled(false); + ui->unbindButton->setEnabled(false); + ui->btnStartMacro->setEnabled(false); + ui->btnStopMacro->setEnabled(true); + helpStatus(2); + } +} + +////////// +/// \brief RebindWidget::on_btnStopMacro_clicked ends the macro recording. +/// Notify channel ist closed, the ReaderThread is deleted if the notification is really down. +/// +/// Afterwards, the characters in the MacroBox are changed from KB-out format to cmd-in format. +/// At last the UI changes to the new state. +/// +void RebindWidget::on_btnStopMacro_clicked() { + if (macReader) { + bind->handleNotificationChannel(false); + delete macReader; + macReader = 0; + convertMacroBox(); + ui->applyButton->setEnabled(true); + ui->resetButton->setEnabled(true); + ui->unbindButton->setEnabled(true); + ui->btnStartMacro->setEnabled(true); + ui->btnStopMacro->setEnabled(false); + helpStatus(3); + } +} + +////////// +/// \brief RebindWidget::on_btnClearMacro_clicked changes the help info an the panel. +/// The job of clearing the input panels is triggerd with signal/slot via the RebindWidget.ui file. +/// +void RebindWidget::on_btnClearMacro_clicked() { + helpStatus(1); +} + +////////// +/// \brief RebindWidget::helpStatus shows a help line in the ui. +/// \param status determines what to display. +/// +void RebindWidget::helpStatus(int status) { + switch (status) { + case 1: + ui->lbl_macro->setText("Type in a macro name in the comment box and click start."); + break; + case 2: + ui->lbl_macro->setText("Type your macro and click stop when finished."); + break; + case 3: + ui->lbl_macro->setText("Click Apply or change values in Macro Key Actions in advance."); + break; + default: + ui->lbl_macro->setText(QString("Oops: Some magic in RebindWidget::helpStatus (%1)").arg(status)); + } +} + +////////// +/// \brief RebindWidget::convertMacroBox converts the macroBox content. +/// The KB sends each keypress as "key [+|-]" +/// +/// the ckb-daemon needs a shorter format, only " [+|-]" +/// +/// That function does the conversion. +/// +void RebindWidget::convertMacroBox() { + QString in; + + in = ui->pteMacroBox->toPlainText(); + in.replace (QRegExp("\n"), ","); + in.replace (QRegExp("key "), ""); + ui->pteMacroBox->setPlainText(in); +} diff --git a/src/ckb/rebindwidget.h b/src/ckb/rebindwidget.h index a7a1369..4fe8495 100644 --- a/src/ckb/rebindwidget.h +++ b/src/ckb/rebindwidget.h @@ -4,6 +4,7 @@ #include #include "kbbind.h" #include "kbprofile.h" +#include "macroreader.h" // Key rebinding widget @@ -64,15 +65,24 @@ private slots: void on_programKpSIBox_clicked(bool checked); void on_programKrSIBox_clicked(bool checked); void on_animButton_clicked(bool checked); + void on_pteMacroBox_textChanged(); + void on_btnStartMacro_clicked(); + void on_btnStopMacro_clicked(); + void on_btnClearMacro_clicked(); private: Ui::RebindWidget *ui; // Tab indices - const static int TAB_KB = 0, TAB_MOUSE = 1, TAB_ANIM = 2, TAB_SPECIAL = 3, TAB_PROGRAM = 4; + const static int TAB_KB = 0, TAB_MOUSE = 1, TAB_ANIM = 2, TAB_SPECIAL = 3, TAB_PROGRAM = 4, TAB_MACRO = 5; void setBox(QWidget* box); + // convert keyboard definion into macro definition + void convertMacroBox(); + // Show some help info + void helpStatus(int status); + KbBind* bind; KbProfile* profile; QStringList selection; @@ -85,6 +95,7 @@ private slots: QStringList mouseKeys; QStringList mouseExtKeys; QStringList wheelKeys; + MacroReader* macReader; ///< \brief macReader holds the MacroReader when macro recording starts. }; #endif // BINDDIALOG_H diff --git a/src/ckb/rebindwidget.ui b/src/ckb/rebindwidget.ui index 36efb96..f88c966 100644 --- a/src/ckb/rebindwidget.ui +++ b/src/ckb/rebindwidget.ui @@ -1229,6 +1229,217 @@ + + + Macro + + + + + 40 + 12 + 241 + 171 + + + + + + + Macro Key Actions + + + + + + + Qt::WheelFocus + + + true + + + + + + + + true + + + + 420 + 20 + 311 + 31 + + + + Qt::WheelFocus + + + Qt::NoContextMenu + + + true + + + What is this macro for? + + + 1 + + + Qt::ImhNone + + + Qt::ScrollBarAlwaysOff + + + true + + + + + + true + + + false + + + + + + 1 + + + + + + 420 + 60 + 311 + 61 + + + + Qt::StrongFocus + + + Qt::NoContextMenu + + + false + + + Qt::ImhNoAutoUppercase + + + + + + false + + + false + + + + + + 1 + + + + + + 300 + 20 + 119 + 81 + + + + + + + Macro Comment + + + + + + + Macro Text + + + + + + + + + 300 + 160 + 436 + 30 + + + + + + + Macro recording + + + + + + + false + + + Stop + + + + + + + Start + + + + + + + Clear + + + + + + + + + 300 + 130 + 431 + 27 + + + + Comment label for help + + + pteMacroComment + layoutWidget1 + layoutWidget + pteMacroText + layoutWidget_2 + lbl_macro + @@ -1281,6 +1492,140 @@ + + tabWidget + pteMacroComment + pteMacroBox + dpiButton + mb2Box + btnStartMacro + fnBox + dpiCustYBox + dpiBox + applyButton + mbBox + cancelButton + wheelBox + btnStopMacro + mbButton + dpiCustXBox + btnClearMacro + wheelButton + mb2Button + animBox + animKrBox + animOnceBox + animButton + modeButton + modeWrapBox + lightWrapBox + lockButton + lightBox + resetButton + lockBox + lightButton + modeBox + unbindButton + programKpButton + programKpBox + programKrBox + programKrButton + programKpSIBox + programKpModeBox + programKrSIBox + programKrModeBox + typingBox + numBox + pteMacroText + typingButton + mediaButton + modBox + modButton + fnButton + mediaBox + numButton + - + + + cancelButton + clicked() + pteMacroBox + clear() + + + 735 + 331 + + + 252 + 201 + + + + + btnClearMacro + clicked() + pteMacroBox + clear() + + + 702 + 206 + + + 270 + 179 + + + + + btnClearMacro + clicked() + pteMacroText + clear() + + + 718 + 206 + + + 705 + 131 + + + + + btnClearMacro + clicked() + pteMacroComment + clear() + + + 723 + 201 + + + 722 + 85 + + + + + btnClearMacro + clicked() + btnStopMacro + animateClick() + + + 671 + 208 + + + 612 + 206 + + + +