Fix accessible output window, revert Tab/Ctrl+Space, sync readme keys
- output_functions.xml: merge NotepadSmartScroll's Win32/Alien-based smart-scroll logic in directly (was a separate plugin). Fixed the caret-restore bug: gating the restore on scroll position was wrong since the notepad tracks near the bottom during normal reading; now gated on whether the caret was literally at the end of the old text. Added a jump-to-end-on-focus toggle (Ctrl+Alt+J) with a 0.2s polling timer to catch focus landing on the output notepad by any means. - Reverted the Tab/Ctrl+Space rebinding - removed switch_to_output and trigger_word_completion along with their accelerators. - cosmic rage.MCL: dropped the dangling NotepadSmartScroll.xml include (file no longer exists, was reintroduced by an auto-save). - readme.md: documented the actual current CosmicRage.xml keys (several had drifted out of sync - Alt+F12 is now stop-sounds not restart, Alt+Delete/Ctrl+S/Ctrl+F1-3/Alt+F3/Alt+Shift+H/F11 were undocumented) and added the output_functions.xml accessible-output keybindings, which weren't documented at all before.
This commit is contained in:
@@ -25,7 +25,7 @@ sequence="10000"
|
||||
for i = 1, 9 do
|
||||
Accelerator("alt + " .. i, "@buffer " .. i)
|
||||
end
|
||||
Accelerator("alt + 0", "@buffer 10") -- 10 is a special case
|
||||
Accelerator("alt + 0", "@buffer 10")
|
||||
Accelerator("alt + right", "@buffer forward")
|
||||
Accelerator("alt + left", "@buffer backward")
|
||||
Accelerator("alt + down", "@buffer down")
|
||||
@@ -34,41 +34,64 @@ Accelerator("alt + pagedown", "@buffer scrolldown")
|
||||
Accelerator("alt + pageup", "@buffer scrollup")
|
||||
Accelerator("alt + end", "@buffer bottom")
|
||||
Accelerator("alt + home", "@buffer top")
|
||||
Accelerator("alt+shift+delete", "@buffer clear")
|
||||
Accelerator("alt+delete", "@buffer clear")
|
||||
Accelerator("alt+shift+delete", "@buffer clearall")
|
||||
Accelerator("alt+shift+right", "@buffer add")
|
||||
Accelerator("alt+shift+left", "@buffer delete")
|
||||
Accelerator("f4", "@sp-settings master volume")
|
||||
Accelerator("f5", "@sp-settings General volume")
|
||||
Accelerator("f6", "@sp-settings ambiance volume")
|
||||
Accelerator("f7", "@sp-settings in and out of character communications volume")
|
||||
Accelerator("f8", "@sp-settings planetary music volume")
|
||||
Accelerator("ctrl+f5", "@sp-settings clocks volume")
|
||||
Accelerator("ctrl+f6", "@sp-settings Combat music volume")
|
||||
Accelerator("ctrl+f7", "@sp-settings IC social volume")
|
||||
Accelerator("ctrl+f8", "@sp-settings OOC social volume")
|
||||
Accelerator("alt+f5", "@sp-settings Ground Combat volume")
|
||||
Accelerator("alt+f6", "@sp-settings harvesting activity volume")
|
||||
Accelerator("alt+f7", "@sp-settings Salvage activity volume")
|
||||
Accelerator("alt+f8", "@sp-settings volume for games such as Pilot's Parody")
|
||||
Accelerator("shift+f5", "@sp-settings starship combat volume")
|
||||
Accelerator("shift+f6", "@sp-settings ship volume")
|
||||
Accelerator("shift+f7", "@sp-settings ship movement volume")
|
||||
Accelerator("shift+f8", "@sp-settings vehicle volume")
|
||||
Accelerator("f9", "@sp-settings IC social sounds")
|
||||
Accelerator("f10", "@sp-settings OOC social sounds")
|
||||
Accelerator("alt+f9", "@sp-settings panning of sounds")
|
||||
Accelerator("alt+f10", "@sp-settings pitch-shifting sounds")
|
||||
Accelerator("alt+f11", "@sp-settings ambiance")
|
||||
Accelerator("ctrl+f9", "@sp-settings planetary music")
|
||||
Accelerator("ctrl+f10", "@sp-settings space combat music")
|
||||
Accelerator("ctrl+f11", "@sp-settings weather sounds")
|
||||
Accelerator("alt+f12", "@sp-restart")
|
||||
Accelerator("shift+alt+f9", "@sp-settings Interrupt OOC Social sounds when the next one plays")
|
||||
Accelerator("f2", "@sp-settings")
|
||||
|
||||
Accelerator("f5", "@sp-settings General volume")
|
||||
Accelerator("f6", "@sp-settings ambiance volume")
|
||||
Accelerator("f7", "@sp-settings in and out of character communications volume")
|
||||
Accelerator("f8", "@sp-settings planetary music volume")
|
||||
Accelerator("ctrl+f5", "@sp-settings clocks volume")
|
||||
Accelerator("ctrl+f6", "@sp-settings Combat music volume")
|
||||
Accelerator("ctrl+f7", "@sp-settings IC social volume")
|
||||
Accelerator("ctrl+f8", "@sp-settings OOC social volume")
|
||||
Accelerator("alt+f5", "@sp-settings Ground Combat volume")
|
||||
Accelerator("alt+f6", "@sp-settings harvesting activity volume")
|
||||
Accelerator("alt+f7", "@sp-settings Salvage activity volume")
|
||||
Accelerator("alt+f8", "@sp-settings volume for games such as Pilot's Parody")
|
||||
Accelerator("shift+f5", "@sp-settings starship combat volume")
|
||||
Accelerator("shift+f6", "@sp-settings ship volume")
|
||||
Accelerator("shift+f7", "@sp-settings ship movement volume")
|
||||
Accelerator("shift+f8", "@sp-settings vehicle volume")
|
||||
Accelerator("f9", "@sp-settings IC social sounds")
|
||||
Accelerator("f10", "@sp-settings OOC social sounds")
|
||||
Accelerator("alt+f9", "@sp-settings panning of sounds")
|
||||
Accelerator("alt+f10", "@sp-settings pitch-shifting sounds")
|
||||
Accelerator("alt+f11", "@sp-settings ambiance")
|
||||
Accelerator("ctrl+f9", "@sp-settings planetary music")
|
||||
Accelerator("ctrl+f10", "@sp-settings space combat music")
|
||||
Accelerator("ctrl+f11", "@sp-settings weather sounds")
|
||||
Accelerator("shift+alt+f9", "@sp-settings Interrupt OOC Social sounds when the next one plays")
|
||||
Accelerator("f2", "@sp-settings")
|
||||
Accelerator("shift+f1", "@sp-toggle")
|
||||
Accelerator("Ctrl+shift+enter", "@sp-settings buffer sounds")
|
||||
Accelerator("ctrl+shift+v", "@sp-version")
|
||||
Accelerator("ctrl+s", "save_state_func")
|
||||
Accelerator("alt+shift+h", "localsphelp")
|
||||
Accelerator("ctrl+f2", "mainvoldown")
|
||||
Accelerator("ctrl+f3", "mainvolup")
|
||||
Accelerator("alt+f3", "intromusictoggle")
|
||||
Accelerator("ctrl+f1", "smute")
|
||||
Accelerator("f11", "f11_stop")
|
||||
Accelerator("alt+f12", "alt_f12_stop")
|
||||
|
||||
function save_state_func()
|
||||
SaveState()
|
||||
Execute("tts_interrupt Client-side Soundpack Settings have been saved successfully!")
|
||||
end
|
||||
|
||||
function f11_stop()
|
||||
ppi.stop(0)
|
||||
Execute("tts_interrupt All currently playing sounds have been stopped.")
|
||||
Send("@sp-restart")
|
||||
end
|
||||
|
||||
function alt_f12_stop()
|
||||
ppi.stop(0)
|
||||
Execute("tts_interrupt All currently playing sounds have been stopped.")
|
||||
end
|
||||
|
||||
|
||||
function RegisterMushClient(name, line, wildcards)
|
||||
@@ -91,7 +114,15 @@ Note("You are currently running version " .. tostring(CURRENT_SP_VERSION).." of
|
||||
|
||||
if version_diff > 0 then
|
||||
ColourNote("yellow", "", "Warning: Your soundpack is " .. version_diff .. " version(s) out of date. We are now on version " .. user_version .. " of the soundpack.")
|
||||
ColourNote("yellow", "", "Run Updater.bat to pull the latest version.")
|
||||
ColourNote("yellow", "", "Launching the updater...")
|
||||
ppi.play(dir.."sounds/ogg/general/comms/announcement.ogg", 0, 0, 100)
|
||||
local bat_path = dir .. "..\\updator.bat"
|
||||
-- Signature is (filename, params, defdir, operation, show) - this was
|
||||
-- previously passing "open" as the filename, so it never actually
|
||||
-- launched cmd.exe/updator.bat at all.
|
||||
utils.shellexecute("cmd", '/c "' .. bat_path .. '"', "", "open", 1)
|
||||
ColourNote("yellow", "", "The updater has been launched in a separate window. Once the update is complete, please restart MUSHclient to apply the updates!")
|
||||
Execute("tts_interrupt Warning: Your soundpack is out of date. The updater has been launched. Once the update is complete, please restart MUSHclient to apply the updates!")
|
||||
else
|
||||
ColourNote("green", "", "Your soundpack is up to date!")
|
||||
end
|
||||
@@ -102,6 +133,109 @@ function url_encode(str)
|
||||
return string.format("%%%02X", string.byte(c))
|
||||
end))
|
||||
end
|
||||
|
||||
pending_downloads = {}
|
||||
pending_play = {}
|
||||
|
||||
function download_sound(path, action, pan, volume, pitch, id)
|
||||
if pending_downloads[path] then
|
||||
table.insert(pending_play[path], {action=action, pan=pan, volume=volume, pitch=pitch, id=id})
|
||||
return
|
||||
end
|
||||
|
||||
pending_downloads[path] = os.time()
|
||||
pending_play[path] = { {action=action, pan=pan, volume=volume, pitch=pitch, id=id} }
|
||||
|
||||
local sound_path = dir .. "sounds/ogg/" .. path .. ".ogg"
|
||||
local safe_path = sound_path:gsub("/", "\\")
|
||||
local parent_dir = safe_path:match("^(.+)\\[^\\]+$")
|
||||
local encoded_path = url_encode(path)
|
||||
local url = "http://nathantech.net:3000/CosmicRage/CosmicRageSounds/raw/branch/main/ogg/" .. encoded_path .. ".ogg"
|
||||
|
||||
-- Download to a .part file and only rename it to the real filename after
|
||||
-- curl succeeds, so check_downloads() can never mistake a half-written or
|
||||
-- failed transfer for a completed one. Clears any leftover .err first, and
|
||||
-- deletes it again on success, so a stale error/empty file can't linger.
|
||||
local cmd_args = string.format(
|
||||
'/C mkdir "%s" 2>nul & del /q "%s.err" 2>nul & ' ..
|
||||
'curl.exe -s -S -L -f -o "%s.part" "%s" 2>"%s.err" ' ..
|
||||
'&& (move /y "%s.part" "%s" >nul & del /q "%s.err" 2>nul) || del /q "%s.part" 2>nul',
|
||||
parent_dir, safe_path, safe_path, url, safe_path, safe_path, safe_path, safe_path, safe_path
|
||||
)
|
||||
-- NOTE: correct shellexecute signature is (filename, params, defdir, operation, show)
|
||||
utils.shellexecute("cmd", cmd_args, "", "open", 0)
|
||||
end
|
||||
|
||||
-- If a download hasn't finished or failed within this long, give up on it
|
||||
-- rather than let a stuck curl process silently block that sound forever.
|
||||
DOWNLOAD_TIMEOUT_SECONDS = 30
|
||||
|
||||
function check_downloads(name)
|
||||
for path, started_at in pairs(pending_downloads) do
|
||||
local sound_path = dir .. "sounds/ogg/" .. path .. ".ogg"
|
||||
local safe_path = sound_path:gsub("/", "\\")
|
||||
local f = io.open(sound_path, "r")
|
||||
if f then
|
||||
f:close()
|
||||
local queue = pending_play[path]
|
||||
pending_downloads[path] = nil
|
||||
pending_play[path] = nil
|
||||
if queue then
|
||||
for _, p in ipairs(queue) do
|
||||
play_sound_now(path, p.action, p.pan, p.volume, p.pitch, p.id)
|
||||
end
|
||||
end
|
||||
else
|
||||
local ef = io.open(safe_path .. ".err", "r")
|
||||
if ef then
|
||||
local msg = ef:read("*all")
|
||||
ef:close()
|
||||
if msg and msg ~= "" then
|
||||
os.remove(safe_path .. ".err")
|
||||
pending_downloads[path] = nil
|
||||
pending_play[path] = nil
|
||||
Note("Soundpack: download failed for " .. path .. ": " .. msg:gsub("[\r\n]", ""))
|
||||
end
|
||||
elseif os.time() - started_at > DOWNLOAD_TIMEOUT_SECONDS then
|
||||
pending_downloads[path] = nil
|
||||
pending_play[path] = nil
|
||||
Note("Soundpack: download timed out for " .. path)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function play_sound_now(path, action, pan, volume, pitch, id)
|
||||
local sound_file = dir.."sounds/ogg/"..path..".ogg"
|
||||
local temp
|
||||
if (action == "play") then
|
||||
temp = ppi.play(sound_file, 0, pan, volume)
|
||||
if (pitch > -10000) then
|
||||
ppi.setPitch(pitch, temp)
|
||||
end
|
||||
if (pan > -10000) then
|
||||
ppi.setPan(pan, temp)
|
||||
end
|
||||
elseif (action == "playrec") then
|
||||
temp = ppi.play(sound_file, 0, pan, volume)
|
||||
SetVariable(id, temp)
|
||||
if (pitch > -10000) then
|
||||
ppi.setPitch(pitch, temp)
|
||||
end
|
||||
if (pan > -10000) then
|
||||
ppi.setPan(pan, temp)
|
||||
end
|
||||
elseif (action == "loop") then
|
||||
temp = ppi.play(sound_file, 1, pan, volume)
|
||||
SetVariable(id, temp)
|
||||
if (pitch > -10000) then
|
||||
ppi.setPitch(pitch, temp)
|
||||
end
|
||||
if (pan > -10000) then
|
||||
ppi.setPan(pan, temp)
|
||||
end
|
||||
end
|
||||
end
|
||||
]]>
|
||||
|
||||
|
||||
@@ -197,106 +331,87 @@ match="^\$sphook\s+(?P<action>[\w]+):(?P<path>[-\w./\\\s]+):(?P<volume>[\w]+):(?
|
||||
pan="%5"
|
||||
id="%6"
|
||||
volume=tonumber(volume)
|
||||
if (path == "na") then
|
||||
else
|
||||
sound_path = dir .. "sounds/ogg/" .. path .. ".ogg"
|
||||
directory_path = sound_path:match("(.*/)")
|
||||
if directory_path then
|
||||
test_file = io.open(directory_path .. "tester.txt", "w")
|
||||
if test_file then
|
||||
test_file:close()
|
||||
os.remove(directory_path .. "checker.txt") -- Clean up
|
||||
|
||||
if (pitch == "na") then
|
||||
pitch = -10000
|
||||
else
|
||||
os.execute('mkdir "' .. directory_path .. '" 2>nul')
|
||||
pitch = tonumber(pitch)
|
||||
pitch = (pitch - 44100) / 98
|
||||
end
|
||||
end
|
||||
file = io.open(sound_path, "r")
|
||||
if file then
|
||||
file:close()
|
||||
else
|
||||
local encoded_path = url_encode(path)
|
||||
local download_url = "http://nathantech.net:3000/CosmicRage/CosmicRageSounds/raw/branch/main/ogg/" .. encoded_path .. ".ogg"
|
||||
body = {}
|
||||
result, status_code, headers, status = socket.http.request{
|
||||
url = download_url, sink = ltn12.sink.table(body)
|
||||
}
|
||||
if result and status_code == 200 then
|
||||
save_file = io.open(sound_path, "wb")
|
||||
if save_file then
|
||||
save_file:write(table.concat(body))
|
||||
save_file:close()
|
||||
|
||||
if (pan == "na") then
|
||||
pan = -10000
|
||||
else
|
||||
pan = tonumber(pan)
|
||||
end
|
||||
|
||||
if (path == "na") then
|
||||
if (action == "adjustsound") then
|
||||
temp = GetVariable(id)
|
||||
if volume and volume > -1 then
|
||||
ppi.setVol(volume, temp)
|
||||
end
|
||||
if (pitch > -10000) then
|
||||
ppi.setPitch(pitch, temp)
|
||||
end
|
||||
if (pan > -10000) then
|
||||
ppi.setPan(pan, temp)
|
||||
end
|
||||
elseif (action == "stop") then
|
||||
temp = GetVariable(id)
|
||||
if temp then
|
||||
ppi.stop(temp)
|
||||
end
|
||||
end
|
||||
else
|
||||
local sound_path = dir .. "sounds/ogg/" .. path .. ".ogg"
|
||||
local file = io.open(sound_path, "r")
|
||||
if file then
|
||||
file:close()
|
||||
play_sound_now(path, action, pan, volume, pitch, id)
|
||||
else
|
||||
download_sound(path, action, pan, volume, pitch, id)
|
||||
end
|
||||
else
|
||||
Note("Download of "..path.." failed with status code: " .. tostring(status_code))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if (pitch == "na") then
|
||||
pitch = -10000
|
||||
else
|
||||
pitch = tonumber(pitch)
|
||||
pitch = (pitch - 44100) / 98
|
||||
end
|
||||
|
||||
if (pan == "na") then
|
||||
pan = -10000
|
||||
else
|
||||
pan = tonumber(pan)
|
||||
end
|
||||
|
||||
if (action == "play") then
|
||||
temp = ppi.play(dir.."sounds/ogg/"..path..".ogg", 0, pan, volume)
|
||||
if (pitch > -10000) then
|
||||
ppi.setPitch(pitch, temp)
|
||||
end
|
||||
if (pan > -10000) then
|
||||
ppi.setPan(pan, temp)
|
||||
end
|
||||
end
|
||||
if (action=="playrec") then
|
||||
temp=ppi.play(dir.."sounds/ogg/"..path..".ogg", 0, pan, volume)
|
||||
SetVariable(id, temp)
|
||||
if(pitch>-10000) then
|
||||
ppi.setPitch(pitch,temp)
|
||||
end
|
||||
if(pan>-10000) then
|
||||
ppi.setPan(pan,temp)
|
||||
end
|
||||
end
|
||||
if (action=="loop") then
|
||||
temp=ppi.play(dir.."sounds/ogg/"..path..".ogg", 1, pan, volume)
|
||||
SetVariable(id, temp)
|
||||
if(pitch>-10000) then
|
||||
ppi.setPitch(pitch,temp)
|
||||
end
|
||||
if(pan>-10000) then
|
||||
ppi.setPan(pan,temp)
|
||||
end
|
||||
end
|
||||
if (action=="adjustsound") then
|
||||
temp=GetVariable (id)
|
||||
if(volume>-1) then
|
||||
ppi.setVol(volume,temp)
|
||||
end
|
||||
if(pitch>-10000) then
|
||||
ppi.setPitch(pitch,temp)
|
||||
end
|
||||
if(pan>-10000) then
|
||||
ppi.setPan(pan,temp)
|
||||
end
|
||||
end
|
||||
if (action=="stop") then
|
||||
temp=GetVariable(id)
|
||||
ppi.stop(temp)
|
||||
end
|
||||
</send>
|
||||
</trigger>
|
||||
</triggers>
|
||||
|
||||
<!-- Aliases -->
|
||||
<!-- Timers -->
|
||||
<timers>
|
||||
<timer
|
||||
enabled="y"
|
||||
second="0.5"
|
||||
script="check_downloads"
|
||||
>
|
||||
</timer>
|
||||
</timers>
|
||||
|
||||
|
||||
|
||||
<aliases>
|
||||
<alias
|
||||
match="f11_stop"
|
||||
enabled="y"
|
||||
script="f11_stop"
|
||||
omit_from_output="y"
|
||||
omit_from_log="y"
|
||||
omit_from_command_history="y"
|
||||
send_to="12"
|
||||
sequence="100"
|
||||
>
|
||||
</alias>
|
||||
<alias
|
||||
match="alt_f12_stop"
|
||||
enabled="y"
|
||||
script="alt_f12_stop"
|
||||
omit_from_output="y"
|
||||
omit_from_log="y"
|
||||
omit_from_command_history="y"
|
||||
send_to="12"
|
||||
sequence="100"
|
||||
>
|
||||
</alias>
|
||||
</aliases>
|
||||
|
||||
</muclient>
|
||||
@@ -298,6 +298,10 @@ function OnPluginScreendraw(t, log, line)
|
||||
-- checking for an empty string, or a string composed only of spaces.
|
||||
-- If we don't, NVDA says blank.
|
||||
if (t == 0 or t == 1) and not line:find("^%s*$") then
|
||||
if GetVariable("skip_speech") == "1" then
|
||||
SetVariable("skip_speech", "0")
|
||||
return
|
||||
end
|
||||
if subst.status==0 then
|
||||
return
|
||||
end
|
||||
|
||||
@@ -14,8 +14,8 @@ save_state="y"
|
||||
language="Lua"
|
||||
purpose="provides functions to help tts users."
|
||||
date_written="2010-04-06 08:37:40"
|
||||
requires="4.46"
|
||||
version="1.0"
|
||||
requires="4.82"
|
||||
version="1.1"
|
||||
>
|
||||
|
||||
</plugin>
|
||||
@@ -172,6 +172,17 @@ sequence="100">
|
||||
</alias>
|
||||
</aliases>
|
||||
|
||||
<!-- Timers -->
|
||||
|
||||
<timers>
|
||||
<timer
|
||||
enabled="y"
|
||||
second="0.2"
|
||||
script="poll_output_focus"
|
||||
>
|
||||
</timer>
|
||||
</timers>
|
||||
|
||||
|
||||
<!-- Get our standard constants -->
|
||||
|
||||
@@ -200,6 +211,7 @@ Accelerator("ctrl+shift+n","endline")
|
||||
Accelerator("ctrl+shift+y","topline")
|
||||
Accelerator("ctrl+shift+h","whichline")
|
||||
Accelerator("ctrl+shift+alt+s","snap_shot")
|
||||
Accelerator("ctrl+alt+j","toggle_jump_to_end")
|
||||
|
||||
function selectscr(eol)
|
||||
buffercheck()
|
||||
@@ -280,12 +292,188 @@ function OnPluginCommandEntered(s)
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------------------------
|
||||
-- Notepad smart-scroll (formerly the separate NotepadSmartScroll
|
||||
-- plugin, merged in here). See SmartAppendToNotepad's comment below
|
||||
-- for why this needs raw Win32 access via Alien.
|
||||
------------------------------------------------------------------
|
||||
|
||||
require "alien"
|
||||
|
||||
local user32 = alien.load("user32.dll")
|
||||
local kernel32 = alien.load("kernel32.dll")
|
||||
|
||||
local FindWindowEx = user32.FindWindowExA
|
||||
FindWindowEx:types{ ret = "pointer", "pointer", "pointer", "string", "string", abi = "stdcall" }
|
||||
|
||||
local GetWindowThreadProcessId = user32.GetWindowThreadProcessId
|
||||
GetWindowThreadProcessId:types{ ret = "uint", "pointer", "ref uint", abi = "stdcall" }
|
||||
|
||||
local GetCurrentProcessId = kernel32.GetCurrentProcessId
|
||||
GetCurrentProcessId:types{ ret = "uint", abi = "stdcall" }
|
||||
|
||||
local SendMessage = user32.SendMessageA
|
||||
SendMessage:types{ ret = "long", "pointer", "uint", "uint", "long", abi = "stdcall" }
|
||||
|
||||
local GetModuleHandle = kernel32.GetModuleHandleA
|
||||
GetModuleHandle:types{ ret = "pointer", "string", abi = "stdcall" }
|
||||
|
||||
local GetProcAddress = kernel32.GetProcAddress
|
||||
GetProcAddress:types{ ret = "pointer", "pointer", "string", abi = "stdcall" }
|
||||
|
||||
local GetFocus = user32.GetFocus
|
||||
GetFocus:types{ ret = "pointer", abi = "stdcall" }
|
||||
|
||||
-- EM_GETSEL's wParam/lParam are *pointers* to DWORD (needed to get the
|
||||
-- real 32-bit selection range - the classic packed-return form is capped
|
||||
-- at 64K). SendMessage above is already typed with "uint"/"long" for its
|
||||
-- generic wParam/lParam, so a second binding of the same underlying
|
||||
-- function is needed with "pointer" typed args for this one message.
|
||||
local user32_handle = GetModuleHandle("user32.dll")
|
||||
local SendMessagePtr = alien.funcptr(GetProcAddress(user32_handle, "SendMessageA"))
|
||||
SendMessagePtr:types{ ret = "long", "pointer", "uint", "pointer", "pointer", abi = "stdcall" }
|
||||
|
||||
local WM_GETTEXTLENGTH = 0x000E
|
||||
local EM_GETFIRSTVISIBLELINE = 0x00CE
|
||||
local EM_LINESCROLL = 0x00B6
|
||||
local EM_GETSEL = 0x00B0
|
||||
local EM_SETSEL = 0x00B1
|
||||
local EM_SCROLLCARET = 0x00B7
|
||||
|
||||
local my_pid = GetCurrentProcessId()
|
||||
|
||||
-- Finds the "Edit" control belonging to a MUSHclient notepad MDI child
|
||||
-- window whose title exactly matches. Scoped to top-level windows owned
|
||||
-- by this process, so it never touches unrelated apps on the desktop.
|
||||
local function find_notepad_edit(title)
|
||||
local top = nil
|
||||
while true do
|
||||
top = FindWindowEx(nil, top, nil, nil)
|
||||
if not top then return nil end
|
||||
|
||||
local _, pid = GetWindowThreadProcessId(top, 0)
|
||||
if pid == my_pid then
|
||||
local mdiClient = FindWindowEx(top, nil, "MDIClient", nil)
|
||||
if mdiClient then
|
||||
local frame = FindWindowEx(mdiClient, nil, nil, title)
|
||||
if frame then
|
||||
local edit = FindWindowEx(frame, nil, "Edit", nil)
|
||||
if edit then
|
||||
return edit
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function get_selection(edit)
|
||||
local start_buf = alien.buffer(4)
|
||||
local end_buf = alien.buffer(4)
|
||||
SendMessagePtr(edit, EM_GETSEL, start_buf, end_buf)
|
||||
return start_buf:get(1, "uint"), end_buf:get(1, "uint")
|
||||
end
|
||||
|
||||
local function scroll_to_line(edit, target)
|
||||
local current = SendMessage(edit, EM_GETFIRSTVISIBLELINE, 0, 0)
|
||||
local delta = target - current
|
||||
if delta ~= 0 then
|
||||
SendMessage(edit, EM_LINESCROLL, 0, delta)
|
||||
end
|
||||
end
|
||||
|
||||
-- Wraps AppendToNotepad(title, text): if the caret wasn't already sitting
|
||||
-- at the very end of the text (i.e. the user has moved it to read
|
||||
-- somewhere else), both the caret/selection and the scroll position are
|
||||
-- restored after the append instead of being left jumped to the end.
|
||||
-- AppendToNotepad hardcodes SetSel(len,len)+ReplaceSel on every call (see
|
||||
-- scripting/methods/methods_notepad.cpp in the MUSHclient source) with no
|
||||
-- scriptable override, so this reads the notepad Edit control's caret and
|
||||
-- scroll position via raw Win32 calls (through the Alien FFI library,
|
||||
-- running in-process on MUSHclient's own thread) before appending, and
|
||||
-- restores them afterward when appropriate.
|
||||
--
|
||||
-- Note: text should use "\r\n" line endings, not bare "\n" - the plain
|
||||
-- Win32 Edit control that backs the notepad doesn't recognise bare "\n"
|
||||
-- as a line break for scrolling/line-count purposes.
|
||||
function SmartAppendToNotepad(title, text)
|
||||
local edit = find_notepad_edit(title)
|
||||
local was_at_end = true
|
||||
local sel_start, sel_end, first_visible
|
||||
|
||||
if edit then
|
||||
local old_length = SendMessage(edit, WM_GETTEXTLENGTH, 0, 0)
|
||||
sel_start, sel_end = get_selection(edit)
|
||||
was_at_end = (sel_start == old_length) and (sel_end == old_length)
|
||||
if not was_at_end then
|
||||
first_visible = SendMessage(edit, EM_GETFIRSTVISIBLELINE, 0, 0)
|
||||
end
|
||||
end
|
||||
|
||||
AppendToNotepad(title, text)
|
||||
|
||||
if edit and not was_at_end then
|
||||
-- Re-find in case the append above created/recreated the window.
|
||||
local edit2 = find_notepad_edit(title) or edit
|
||||
SendMessage(edit2, EM_SETSEL, sel_start, sel_end)
|
||||
scroll_to_line(edit2, first_visible)
|
||||
end
|
||||
end
|
||||
|
||||
-- Moves the caret to the very end of a notepad and scrolls it into view.
|
||||
local function jump_notepad_to_end(edit)
|
||||
local len = SendMessage(edit, WM_GETTEXTLENGTH, 0, 0)
|
||||
SendMessage(edit, EM_SETSEL, len, len)
|
||||
SendMessage(edit, EM_SCROLLCARET, 0, 0)
|
||||
end
|
||||
|
||||
function toggle_jump_to_end()
|
||||
if GetVariable("jump_to_end_on_focus") == "0" then
|
||||
SetVariable("jump_to_end_on_focus", "1")
|
||||
Execute("tts_interrupt Jump to end when switching to output: on")
|
||||
else
|
||||
SetVariable("jump_to_end_on_focus", "0")
|
||||
Execute("tts_interrupt Jump to end when switching to output: off")
|
||||
end
|
||||
end
|
||||
|
||||
-- Polls for the "output" notepad gaining focus (mouse click, Ctrl+Tab
|
||||
-- between MDI windows, ActivateNotepad from a script, etc.), so the
|
||||
-- jump-to-end behaviour applies regardless of how the user switches to
|
||||
-- it. MUSHclient's own Accelerator/hotkey system can't see keystrokes
|
||||
-- while a separate notepad window has focus (confirmed by Nick Gammon on
|
||||
-- the MUSHclient forum), so this can't be event-driven - polling every
|
||||
-- 0.2s is the safe, non-hooking way to catch it.
|
||||
was_output_focused = false
|
||||
|
||||
function poll_output_focus()
|
||||
if GetVariable("jump_to_end_on_focus") == "0" then
|
||||
was_output_focused = false
|
||||
return
|
||||
end
|
||||
local edit = find_notepad_edit("output")
|
||||
if not edit then
|
||||
was_output_focused = false
|
||||
return
|
||||
end
|
||||
local focused = GetFocus()
|
||||
local is_focused = (focused == edit)
|
||||
if is_focused and not was_output_focused then
|
||||
jump_notepad_to_end(edit)
|
||||
end
|
||||
was_output_focused = is_focused
|
||||
end
|
||||
|
||||
------------------------------------------------------------------
|
||||
|
||||
function OnPluginInstall()
|
||||
if GetVariable("output") == nil then
|
||||
SetVariable("output","1")
|
||||
end
|
||||
if GetVariable("jump_to_end_on_focus") == nil then
|
||||
SetVariable("jump_to_end_on_focus","1")
|
||||
end
|
||||
modes = {}
|
||||
msgbuffer = {}
|
||||
cline = 1
|
||||
lastcount=0
|
||||
line = 0
|
||||
@@ -297,23 +485,7 @@ function OnPluginScreendraw(t,l,line)
|
||||
if(GetVariable("output")=="0") then
|
||||
return
|
||||
end
|
||||
if GetInfo(113) then
|
||||
AppendToNotepad("output", line.."\r\n")
|
||||
else
|
||||
table.insert(msgbuffer, line)
|
||||
end
|
||||
end
|
||||
|
||||
function OnPluginGetFocus()
|
||||
if GetVariable("output") == "0" then
|
||||
return
|
||||
end
|
||||
if #msgbuffer > 0 then
|
||||
for i, buffered in ipairs(msgbuffer) do
|
||||
AppendToNotepad("output", buffered.."\r\n")
|
||||
end
|
||||
msgbuffer={}
|
||||
end
|
||||
SmartAppendToNotepad("output", line.."\r\n")
|
||||
end
|
||||
|
||||
function prev_line()
|
||||
|
||||
Reference in New Issue
Block a user