Add TabletButtons demo

hotfix/issue-44
Clive Galway 5 years ago
parent 09b52b174f
commit 6a5710aeae

1
.gitignore vendored

@ -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.

@ -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

@ -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
}

@ -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

@ -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 <JSON>
* Links:
* GitHub: - https://github.com/cocobelgica/AutoHotkey-JSON
* Forum Topic - http://goo.gl/r0zI8t
* Email: - cocobelgica <at> gmail <dot> 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*)
}
}
}

@ -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
}
}
Loading…
Cancel
Save