diff --git a/.gitignore b/.gitignore index aa563b8..18982f9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ C#/Dependencies/*.lib *.dll *.zip *.lib +*.ini ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. diff --git a/CHANGELOG.md b/CHANGELOG.md index 57845da..f5083e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] ### Added +- Added TabletButtons demo for converting a graphics tablet into a button box ### Changed ### Deprecated ### Removed ### Fixed -- Absolute Mode mouse movement subscriptions now work again +- Absolute Mode mouse movement subscriptions now work again ## [0.4.4] - 2019-07-09 - Added SetState to allow toggling on/off of bindings diff --git a/TabletButtonBuilder.ahk b/TabletButtonBuilder.ahk new file mode 100644 index 0000000..a15ee1b --- /dev/null +++ b/TabletButtonBuilder.ahk @@ -0,0 +1,139 @@ +/* +Script to build INI file with database of button names / coordinates for TabletButtons.ahk to read +For use with an Absolute Mode pointing device (eg a Wacom graphics tablet) + +Usage: +1) Find the VID/PID of your tablet using the Monitor.ahk demo and paste it where indicated below +2) Mark up your tablet device with some buttons (eg tape a piece of paper to it, draw some boxes on it with names) +3) Run this script +4) Click "Create Box", give it a name, draw a line from top left to bottom right of one of the boxes +5) Repeat (4) as needed +6) Click "Save" at the bottom of the GUI to savce button coordinates to the INI file +7) Close the GUI of this script, edit TabletButtons.ahk as appropriate and run it +*/ +#SingleInstance force +#include Lib\AutoHotInterception.ahk +#include TabletLib\TabletLib.ahk + +; Initialize AHI +AHI := new AutoHotInterception() +mouseId := AHI.GetMouseId(0x0B57, 0x9091) ; Get VID/PID of your device from the Monitor app and paste it in here +AHI.SubscribeMouseButton(mouseId, 0, true, Func("ButtonEvent")) +AHI.SubscribeMouseMoveAbsolute(mouseId, true, Func("OnMouseMove")) +AHI.SetState(false) + +; Build GUI +guiW := 400 +Menu, BoxMenu, Add, Delete, DeleteBoxClicked +Menu, BoxMenu, Add, Edit, EditBoxClicked +Gui, Add, Button, % "w" guiW " Center gCreateBox", Create Box +Gui, Add, Text, % "w" guiW " Center gCreateBox", Defined Boxes (Right Click to edit) +Gui, Add, ListView, % "w" guiW " h400", Name|Start X|Start Y|End X|End Y +LV_ModifyCol(1, 200) +LV_ModifyCol(2, 45) +LV_ModifyCol(3, 45) +LV_ModifyCol(4, 45) +LV_ModifyCol(5, 45) +Gui, Add, Button, % "w" guiW " Center gSave", Save to disk +Gui, Show, , Tablet button builder tool + +boxArr := {} + +; Load data from existing INI file +bA := LoadBoxes("TabletButtons.ini") +for name, box in bA { + AddBox(box) +} +return + +ButtonEvent(state){ + global boxMode, newBox + if (state && boxMode == 1){ + boxMode := 2 ; Waiting for release + } else if (!state && boxMode == 2){ + GoSub, EndBox + } +} + +OnMouseMove(x, y){ + global boxMode, newBox + if (boxMode == 1){ + newBox.StartX := x + newBox.StartY := y + } else if (boxMode == 2) { + newBox.EndX := x + newBox.EndY := y + } +} + +CreateBox: + InputBox, boxName, Create Box, Enter box name, , , 120 + if (ERRORLEVEL = 0){ + if (boxArr.HasKey(boxName)){ + msgbox % "Box " boxName " already exists" + } else { + StartCreateBox(new Box(boxName)) + } + } + return + +StartCreateBox(box){ + global AHI, boxMode, newBox + boxMode := 1 ; waiting for press + AHI.SetState(true) + newBox := box +} + +EndBox: + AHI.SetState(false) + AddBox(newBox) + boxMode := 0 + return + +GuiContextMenu: ; This is automatically called when the GUI is rightclicked + If (A_EventInfo) { ; This will be the row number selected when right-clicked, if right clicking outside the list view will be 0 (False) + rightClickedRow := A_EventInfo ; Save the selected row + Menu, BoxMenu, Show ; Show the Rightclick menu + } + Return + +DeleteBoxClicked(){ + global rightClickedRow + DeleteBox(rightClickedRow) +} + +EditBoxClicked(){ + global rightClickedRow, boxArr + newBox := boxArr[GetBoxName(rightClickedRow)].Clone() + DeleteBox(rightClickedRow) + StartCreateBox(newBox) +} + +Save(){ + global JSON, boxArr + FileDelete, TabletButtons.ini + FileAppend, % JSON.Dump(boxArr, ,true), TabletButtons.ini +} + +GuiClose: + ExitApp + +AddBox(box){ + global boxArr + ; ToDo: Validate if new box overlaps with any existing boxes + LV_Add(, box.BoxName, box.StartX, box.StartY, box.EndX, box.EndY) + boxArr[box.BoxName] := box.Clone() + LV_ModifyCol(1, "Sort") +} + +DeleteBox(rowNum){ + global boxArr + boxName := GetBoxName(rowNum) + LV_Delete(rowNum) + boxArr.Delete(boxName) +} + +GetBoxName(rowNum){ + LV_GetText(boxName, rowNum, 1) + return boxName +} \ No newline at end of file diff --git a/TabletButtons.ahk b/TabletButtons.ahk new file mode 100644 index 0000000..e1bf8f2 --- /dev/null +++ b/TabletButtons.ahk @@ -0,0 +1,45 @@ +/* +For use with an Absolute Mode pointing device (eg a Wacom graphics tablet) +Turns a Absolute Mode device into a button box + +To Use: +1) Use the TabletButtonBuilder.ahk script to build a TabletButtons.ini file containing names and coordinates for your buttons +2) Find the VID/PID of your tablet using the Monitor.ahk demo and paste it where indicated below +3) Run this script +*/ + +#SingleInstance force +#Persistent ; If you have no hotkeys or GUI in the script, you need this else the script will instantly exit +#include Lib\AutoHotInterception.ahk +#include TabletLib\TabletLib.ahk + +; Initialize AHI +AHI := new AutoHotInterception() +mouseId := AHI.GetMouseId(0x0B57, 0x9091) ; Get VID/PID of your device from the Monitor app and paste it in here +AHI.SubscribeMouseButton(mouseId, 0, true, Func("ButtonEvent")) +AHI.SubscribeMouseMoveAbsolute(mouseId, true, Func("OnMouseMove")) + +boxArr := LoadBoxes("TabletButtons.ini") +return + +ButtonEvent(state){ + global lastX, lastY, boxArr + if (state){ ; On button press... + ; Find name of box that was clicked (If any) + name := FindBoxName(lastX, lastY, boxArr) + if (name != ""){ + ; Your code here to decide what action to take depending on which box was selected + Tooltip % "You selected box " name + } + } +} + +; Store coordinates on move, so they can be used in ButtonEvent +OnMouseMove(x, y){ + global lastX, lastY + lastX := x + lastY := y +} + +^Esc:: + ExitApp diff --git a/TabletLib/JSON.ahk b/TabletLib/JSON.ahk new file mode 100644 index 0000000..c43980f --- /dev/null +++ b/TabletLib/JSON.ahk @@ -0,0 +1,361 @@ +/** + * Lib: JSON.ahk + * JSON lib for AutoHotkey. + * Version: + * v2.1.0 [updated 01/28/2016 (MM/DD/YYYY)] + * License: + * WTFPL [http://wtfpl.net/] + * Requirements: + * Latest version of AutoHotkey (v1.1+ or v2.0-a+) + * Installation: + * Use #Include JSON.ahk or copy into a function library folder and then + * use #Include + * Links: + * GitHub: - https://github.com/cocobelgica/AutoHotkey-JSON + * Forum Topic - http://goo.gl/r0zI8t + * Email: - cocobelgica gmail com + */ + + +/** + * Class: JSON + * The JSON object contains methods for parsing JSON and converting values + * to JSON. Callable - NO; Instantiable - YES; Subclassable - YES; + * Nestable(via #Include) - NO. + * Methods: + * Load() - see relevant documentation before method definition header + * Dump() - see relevant documentation before method definition header + */ +class JSON +{ + /** + * Method: Load + * Parses a JSON string into an AHK value + * Syntax: + * value := JSON.Load( text [, reviver ] ) + * Parameter(s): + * value [retval] - parsed value + * text [in, opt] - JSON formatted string + * reviver [in, opt] - function object, similar to JavaScript's + * JSON.parse() 'reviver' parameter + */ + class Load extends JSON.Functor + { + Call(self, text, reviver:="") + { + this.rev := IsObject(reviver) ? reviver : false + this.keys := this.rev ? {} : false + + static q := Chr(34) + , json_value := q . "{[01234567890-tfn" + , json_value_or_array_closing := q . "{[]01234567890-tfn" + , object_key_or_object_closing := q . "}" + + key := "" + is_key := false + root := {} + stack := [root] + next := json_value + pos := 0 + + while ((ch := SubStr(text, ++pos, 1)) != "") { + if InStr(" `t`r`n", ch) + continue + if !InStr(next, ch, 1) + this.ParseError(next, text, pos) + + holder := stack[1] + is_array := holder.IsArray + + if InStr(",:", ch) { + next := (is_key := !is_array && ch == ",") ? q : json_value + + } else if InStr("}]", ch) { + ObjRemoveAt(stack, 1) + next := stack[1]==root ? "" : stack[1].IsArray ? ",]" : ",}" + + } else { + if InStr("{[", ch) { + ; Check if Array() is overridden and if its return value has + ; the 'IsArray' property. If so, Array() will be called normally, + ; otherwise, use a custom base object for arrays + static json_array := Func("Array").IsBuiltIn || ![].IsArray ? {IsArray: true} : 0 + + ; sacrifice readability for minor(actually negligible) performance gain + (ch == "{") + ? ( is_key := true + , value := {} + , next := object_key_or_object_closing ) + ; ch == "[" + : ( value := json_array ? new json_array : [] + , next := json_value_or_array_closing ) + + ObjInsertAt(stack, 1, value) + + if (this.keys) + this.keys[value] := [] + + } else { + if (ch == q) { + i := pos + while (i := InStr(text, q,, i+1)) { + value := StrReplace(SubStr(text, pos+1, i-pos-1), "\\", "\u005c") + + static ss_end := A_AhkVersion<"2" ? 0 : -1 + if (SubStr(value, ss_end) != "\") + break + } + + if (!i) + this.ParseError("'", text, pos) + + value := StrReplace(value, "\/", "/") + , value := StrReplace(value, "\" . q, q) + , value := StrReplace(value, "\b", "`b") + , value := StrReplace(value, "\f", "`f") + , value := StrReplace(value, "\n", "`n") + , value := StrReplace(value, "\r", "`r") + , value := StrReplace(value, "\t", "`t") + + pos := i ; update pos + + i := 0 + while (i := InStr(value, "\",, i+1)) { + if !(SubStr(value, i+1, 1) == "u") + this.ParseError("\", text, pos - StrLen(SubStr(value, i+1))) + + uffff := Abs("0x" . SubStr(value, i+2, 4)) + if (A_IsUnicode || uffff < 0x100) + value := SubStr(value, 1, i-1) . Chr(uffff) . SubStr(value, i+6) + } + + if (is_key) { + key := value, next := ":" + continue + } + + } else { + value := SubStr(text, pos, i := RegExMatch(text, "[\]\},\s]|$",, pos)-pos) + + static number := "number" + if value is %number% + value += 0 + else if (value == "true" || value == "false") + value := %value% + 0 + else if (value == "null") + value := "" + else + ; we can do more here to pinpoint the actual culprit + ; but that's just too much extra work. + this.ParseError(next, text, pos, i) + + pos += i-1 + } + + next := holder==root ? "" : is_array ? ",]" : ",}" + } ; If InStr("{[", ch) { ... } else + + is_array? key := ObjPush(holder, value) : holder[key] := value + + if (this.keys && this.keys.HasKey(holder)) + this.keys[holder].Push(key) + } + + } ; while ( ... ) + + return this.rev ? this.Walk(root, "") : root[""] + } + + ParseError(expect, text, pos, len:=1) + { + static q := Chr(34) + + line := StrSplit(SubStr(text, 1, pos), "`n", "`r").Length() + col := pos - InStr(text, "`n",, -(StrLen(text)-pos+1)) + msg := Format("{1}`n`nLine:`t{2}`nCol:`t{3}`nChar:`t{4}" + , (expect == "") ? "Extra data" + : (expect == "'") ? "Unterminated string starting at" + : (expect == "\") ? "Invalid \escape" + : (expect == ":") ? "Expecting ':' delimiter" + : (expect == q) ? "Expecting object key enclosed in double quotes" + : (expect == q . "}") ? "Expecting object key enclosed in double quotes or object closing '}'" + : (expect == ",}") ? "Expecting ',' delimiter or object closing '}'" + : (expect == ",]") ? "Expecting ',' delimiter or array closing ']'" + : InStr(expect, "]") ? "Expecting JSON value or array closing ']'" + : "Expecting JSON value(string, number, true, false, null, object or array)" + , line, col, pos) + + static offset := A_AhkVersion<"2" ? -3 : -4 + throw Exception(msg, offset, SubStr(text, pos, len)) + } + + Walk(holder, key) + { + value := holder[key] + if IsObject(value) + for i, k in this.keys[value] + value[k] := this.Walk.Call(this, value, k) ; bypass __Call + + return this.rev.Call(holder, key, value) + } + } + + /** + * Method: Dump + * Converts an AHK value into a JSON string + * Syntax: + * str := JSON.Dump( value [, replacer, space ] ) + * Parameter(s): + * str [retval] - JSON representation of an AHK value + * value [in] - any value(object, string, number) + * replacer [in, opt] - function object, similar to JavaScript's + * JSON.stringify() 'replacer' parameter + * space [in, opt] - similar to JavaScript's JSON.stringify() + * 'space' parameter + */ + class Dump extends JSON.Functor + { + Call(self, value, replacer:="", space:="") + { + this.rep := IsObject(replacer) ? replacer : "" + + this.gap := "" + if (space) { + static integer := "integer" + if space is %integer% + Loop, % ((n := Abs(space))>10 ? 10 : n) + this.gap .= " " + else + this.gap := SubStr(space, 1, 10) + + this.indent := "`n" + } + + return this.Str({"": value}, "") + } + + Str(holder, key) + { + value := holder[key] + + if (this.rep) + value := this.rep.Call(holder, key, value) + + if IsObject(value) { + ; Check object type, skip serialization for other object types such as + ; ComObject, Func, BoundFunc, FileObject, RegExMatchObject, Property, etc. + static type := A_AhkVersion<"2" ? "" : Func("Type") + if (type ? type.Call(value) == "Object" : ObjGetCapacity(value) != "") { + if (this.gap) { + stepback := this.indent + this.indent .= this.gap + } + + is_array := value.IsArray + ; Array() is not overridden, rollback to old method of + ; identifying array-like objects. Due to the use of a for-loop + ; sparse arrays such as '[1,,3]' are detected as objects({}). + if (!is_array) { + for i in value + is_array := i == A_Index + until !is_array + } + + str := "" + if (is_array) { + Loop, % value.Length() { + if (this.gap) + str .= this.indent + + v := this.Str(value, A_Index) + str .= (v != "") && value.HasKey(A_Index) ? v . "," : "null," + } + } else { + colon := this.gap ? ": " : ":" + for k in value { + v := this.Str(value, k) + if (v != "") { + if (this.gap) + str .= this.indent + + str .= this.Quote(k) . colon . v . "," + } + } + } + + if (str != "") { + str := RTrim(str, ",") + if (this.gap) + str .= stepback + } + + if (this.gap) + this.indent := stepback + + return is_array ? "[" . str . "]" : "{" . str . "}" + } + + } else ; is_number ? value : "value" + return ObjGetCapacity([value], 1)=="" ? value : this.Quote(value) + } + + Quote(string) + { + static q := Chr(34) + + if (string != "") { + string := StrReplace(string, "\", "\\") + ; , string := StrReplace(string, "/", "\/") ; optional in ECMAScript + , string := StrReplace(string, q, "\" . q) + , string := StrReplace(string, "`b", "\b") + , string := StrReplace(string, "`f", "\f") + , string := StrReplace(string, "`n", "\n") + , string := StrReplace(string, "`r", "\r") + , string := StrReplace(string, "`t", "\t") + + static rx_escapable := A_AhkVersion<"2" ? "O)[^\x20-\x7e]" : "[^\x20-\x7e]" + while RegExMatch(string, rx_escapable, m) + string := StrReplace(string, m.Value, Format("\u{1:04x}", Ord(m.Value))) + } + + return q . string . q + } + } + + /** + * Property: Undefined + * Proxy for 'undefined' type + * Syntax: + * undefined := JSON.Undefined + * Remarks: + * For use with reviver and replacer functions since AutoHotkey does not + * have an 'undefined' type. Returning blank("") or 0 won't work since these + * can't be distnguished from actual JSON values. This leaves us with objects. + * The caller may return a non-serializable AHK objects such as ComObject, + * Func, BoundFunc, FileObject, RegExMatchObject, and Property to mimic the + * behavior of returning 'undefined' in JavaScript but for the sake of code + * readability and convenience, it's better to do 'return JSON.Undefined'. + * Internally, the property returns a ComObject with the variant type of VT_EMPTY. + */ + Undefined[] + { + get { + static empty := {}, vt_empty := ComObject(0, &empty, 1) + return vt_empty + } + } + + class Functor + { + __Call(method, args*) + { + ; When casting to Call(), use a new instance of the "function object" + ; so as to avoid directly storing the properties(used across sub-methods) + ; into the "function object" itself. + if IsObject(method) + return (new this).Call(method, args*) + else if (method == "") + return (new this).Call(args*) + } + } +} \ No newline at end of file diff --git a/TabletLib/TabletLib.ahk b/TabletLib/TabletLib.ahk new file mode 100644 index 0000000..32ff3d7 --- /dev/null +++ b/TabletLib/TabletLib.ahk @@ -0,0 +1,42 @@ +/* +Common functions for Tablet Library +*/ +#include %A_LineFile%\..\JSON.ahk ; JSON library is used to save / load data as it is easier than using INIRead / INIWrite + +LoadBoxes(filename){ + global JSON + boxes := {} + FileRead, j, % filename + if (ERRORLEVEL == 0){ + j := JSON.Load(j) + for name, b in j { + box := new Box(name) + box.StartX := b.StartX + box.StartY := b.StartY + box.EndX := b.EndX + box.EndY := b.EndY + boxes[name] := box + } + } + return boxes +} + +FindBoxName(x, y, boxes){ + for name, box in boxes { + if (x >= box.StartX && x <= box.EndX && y >= box.StartY && y <= box.EndY){ + return name + } + } + return "" +} + +Class Box { + StartX := 0 + StartY := 0 + EndX := 0 + EndY := 0 + + __New(name){ + this.BoxName := name + } +} \ No newline at end of file