local Info = package.loaded.regscript or function(...) return ... end
local nfo = Info {_filename or ...,
name = "RecentScreens";
--переключение между окнами как в MS Windows™
description = "switch screens in MRU-order";
version = "3.11.1"; --http://semver.org/lang/ru/
author = "jd";
url = "http://forum.farmanager.com/viewtopic.php?f=60&t=7876";
id = "789DF383-23EE-4CF2-B274-7F3D387AEB98";
minfarversion = {3,0,0,5547,0}; --DN_LISTCHANGE
options = {
macroKey = "Tab", -- Ctrl-Tab / CtrlShift-Tab
--macroKey = "`", -- Ctrl-~ / CtrlShift~
--macroKey = "CapsLock", -- Ctrl-CapsLock / CtrlShift-CapsLock
-- default settings:
listDelay = 200, -- * delay before RecentScreens list show
-- 0 means no delay
-- <no list> means no list
listInstant = true, -- * in list:
-- true - switch screen on every Ctrl+Tab
-- false - switch only after Ctrl release
-- other options
fixAltDigitMacro = true,
hideDesktop = true,
debug = true,
};
help = function(self) far.Message(self.helpstr,self.name.." v"..self.version,nil,"kl") end;
helpstr = [[
Параметры, управляющие переключением,
можно изменить с помощью горячих клавиш
на цифровой клавиатуре:
[/] Instant - определяет момент переключения
- true - сразу, при каждом нажатии Ctrl+Tab
- false - после отпускания Ctrl
[*] Delay - задержка появления списка экранов
Плавно изменить: [+/-]
/* при <no list> список будет доступен по Ctrl+F12 */]];
--todo full help
--CtrlBS
--CtrlBackSlash
--Ctrl=
--CtrlEnter
--Enter/Esc/Break
--CtrlF8
--disabled = false;
}
if not nfo or nfo.disabled then return end
local O = nfo.options
local F = far.Flags
------------------------------------------------
--Settings
local _KEY,_NAME = "JD",nfo.name
local S -- current settings
local function defaultSettings()
return {
listDelay = O.listDelay,
listInstant = O.listInstant,
}
end
local function resetSettings()
if far.Message("\nDelete saved settings?\n",nfo.name,";OkCancel") then
mf.mdelete(_KEY,_NAME)
S = defaultSettings()
end
end
Event { description="[RecentScreens] Save settings (on exit)";
id="3B6F9E40-C470-4194-9DD3-4A76206ED8C8";
group="ExitFAR";
action=function() mf.msave(_KEY,_NAME,S) end;
}
S = mf.mload(_KEY,_NAME) --initial settings load
if not S or type(S)~="table" or type(S.listDelay)~="number" then
S = defaultSettings()
end
-------------------------------------------------------------
--- some general definitions
local function aKey() return mf.akey(1,1) end
local function Warning(msg)
far.Message(msg,"Warning","","w"); far.Text(); win.Sleep(500)
end
local Lock = {Num=0, Caps=1, Scroll=2}
local modeIsOn,keyIsPressed = 1,0xff80
local function LockState(xLock,Action) --set State: 0|1|2=toggle
--get State: nil|-1
local Lkey = assert(Lock[xLock])
local State = mf.flock(Lkey,-1)
local mode, hold = band(State,modeIsOn)==modeIsOn,
band(State,keyIsPressed)==keyIsPressed
if Action==2 or Action~=-1 and (mode==(Action==0)) then
if hold then mf.flock(Lkey,Action) end --http://forum.farmanager.com/viewtopic.php?p=139236#p139236
mf.flock(Lkey,Action)
end
return mode,hold
end
local _mod
-- can be rewrited using win.ExtractKeyEx and DN_INPUT (+DM_SETINPUTNOTIFY)
-- EventType==KEY_EVENT / Param2.ControlKeyState
local mod_mask = bor(F.RIGHT_CTRL_PRESSED,F.LEFT_CTRL_PRESSED)
local function ModReleased(set)
if set then
_mod = band(mod_mask,Mouse.LastCtrlState) -- state updates only after mf.waitkey / or in dialog event loop
elseif set==false then
_mod = false
else
return _mod and band(mod_mask,Mouse.LastCtrlState,_mod)~=_mod
end
end
local actl, GetWindowCount, GetWindowInfo, SetCurrentWindow
= far.AdvControl,F.ACTL_GETWINDOWCOUNT,F.ACTL_GETWINDOWINFO,F.ACTL_SETCURRENTWINDOW
local En = {
[F.WTYPE_PANELS]="Panels",[F.WTYPE_VIEWER]="View",[F.WTYPE_EDITOR]="Edit",
[F.WTYPE_DIALOG]="Dialog",[F.WTYPE_DESKTOP]=not O.hideDesktop and "Desktop",
}
local function Uid(w) --skip unsupported screen types
return En[w.Type]
and (w.Type~=F.WTYPE_DIALOG or band(w.Flags,F.WIF_MODAL)==0) --no modal dialogs
and En[w.Type]..tostring(w.Id)
end
--------------------------------------------------------
local Screens = { --array of windows' uid's in MRU order
--[1]="Desktop0",
--[2]="Panels0",
--[3]="Edit5",
uids = {
--["Desktop0"]=true,
--["Panels0"]=true,
--["Edit5"]=true,
},
--stateFreezeOrder=false,
}
function Screens:exist(uid)
return self.uids[uid]
end
function Screens:add(uid)
table.insert(self,uid)
self.uids[uid] = true
end
function Screens:Remove(uid,nonstrict)
if self:exist(uid) then
for i=1,#self do
if self[i]==uid then
table.remove(self,i)
self.uids[uid] = nil
return i
end
end
elseif nonstrict then --or self.stateFreezeOrder
return
end
if O.debug then Warning("uid do not exist: "..tostring(uid)) end
end
function Screens:Delta(d) return self[#self-d] end
function Screens:Focus(uid) -- move to top
if O.debug then
--win.OutputDebugString(table.concat(Screens,"|").."=>"..uid)
end
if self[#self]~=uid then
self:Remove(uid,"if any")
self:add(uid)
return true
end
end
function Screens.it_windows() --iterate over all screens / ret: i,uid,w
return function(n,i)
while(i<n) do
i = i+1
local w = actl(GetWindowInfo,i)
local uid = Uid(w)
if uid then return i,uid,w end
end
end,actl(GetWindowCount),0
end
function Screens:Goto(_uid) -- switch screen
for _,uid,w in self:it_windows() do
if uid==_uid then
if band(w.Flags,F.WIF_CURRENT)==0 then
actl(SetCurrentWindow,w.Pos)
end
return true
end
end
--
if O.debug then Warning("cannot find window "..tostring(_uid)) end
self:Remove(_uid)
end
--[[
function:Screens:Commit()
--for i=1,#self do self:Goto(self[i]) end
local windows = {}; for _,uid,w in self:it_windows() do windows[uid] = w end -- helper table (windows by uid)
local pos,n = {},#self; for i=1,n do pos[i] = windows[Screens[i] ].Pos end
for i=1,n do
local Pos = pos[i]
actl(SetCurrentWindow,Pos)
for j=i+1,n do
if pos[j]>Pos then pos[j] = pos[j]-1
end
end
end
--]]
-- limitations: https://forum.farmanager.com/viewtopic.php?p=123663#p123663
-- CtrlTab usually is served by Conemu itself
-- so we are unable to properly handle Panels/Desktop screens
-- thus MRU order will be incorrect
-- nevertheless, someone still wants it in conemu:
-- https://forum.farmanager.com/viewtopic.php?p=124349#p124349
-- so let them
-- but it's still better to disable some functions (MRUswithOnClose).. --todo
--[[
local function Conemu(param)
return Plugin.SyncCall("4B675D80-1D4A-4EA9-8436-FDC23F2FC14B",param)
end
--if win.GetEnv"ConEmuDir"
--if Conemu("IsConEmu")
--and O.macroKey=="Tab"
--!!check attach/detach
if akey():sub(-3)=="Tab" and Conemu("GetOption TabSelf")~="0" then
--
end
--]]
for _,uid in Screens:it_windows() do Screens:add(uid) end -- init Screens
---------------------------------------------------------------
-- Helper event-handlers
Event{ description="[RecentScreens] editor handler"; group="EditorEvent";
id="39C42CCD-E4E1-4165-AAAF-FD81D9C54E37";
action=function(id,Event)
if Event==F.EE_CLOSE then
local uid = "Edit"..id
Screens:Remove(uid)
elseif Event==F.EE_GOTFOCUS and not Screens.stateFreezeOrder then
local uid = "Edit"..id
Screens:Focus(uid)
end
end;
}
Event{ description="[RecentScreens] viewer handler"; group="ViewerEvent";
id="267160C4-7C26-4883-A126-D4EB081C254D";
action=function(id,Event)
if Event==F.VE_CLOSE then
local uid = "View"..id
Screens:Remove(uid,"skip QView,Info")
elseif Event==F.VE_GOTFOCUS and not Screens.stateFreezeOrder then
local uid = "View"..id
Screens:Focus(uid)
end
end;
}
Event{ description="[RecentScreens] dialog handler"; group="DialogEvent";
id="2FE435A8-52B6-47F6-8823-9F4D064B1DBA";
action=function(Event,param)
if Event~=F.DE_DLGPROCEND then return end
local Msg,hDlg,Param1 = param.Msg,param.hDlg,param.Param1
if Msg==F.DN_CLOSE then
local uid = "Dialog"..tostring(hDlg)
Screens:Remove(uid,"skip modals")
elseif Msg==F.DN_GOTFOCUS and Param1==-1 --nonmodals only
and not Screens.stateFreezeOrder then
local uid = "Dialog"..tostring(hDlg)
Screens:Focus(uid)
end
end;
}
-----------------------------------------------------------
-- RecentScreens menu logic helpers (see showRecentScreens)
local RSid = "92B73618-3C29-401C-BE62-B0406513CB82" -- RecentScreens menu id
local function isRecentScreensMenu() return Menu.Id==RSid end; --DM_GETDIALOGINFO
local STEP = 50 --ms
local NOLIST = -STEP
local function needSwitch() return S.listInstant or S.listDelay==NOLIST end
--!!2do reimplement menu via dialog
Event{ description="[RecentScreens] internal handler"; group="DialogEvent";
id="15FF5001-632B-4FB8-8A62-A0F629791369";
condition=function(Event) return Event==F.DE_DLGPROCINIT end;
action=function(_,Param)
local Msg,hDlg,Param2 = Param.Msg,Param.hDlg,Param.Param2
if Msg==F.DN_INPUT then -- close on mod release
if Param2.KeyDown==false and isRecentScreensMenu() and ModReleased() then
--hDlg:send(F.DM_KEY,nil,"Enter")
hDlg:send(F.DM_CLOSE)
end
elseif Msg==F.DN_LISTCHANGE then -- auto-switching
if needSwitch() and isRecentScreensMenu() then
mf.postmacro(Keys,"CtrlEnter") -- send BreakKey /close, switch window, and reopen/
end
end
end;
}
------------------------------------------------
-- RecentScreens-menu macros
Macro { description="[RecentScreens] Prev/Next";
area="Menu"; key="/^[LR]Ctrl(Shift)?(CapsLock|Tab|`)$/";
id="650C5D50-2750-4814-A12A-E7E749F4E322";
condition=isRecentScreensMenu;
action=function()
Keys(aKey():find"Shift" and "Up" or "Down")
end;
}
local NavKeys = "Up|Down|PgUp|PgDn|Num9|Num3|Home|End|Num7|Num1"
Macro { description="[RecentScreens] Prev/Next (additional)";
area="Menu"; key="/^[LR]Ctrl("..NavKeys..")|"..NavKeys.."$/";
id="F7ABABFA-A70D-4A26-A725-632040747F0A";
condition=isRecentScreensMenu;
action=function()
Keys(aKey():match"^R?Ctrl(.+)$" or "AKey")
end;
}
Macro { description="[RecentScreens] Scroll long titles";
area="Menu"; key="CtrlRight CtrlLeft";
id="BCD83367-88D6-4CAF-BF33-C2FF176D09AB";
condition=isRecentScreensMenu;
action=function() Keys("Alt"..aKey():match"Ctrl(.+)") end;
}
Macro { description="[RecentScreens] Goto [0-9A-Z]";
area="Menu"; key="/^[RL]Ctrl\\w$/";
id="A1428D9E-C5A0-46A8-8A1B-F5B0A2407FE1";
condition=isRecentScreensMenu;
action=function() Keys(aKey():sub(-1)) end;
}
-------------------------------------------
-- RecentScreens menu itself
local function getPanelInfo(name,typename)
local p = panel.GetPanelInfo(nil,1)
local ptype = p.PanelType
if ptype==F.PTYPE_FILEPANEL then
--https://forum.farmanager.com/viewtopic.php?f=8&t=11290
--unfortunately there is no strict order in plugins different plugins' info
--so to construct panel title we have to use heavy smart-ass euristics here(
name = name:match".*\\"
if band(p.Flags,F.PFLAGS_PLUGIN)~=0 then
--??2do: name from plugin panel
--??todo APanel.UNCPath
--??Object.Title
typename = "[plugin]"
local prefix = panel.GetPanelPrefix(nil,1) or ""
if prefix:len()>0 then
prefix = prefix:match"[^:]*"..":"
typename = prefix
end
local host = panel.GetPanelHostFile(nil,1) or ""
if host:len()>0 then
--??2do: truncate long host path, add plugin path
name = host
else
local format = panel.GetPanelFormat(nil,1) or ""
if format:len()>0 then
name = format.." - "..name
end
end
end
else
typename = ptype==F.PTYPE_INFOPANEL and "Info"
or ptype==F.PTYPE_QVIEWPANEL and "QView"
or ptype==F.PTYPE_TREEPANEL and "Tree"
--todo QView
name = APanel.Path --https://bugs.farmanager.com/view.php?id=3639
end
return name,typename,band(p.Flags,F.PFLAGS_VISIBLE)==0 and "-"
or band(p.Flags,F.PFLAGS_PANELLEFT)~=0 and "<" or ">"
end
local function prepItem(w,i,modal)
local name,typename = w.Name,w.TypeName
local current = band(w.Flags,F.WIF_CURRENT)~=0
local mark,checked,key
-- =≡■
if w.Type==F.WTYPE_PANELS then
checked,key = "≡","BackSlash"
name,typename,mark = getPanelInfo(name,typename)
elseif w.Type==F.WTYPE_DESKTOP then
checked = ""
elseif w.Type==F.WTYPE_EDITOR then
mark = band(w.Flags,F.WIF_MODIFIED)~=0 and "*"
local title = editor.GetTitle(w.Id) --Показывать заголовки редакторов в меню Screens вместо путей
if title~=name then name = title.." - "..name end --https://bugs.farmanager.com/view.php?id=3316
end
--local hk = w.Pos-1 --"Pos" used to be same as Screens #hotkey
-- not anymore http://forum.farmanager.com/viewtopic.php?f=54&t=8883&p=125016#p125016
local hk = i
hk = hk<=9 and hk or hk<=35 and string.char(("A"):byte()+hk-10)
hk = hk and "&"..hk.."." or ""
return {
text = ("%2s %-8s %1s %s"):format(hk,typename,mark or "",name),
checked=band(w.Flags,F.WIF_MODAL)~=0 and "!" or checked,
disable=modal and not current,
selected=current,
AccelKey=key, --https://bugs.farmanager.com/view.php?id=3471
}
end
local BreakKeys = {
{BreakKey="CtrlEnter"}, --no action, need for menu close/reopen and window auto-switching
{BreakKey="CtrlDivide", action=function() S.listInstant = not S.listInstant; end},
{BreakKey="CtrlAdd", action=function() S.listDelay = S.listDelay<0 and 0 or S.listDelay+STEP; end},
{BreakKey="CtrlSubtract",action=function() S.listDelay = S.listDelay<0 and NOLIST or S.listDelay-STEP; end},
{BreakKey="CtrlMultiply",action=function() --cycle through listDelay values
S.listDelay = S.listDelay==NOLIST and 0
or S.listDelay==0 and O.listDelay --default
or S.listDelay>0 and NOLIST
end},
{BreakKey="F1", action=function() nfo:help() end},
}
local CommonKeys = { --!!to doc
CtrlF8 = resetSettings;
CtrlF1 = function(self) nfo:help(); self.br = ModReleased() end;
CtrlBS = function() return Screens:Delta(0) end;
CtrlO = not O.hideDesktop
and function() return "Desktop0" end or nil;
CtrlBackSlash = panel.CheckPanelsExist()
and function() return "Panels0" end or nil;
}
for k,v in pairs(CommonKeys) do table.insert(BreakKeys,{BreakKey=k,action=v}) end
local Props = {Title="RecentScreens",Id=win.Uuid(RSid),Flags=F.FMENU_WRAPMODE}
local Bottom = ("[F1] Help [/] Instant: %-5s [*/+/-] Delay: %9s")
local function _shift(key)
return key:find("Down$") and -1
or key:find("^R?CtrlShift") and -1
or key:find("Up$") and 1
or key:find("^R?Ctrl") and 1
or 0
end
local function _align(pos)
return pos>#Screens and 1 or pos<1 and #Screens or pos
end
local function RS_menu(key,is_modal) --switch far screens (shows list)
--??idea: show all windows even if nouid
local windows = {} -- helper table (windows by uid)
for _,uid,w in Screens:it_windows() do windows[uid] = w end
local Items = {}
for i=#Screens,1,-1 do
table.insert(Items,prepItem(windows[Screens[i]],i,is_modal))
end
local pos = not is_modal and key and _align(1+_shift(key))
local uid
repeat
if needSwitch() and not is_modal and pos then
Screens:Goto(Screens:Delta(pos-1))
end
Props.Bottom = Bottom:format(
tostring(S.listInstant),
S.listDelay==NOLIST and "<no list>" or S.listDelay.." ms"
)
Props.SelectIndex = pos
local item
item,pos = far.Menu(Props,Items,BreakKeys)
if item and item.action then --if BreakKey
uid = item:action()
end
until not item -- menu cancelled (Esc, CtrlBreak ets) / pos==nil
or item.text -- item selected (Enter pressed etc) / item==Items[pos]
or item.br -- BrK: currently active item will be selected
or uid and not is_modal -- BrK
if not is_modal then
return uid
or Screens:Delta((pos or 1)-1) -- no pos if dialog canceled (Ctrl-Break, Esc, ...)
-- !!pos==0 if all items disabled
end
end
---------------------------
-- switch without menu
local time
local function timeout(new)
if S.listDelay==NOLIST then return end --no-menu mode does not need timeout
local uptime = Far.UpTime
if not new then return uptime-time>S.listDelay end
time = uptime + S.listDelay
end
local function RS_nomenu(key) --switch far screens (no list)
timeout"new"
local Pos,uid = #Screens
mmode(1,0) --enable output
repeat -- NB: new loop every 50 ms
if key=="" and timeout() or key=="CtrlF12" then
return
elseif key~="" then
if CommonKeys[key] then
uid = CommonKeys[key]{}
else
Pos = _align(Pos-_shift(key))
Screens:Goto(Screens[Pos])
end
timeout"new"
end
key = uid or mf.waitkey(50)
until uid or ModReleased()
return uid or Screens[Pos]
end
----------------------------------------
-- main
local function need_manual_tracking(w)
return w.Type~=F.WTYPE_DIALOG and w.Type~=F.WTYPE_EDITOR and w.Type~=F.WTYPE_VIEWER
end
local function RecentScreens(mode)
local nomenu,key
if mode then
key = aKey()
nomenu = mode=="switch w/o menu"
end
local w = assert(actl(GetWindowInfo))
local uid = Uid(w)
if uid and need_manual_tracking(w) then
Screens:Focus(uid)
end
local is_modal = band(w.Flags,F.WIF_MODAL)~=0 or not uid
ModReleased(key and "set current" or false)
Screens.stateFreezeOrder = true
local target_uid
if not is_modal and nomenu and S.listInstant and S.listDelay~=0 and #Screens~=1 then
target_uid = RS_nomenu(key,is_modal)
key = false
end
if not target_uid then
target_uid = RS_menu(key,is_modal)
end
if target_uid then
Screens:Focus(target_uid)
Screens:Goto(target_uid)
end
Screens.stateFreezeOrder = false
end
nfo.config = function() RecentScreens() end;
---------------------------------------------
-- Main macro
local allScreens = "Shell Info QView Tree Editor Viewer Dialog "
if not O.hideDesktop then allScreens = allScreens.."Desktop " end
local function has_uid()
return Uid(assert(actl(GetWindowInfo)))
end
if O.macroKey then
Macro { description="[RecentScreens] switch";
area=allScreens; key="/^LCtrl(Shift)?("..O.macroKey..")$/";
id="792800D3-DD8A-4F5D-A4A8-1004E6AFBE7E";
condition=has_uid;
action=function()
RecentScreens"switch w/o menu"
if O.macroKey=="CapsLock" then LockState("Caps",0) end
end;
}
end
NoMacro { description="[RecentScreens] switch & show list";--https://forum.farmanager.com/viewtopic.php?p=121183#p121183
area=allScreens; key="/^LCtrl(Shift)?CapsLock$/";
id="3DC8C55E-376D-4ED5-8C42-FF3E30F0AD68";
condition=has_uid;
action=function()
RecentScreens"switch"
LockState("Caps",0)
end;
}
Macro { description="[RecentScreens] show list";
area="Common"; key="CtrlShiftF12";
id="40CA4C44-03C3-426A-8D25-0F882E7A50E5";
condition=function() return not isRecentScreensMenu() end;
action=function()
RecentScreens()
end;
}
MenuItem{ --!!test
menu= "Plugins Disks Config"; area="Common";
description=nfo.description;
text=function() return not isRecentScreensMenu() and nfo.name end;
guid="5019E6EC-1CD0-4C1F-91D7-DB505DC60ED6";
action=function() RecentScreens() end;
}
if O.fixAltDigitMacro then
Macro { description="[RecentScreens] fix MRU for Addons/Macros/AltScreens.lua";
area=allScreens.."Desktop "; key="/[LR]Alt[0-9]/";
id="67124A52-B6F8-448D-BF9D-846423785181";
condition=function()
local uid = Uid(assert(actl(GetWindowInfo)))
if uid then Screens:Focus(uid) end
end;
action=function()end;
}
end
--todo check Screens:Commit
--todo: сделать лайт-версию (после 4138 известен z-order, что позволяет не хранить порядок)