From 0f80199a0d9c004e3d625312b15322be6b202f95 Mon Sep 17 00:00:00 2001 From: Lunaticsky-tql <99857443+Lunaticsky-tql@users.noreply.github.com> Date: Wed, 7 Aug 2024 21:33:27 +0800 Subject: [PATCH] Add InputMethodIndicator.spoon (#307) --- Source/InputMethodIndicator.spoon/docs.json | 177 +++++++++++++ Source/InputMethodIndicator.spoon/init.lua | 268 ++++++++++++++++++++ 2 files changed, 445 insertions(+) create mode 100644 Source/InputMethodIndicator.spoon/docs.json create mode 100644 Source/InputMethodIndicator.spoon/init.lua diff --git a/Source/InputMethodIndicator.spoon/docs.json b/Source/InputMethodIndicator.spoon/docs.json new file mode 100644 index 00000000..95484e7e --- /dev/null +++ b/Source/InputMethodIndicator.spoon/docs.json @@ -0,0 +1,177 @@ +[ + { + "Constant" : [ + + ], + "submodules" : [ + + ], + "Function" : [ + + ], + "Variable" : [ + + ], + "stripped_doc" : [ + + ], + "desc" : "Show input method indicator in the current mouse position.", + "Deprecated" : [ + + ], + "type" : "Module", + "Constructor" : [ + + ], + "Field" : [ + + ], + "Method" : [ + { + "doc" : "init.\n\nParameters:\n * None\n\nReturns:\n * The InputMethodIndicator object", + "stripped_doc" : [ + "init.", + "" + ], + "def" : "InputMethodIndicator:init()", + "parameters" : [ + " * None", + "" + ], + "notes" : [ + + ], + "signature" : "InputMethodIndicator:init()", + "type" : "Method", + "returns" : [ + " * The InputMethodIndicator object" + ], + "desc" : "init.", + "name" : "init" + }, + { + "doc" : "Start InputMethodIndicator.\nParameters:\n * config - A table contains config options for the module\n * ABCColor - the dot color when the input method is ABC\n * LocalLanguageColor - the dot color when the input method is not ABC\n * mode - the mode of the indicator\n * showOnChangeDuration - seconds to show the indicator when the input method is changed\n * checkInterval - seconds to check the input method\n * dotSize - the size of the dot\n * deltaY - the distance between the dot and the center of the selection or mouse", + "stripped_doc" : [ + "Start InputMethodIndicator." + ], + "def" : "InputMethodIndicator:start(config)", + "parameters" : [ + " * config - A table contains config options for the module", + " * ABCColor - the dot color when the input method is ABC", + " * LocalLanguageColor - the dot color when the input method is not ABC", + " * mode - the mode of the indicator", + " * showOnChangeDuration - seconds to show the indicator when the input method is changed", + " * checkInterval - seconds to check the input method", + " * dotSize - the size of the dot", + " * deltaY - the distance between the dot and the center of the selection or mouse" + ], + "notes" : [ + + ], + "signature" : "InputMethodIndicator:start(config)", + "type" : "Method", + "returns" : [ + + ], + "desc" : "Start InputMethodIndicator.", + "name" : "start" + }, + { + "doc" : "Stop InputMethodIndicator.\nParameters:\n * None", + "stripped_doc" : [ + "Stop InputMethodIndicator." + ], + "def" : "InputMethodIndicator:stop()", + "parameters" : [ + " * None" + ], + "notes" : [ + + ], + "signature" : "InputMethodIndicator:stop()", + "type" : "Method", + "returns" : [ + + ], + "desc" : "Stop InputMethodIndicator.", + "name" : "stop" + } + ], + "Command" : [ + + ], + "doc" : "Show input method indicator in the current mouse position.\nIt is a small but noticable dot near the cursor.\nIt can be very useful when you are using a non-ABC input method and often needs to switch between ABC and the non-ABC input method.\nYou can use it as follows in the init.lua:\nhs.loadSpoon(\"InputMethodIndicator\")\nspoon.InputMethodIndicator:start(nil)\nnote: config parameter is a table, pass nil to use the default config\nthe default config is as follows:\n{\n ABCColor = \"#62C555\", -- the dot color when the input method is ABC\n LocalLanguageColor = \"#ED6A5E\", -- the dot color when the input method is not ABC\n mode = \"nearMouse\", -- the mode of the indicator\n showOnChangeDuration = 3, -- seconds to show the indicator when the input method is changed\n checkInterval = .01, -- seconds to check the input method\n dotSize = 6, -- the size of the dot\n deltaY=7, -- the distance between the dot and the center of the selection or mouse\n}\nthe mode can be \"nearMouse\",\"onChange\",\"adaptive\", the default mode is \"adaptive\"\n\"nearMouse\" means the indicator will always show near the mouse\n\"onChange\" means the indicator will show when the input method is changed and hide after showOnChangeDuration seconds\n\"adaptive\" means the indicator will show near the textarea when typing, otherwise it will show near the mouse\nNote: the \"adaptive\" mode is not perfect, it may not work in some apps because of the limitation of the accessibility API", + "items" : [ + { + "doc" : "init.\n\nParameters:\n * None\n\nReturns:\n * The InputMethodIndicator object", + "stripped_doc" : [ + "init.", + "" + ], + "def" : "InputMethodIndicator:init()", + "parameters" : [ + " * None", + "" + ], + "notes" : [ + + ], + "signature" : "InputMethodIndicator:init()", + "type" : "Method", + "returns" : [ + " * The InputMethodIndicator object" + ], + "desc" : "init.", + "name" : "init" + }, + { + "doc" : "Start InputMethodIndicator.\nParameters:\n * config - A table contains config options for the module\n * ABCColor - the dot color when the input method is ABC\n * LocalLanguageColor - the dot color when the input method is not ABC\n * mode - the mode of the indicator\n * showOnChangeDuration - seconds to show the indicator when the input method is changed\n * checkInterval - seconds to check the input method\n * dotSize - the size of the dot\n * deltaY - the distance between the dot and the center of the selection or mouse", + "stripped_doc" : [ + "Start InputMethodIndicator." + ], + "def" : "InputMethodIndicator:start(config)", + "parameters" : [ + " * config - A table contains config options for the module", + " * ABCColor - the dot color when the input method is ABC", + " * LocalLanguageColor - the dot color when the input method is not ABC", + " * mode - the mode of the indicator", + " * showOnChangeDuration - seconds to show the indicator when the input method is changed", + " * checkInterval - seconds to check the input method", + " * dotSize - the size of the dot", + " * deltaY - the distance between the dot and the center of the selection or mouse" + ], + "notes" : [ + + ], + "signature" : "InputMethodIndicator:start(config)", + "type" : "Method", + "returns" : [ + + ], + "desc" : "Start InputMethodIndicator.", + "name" : "start" + }, + { + "doc" : "Stop InputMethodIndicator.\nParameters:\n * None", + "stripped_doc" : [ + "Stop InputMethodIndicator." + ], + "def" : "InputMethodIndicator:stop()", + "parameters" : [ + " * None" + ], + "notes" : [ + + ], + "signature" : "InputMethodIndicator:stop()", + "type" : "Method", + "returns" : [ + + ], + "desc" : "Stop InputMethodIndicator.", + "name" : "stop" + } + ], + "name" : "InputMethodIndicator" + } +] diff --git a/Source/InputMethodIndicator.spoon/init.lua b/Source/InputMethodIndicator.spoon/init.lua new file mode 100644 index 00000000..22051ec2 --- /dev/null +++ b/Source/InputMethodIndicator.spoon/init.lua @@ -0,0 +1,268 @@ +--- === InputMethodIndicator === +--- +--- Show input method indicator in the current mouse position. +--- It is a small but noticable dot near the cursor. +--- It can be very useful when you are using a non-ABC input method and often needs to switch between ABC and the non-ABC input method. +--- You can use it as follows in the init.lua: +--- hs.loadSpoon("InputMethodIndicator") +--- spoon.InputMethodIndicator:start(nil) +--- note: config parameter is a table, pass nil to use the default config +--- the default config is as follows: +--- { +--- ABCColor = "#62C555", -- the dot color when the input method is ABC +--- LocalLanguageColor = "#ED6A5E", -- the dot color when the input method is not ABC +--- mode = "nearMouse", -- the mode of the indicator +--- showOnChangeDuration = 3, -- seconds to show the indicator when the input method is changed +--- checkInterval = .01, -- seconds to check the input method +--- dotSize = 6, -- the size of the dot +--- deltaY=7, -- the distance between the dot and the center of the selection or mouse +--- } +--- the mode can be "nearMouse","onChange","adaptive", the default mode is "adaptive" +--- "nearMouse" means the indicator will always show near the mouse +--- "onChange" means the indicator will show when the input method is changed and hide after showOnChangeDuration seconds +--- "adaptive" means the indicator will show near the textarea when typing, otherwise it will show near the mouse +--- Note: the "adaptive" mode is not perfect, it may not work in some apps because of the limitation of the accessibility API + +local obj = {} +local _store = {} +setmetatable(obj, { + __index = function(_, k) + return _store[k] + end, + __newindex = function(t, k, v) + rawset(_store, k, v) + if t._init_done then + if t._attribs[k] then + t:init() + end + end + end +}) +obj.__index = obj + +-- Metadata +obj.name = "InputMethodIndicator" +obj.version = "1.0" +obj.author = "lunaticsky <2013599@mail.nankai.edu.cn>" +obj.homepage = "https://github.com/Hammerspoon/Spoons" +obj.license = "MIT - https://opensource.org/licenses/MIT" + +local logger = hs.logger.new("InputMethodIndicator") +obj.logger = logger + +-- Defaults +obj._attribs = { + ABCColor = "#62C555", + LocalLanguageColor = "#ED6A5E", + mode = "adaptive", + showOnChangeDuration = 3, + checkInterval = .01, + dotSize = 6, + deltaY=7, +} +for k, v in pairs(obj._attribs) do + obj[k] = v +end + +--- InputMethodIndicator:init() +--- Method +--- init. +--- +--- Parameters: +--- * None +--- +--- Returns: +--- * The InputMethodIndicator object +function obj:init() + local mousePosition = hs.mouse.absolutePosition() + if not self.canvas then + self.canvas = hs.canvas.new({ + x = mousePosition.x - self.dotSize / 2, + y = mousePosition.y - 2*self.deltaY, + w = self.dotSize, + h = self.dotSize + }) + end + local sourceID = hs.keycodes.currentSourceID() + print(sourceID) + if (sourceID == "com.apple.keylayout.ABC") then + self.color = self.ABCColor + self.lastLayout = sourceID + else + self.color = self.LocalLanguageColor + self.lastLayout = sourceID + end + self.canvas[1] = { + action = "fill", + type = "circle", + fillColor = { + hex = self.color + }, + frame = { + x = 0, + y = 0, + h = self.dotSize, + w = self.dotSize + } + } + self._init_done = true + return self +end + +function obj:hideCanvasTimer() + return hs.timer.doAfter(self.showOnChangeDuration, function() + self.canvas:hide() + end) +end + +function obj:showCanvasOnChanged() + local sourceID = hs.keycodes.currentSourceID() + if (sourceID == self.lastLayout) then + return + end + self.setColor(self, sourceID) + self.canvas:show() + if not self.hideCanvasTimer:running() then + self.hideCanvasTimer:start() + else + self.hideCanvasTimer:stop() + self.hideCanvasTimer:start() + end +end +function obj:setColor(sourceID) + -- change the color of the circle according to the input layout + if (sourceID == "com.apple.keylayout.ABC") then + self.color = self.ABCColor + else + self.color = self.LocalLanguageColor + end + self.lastLayout = sourceID + self.canvas[1].fillColor = { + hex = self.color + } +end + +function obj:showNearMouse() + local cp = hs.mouse.absolutePosition() + -- change the position of the canvas + self.canvas:topLeft({ + x = cp.x - self.dotSize / 2, + y = cp.y - 15 + }) +end + +function obj:adaptiveChangePosition() + local systemWideElement = hs.axuielement.systemWideElement() + local focusedElement = systemWideElement.AXFocusedUIElement + if focusedElement then + local selectedRange = focusedElement.AXSelectedTextRange + if selectedRange then + local selectionBounds = focusedElement:parameterizedAttributeValue("AXBoundsForRange", selectedRange) + -- print the position and size of the selection,which is a table + if selectionBounds then + if selectionBounds.h == 0 or selectionBounds.y < 0 then + self:showNearMouse() + else + self.canvas:topLeft({ + x = selectionBounds.x - self.dotSize / 2, + y = selectionBounds.y - self.deltaY + }) + end + else + self:showNearMouse() + end + else + self:showNearMouse() + end + end +end + +function obj:adaptiveTimer() + return hs.timer.doEvery(self.checkInterval, function() + self:adaptiveChangePosition() + self:setColor(hs.keycodes.currentSourceID()) + end) +end + +function obj:showOnChangeTimer() + return hs.timer.doEvery(self.checkInterval, function() + self:adaptiveChangePosition() + self:showCanvasOnChanged() + end) +end + +function obj:showNearMouseTimer() + return hs.timer.doEvery(self.checkInterval, function() + self:showNearMouse() + self:setColor(hs.keycodes.currentSourceID()) + end) +end +--- InputMethodIndicator:start(config) +--- Method +--- Start InputMethodIndicator. +--- +--- Parameters: +--- * config - A table contains config options for the module +--- * ABCColor - the dot color when the input method is ABC +--- * LocalLanguageColor - the dot color when the input method is not ABC +--- * mode - the mode of the indicator +--- * showOnChangeDuration - seconds to show the indicator when the input method is changed +--- * checkInterval - seconds to check the input method +--- * dotSize - the size of the dot +--- * deltaY - the distance between the dot and the center of the selection or mouse +function obj:start(config) + -- check whether the config is a table + if config then + if type(config) ~= "table" then + hs.alert.show("Config must be a table") + logger.e("Config must be a table") + return + end + for k, v in pairs(config) do + if self[k] then + self[k] = v + else + logger.e("Invalid config key: " .. k) + end + end + end + if self.mode == "onChange" then + self.hideCanvasTimer = self:hideCanvasTimer() + self.showOnChangeTimer = self:showOnChangeTimer() + elseif self.mode == "adaptive" then + self.canvas:show() + self.adaptiveTimer = self:adaptiveTimer() + elseif self.mode == "nearMouse" then + self.canvas:show() + self.showNearMouseTimer = self:showNearMouseTimer() + else + hs.alert.show("Invalid mode") + logger.e("Invalid mode") + return + end +end + +--- InputMethodIndicator:stop() +--- Method +--- Stop InputMethodIndicator. +--- +--- Parameters: +--- * None +function obj:stop() + self.canvas:hide() + self.canvas = nil + if self.showOnChangeTimer then + self.showOnChangeTimer:stop() + self.showOnChangeTimer = nil + end + if self.adaptiveTimer then + self.adaptiveTimer:stop() + self.adaptiveTimer = nil + end + if self.showNearMouseTimer then + self.showNearMouseTimer:stop() + self.showNearMouseTimer=nil + end +end + +return obj