Skip to content

Instantly share code, notes, and snippets.

@johnd0e
Last active July 3, 2021 15:46
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save johnd0e/3a8637378eb3415ea28b82f51cbf9f8a to your computer and use it in GitHub Desktop.
[Far Manager macro] MultiPanel
local Info = Info or package.loaded.regscript or function(...) return ... end --luacheck: ignore 113/Info
local nfo = Info { _filename or ...,
name = "MultiPanel";
description = "Store and switch between multiple file panels";
version = "0.5.1"; --http://semver.org/lang/ru/
author = "jd";
url = "http://forum.farmanager.com/viewtopic.php?f=15&t=8759";
id = "75950345-29C3-49DB-804D-8AA96345C1B2";
minfarversion = {3,0,0,4151,0}; -- local profile
help = function(nfo)
far.Message("\nSee Bottom title for hotkeys\n\n[...]\n",nfo.name.." v"..nfo.version,nil,"kl")
end;
--disabled = false;
options = {
macroKey = "`", -- Ctrl~ / CtrlShift~
popupDelayLong=1000,
popupDelay=300,
};
}
if not nfo or nfo.disabled then return end
local O = nfo.options
local F = far.Flags
local DefPanel = {
Directory = {
Name=win.GetEnv"SystemDrive".."\\",
File="",Param="",PluginId=win.Uuid"00000000-0000-0000-0000-000000000000",
},
Info = {
SortMode=F.SM_EXT,--http://api.farmanager.com/ru/defs/sortmetods.html
Flags=F.PFLAGS_NONE,--F.PFLAGS_REVERSESORTORDER,
ViewMode=2,
}
}
local g = win.Uuid
local notRealHostFile = {
[g"42E4AEB1-A230-44F4-B33C-F195BB654931"] = "NetBox",
}
local openFromMenu = {
[g"1E26A927-5135-48C6-88B2-845FB8945484"] = "Process list",
[g"B77C964B-E31E-4D4C-8FE5-D6B0C6853E7C"] = "Temporary panel",
}
-------------------------------------------------------------
--- some general definitions
local function AKey() return mf.akey(1,1) end
local function LockState(xLock,State) --set State: 0|1|2=toggle
--get State: nil|-1
local Lock = {Num=0, Caps=1, Scroll=2}
local modeIsOn,keyIsPressed = 1,0xff80
local r = mf.flock(assert(Lock[xLock]),State or -1)
return band(r,modeIsOn)==modeIsOn,
band(r,keyIsPressed)==keyIsPressed
end
-- NumLock,ScrollLock,CapsLock = 0x20,0x40,0x80
local anyCtrl = bor(F.RIGHT_CTRL_PRESSED,F.LEFT_CTRL_PRESSED)
local function ModState(Mod) return band(Mouse.LastCtrlState,Mod) end -- State after last mf.waitkey!!
local function MessagePopup(msg,title,flags,delay)
if flags and flags:find("w") then
mf.beep()
delay = delay or O.popupDelayLong
end
local s = far.SaveScreen()
far.Message(msg,title or nfo.name,"",flags)
far.Text()
win.Sleep(delay or O.popupDelay); far.RestoreScreen(s)
end
--[[
local function eq(a,b) --simple compare tables
for k,v in pairs(a) do if b[k]~=v then return false end end
return true
end
--]]
--------------------------------------------------------
-- MultiPanel panel api logic
local ACTIVE,PASSIVE = F.PANEL_ACTIVE,F.PANEL_PASSIVE
local function getSelection(p,pInfo)
if pInfo.SelectedItemsNumber==1 then
local item = panel.GetSelectedPanelItem(p,nil,1)
return band(item.Flags,F.PPIF_SELECTED)~=0 and {item.FileName}
elseif pInfo.SelectedItemsNumber>0 then
local Sel = {}
for i=1,pInfo.SelectedItemsNumber do
Sel[i] = panel.GetSelectedPanelItem(p,nil,i).FileName
end
return Sel
end
end
local function setSelection(p,Sel)
if not Sel then return end
local idx = {}
for i=1,panel.GetPanelInfo(p).ItemsNumber do
local FileName = panel.GetPanelItem(p,nil,i).FileName
for j=1,#Sel do
if Sel[j]==FileName then idx[#idx+1]=i end
end
end
panel.BeginSelection(p)
panel.SetSelection(p,nil,idx,true) --https://bugs.farmanager.com/view.php?id=3766
panel.EndSelection(p)
--panel.RedrawPanel(p)
end
--------------------------------------------------------
-- MultiPanel helpers
local stack = {currentHK=false}
function stack:_makeHK()
local used = {[self.currentHK or ""]=true}
for i=1,#self do used[self[i].HK or ""] = true end
local X=mf.xlat
for hk in ("0123456789ABCDEFGHIJKLMOPQRSTUVWXYZ"):gmatch(".") do
if not (used[hk] or used[hk:lower()] or used[X(hk)] or used[X(hk:lower())]) then
return hk
end
end
end
function stack:_get(p)
local pInfo = panel.GetPanelInfo(p)
local pnl = {}
if band(pInfo.Flags,F.PFLAGS_PLUGIN)==F.PFLAGS_PLUGIN then
pnl = {
isPlugin=true,
Format=panel.GetPanelFormat(p),
Prefix=panel.GetPanelPrefix(p),
}
if band(pInfo.Flags,F.PFLAGS_SHORTCUT)~=F.PFLAGS_SHORTCUT then
pnl.openFromMenu = openFromMenu[pInfo.OwnerGuid]
pnl.notSupported = not pnl.openFromMenu
end
end
pnl.Info=pInfo
pnl.Sel=getSelection(p,pInfo)
pnl.Directory=panel.GetPanelDirectory(p)
pnl.HK = p==ACTIVE and self.currentHK or self:_makeHK()
return pnl
end
function stack:_set(p,idx,pnl)
pnl = pnl or self[idx]
--if not pnl then return end --debug
if pnl.openFromMenu then
Plugin.Menu(g(pnl.Info.OwnerGuid))
elseif not pnl.Directory or not panel.SetPanelDirectory(p,nil,pnl.Directory) then
--todo: blacklist NetBox: Stored sessions
MessagePopup("oops",nil,"w")
return
end
--if not eq(panel.GetPanelDirectory(p),pnl.Directory) then le({panel.GetPanelDirectory(p),pnl.Directory}) end
--assert(eq(panel.GetPanelDirectory(p),pnl.Directory)) --debug APPs~=Apps
self.currentHK = pnl.HK
local pInfo = pnl.Info
panel.SetSortMode(p,nil,pInfo.SortMode)
local Flags = pInfo.Flags
panel.SetSortOrder(p,nil,band(Flags,F.PFLAGS_REVERSESORTORDER)~=0)
panel.SetViewMode(p,nil,pInfo.ViewMode)
setSelection(p,pnl.Sel)
panel.RedrawPanel(p,nil,pInfo)
return true
end
--------------------------------------------------------
-- MultiPanel stack logic
function stack:Push(p,pnl,new)
pnl = pnl or self:_get(p)
if pnl.notSupported then return false, pnl end
table.insert(self,1,pnl)
if new then self.currentHK = self:_makeHK() end
return true
end
function stack:Clone(idx)
local pnl=self[idx]
local copy = {}; for k,v in pairs(pnl) do copy[k] = v end
copy.HK = self:_makeHK()
table.insert(self,idx,copy)
end
function stack:Pop(idx,p)
if self:_set(p,idx) then
return table.remove(self,idx)
end
end
function stack:Switch(idx,p,pnl) --push p/pnl and switch to i
local pushed = self:Push(p,pnl)
--[[
if not pushed then
-- ??warning msg?
-- ??or try to store underlying panel
panel.ClosePanel(p)
panel.RedrawPanel(p)
pushed = self:Push(p,pnl)
end
--]]
idx = pushed and idx+1 or idx
if not self:Pop(idx,p) and pushed then
-- or eq(panel.GetPanelDirectory(p),pnl.Directory) -- ??check: setPanel may return false even on success
return table.remove(self,1)
end
end
function stack:Close(p,pnl)
pnl = pnl or self:_get(p)
if pnl.isPlugin then
panel.ClosePanel(p)
--panel.RedrawPanel(p)--??
else
self:_set(p,nil,DefPanel)
end
end
function stack:Restore(p,pnl)
if #self>0 then
self:Pop(1,p)
else
self:Close(p,pnl)
end
end
setmetatable(stack,{__index=function(_,k)
error("out of bounds index: "..k)
end})
local mtPanelsStack = {__index=stack}
--load/save
local Panels
local _KEY,_NAME = nfo.author:upper(),nfo.name
local function delSettings()
mf.mdelete(_KEY,_NAME,"local")
end
local function saveSettings()
mf.msave(_KEY,_NAME,{Panels=Panels},"local")
end
local function loadSettings()
local S = mf.mload(_KEY,_NAME,"local") or {}
Panels = S.Panels or Panels or {currentHK=0}
setmetatable(Panels,mtPanelsStack)
end
loadSettings()
Event { description="[MultiPanel] Save settings (on exit)";
group="ExitFAR";
action=saveSettings;
}
local MPid = "D3656A7D-A75F-40F1-AB9B-59D962CE2224"
local function isMultiPanelMenu() return Menu.Id==MPid end;
local closeOnRelease
Event{ description="[MultiPanel] (close on release)"; group="DialogEvent";
condition=function(Event,Param)
return closeOnRelease
and Event==F.DE_DLGPROCINIT and Param.Msg==F.DN_INPUT and Param.Param2.KeyDown==false
and isMultiPanelMenu() and ModState(anyCtrl)==0
end;
action=function(_,Param)
Param.hDlg:send("DM_CLOSE",-1)
end;
}
------------------------------------------------
-- MultiPanel-menu macros
Macro { description="[MultiPanel] Prev/Next";
area="Menu"; key="/^[LR]Ctrl(Shift)?(CapsLock|Tab|`)$/";
condition=isMultiPanelMenu;
action=function() Keys(AKey():find"Shift" and "Up" or "Down") end;
}
Macro { description="[MultiPanel] Prev/Next (additional)";
area="Menu"; key="/^[LR]Ctrl(Up|Down|PgUp|PgDn|Num9|Num3)$/";
condition=isMultiPanelMenu;
action=function() Keys(AKey():match"^R?Ctrl(.+)$") end;
}
Macro { description="[MultiPanel] Scroll long titles";
area="Menu"; key="CtrlRight CtrlLeft";
condition=isMultiPanelMenu;
action=function() Keys("Alt"..AKey():match"Ctrl(.+)") end;
}
Macro { description="[MultiPanel] Goto [0-9A-Z]";
area="Menu"; key="/^[RL]Ctrl\\w$/";
condition=isMultiPanelMenu;
action=function() Keys(AKey():sub(-1)) end;
}
local TypeW = 10
local function prepareMenuItems()
local Items = {}
for i=0,#Panels do
local pnl = i~=0 and Panels[i] or Panels:_get(ACTIVE)
local Dir = pnl.Directory or {}; Dir.File = Dir.File or ""; Dir.Name = Dir.Name or "" -- protect from bad data
local file = Dir.File:match"[^\\]+$"
local Name = Dir.Name:match".+" or "[Root]"
local path,Type
if pnl.isPlugin then
Type = pnl.Prefix:match"^[^:]*"
Name = (file and file or pnl.Format)..":"..Name or Name
if Dir.File~="" and not notRealHostFile[Dir.PluginId] then
path = Dir.File
end
else
Type = "Panel"
path = Dir.Name
end
Type = Type..(" "):rep(TypeW-Type:len())
local notfound = path and not win.GetFileAttr(path)
local Mark = notfound and "!" or pnl.Sel and "*"
local hk = pnl.HK and "&"..pnl.HK or ""
local text = ("%2s %-$s %s %s"):gsub("%$",TypeW)
:format(hk,Type,Mark or " ",Name) --Sel
Items[i+1] = {
pnl=pnl,
text=text,
checked = not pnl.isPlugin and "", -- =≡■
grayed = pnl.notSupported or notfound,
--disable
--hidden
}
end
return Items
end
local Items
local function _align(pos)
return pos>#Items and 1 or pos<1 and #Items or pos
end
local function expandBreakKeys(BreakKeysArr)
local newBreakKeysArr = {}
for _,item in ipairs(BreakKeysArr) do
for key in item.BreakKey:gmatch("%S+") do
local newitem = {}
for k,v in pairs(item) do newitem[k]=v end
newitem.BreakKey = key
table.insert(newBreakKeysArr,newitem)
end
end
return newBreakKeysArr
end
local BreakKeys = expandBreakKeys { --http://msdn.microsoft.com/library/dd375731
{BreakKey="RETURN", action=function(pos)
if pos>1 then
Panels:Switch(pos-1,ACTIVE,Items[1].pnl)
end
return "break"
end},
{BreakKey="C+RETURN", action=function(pos)
if pos>1 then
Panels:Switch(pos-1,PASSIVE,nil)
else
Keys"CtrlU Tab"
end
return "break"
end},
{BreakKey="C+CONTROL",br=true},
{BreakKey="C+ESCAPE ESCAPE",br=true},
{BreakKey="C+F3 F3", action=function(pos)
if pos==1 then
Panels:Push(ACTIVE,Items[1].pnl,"new")
else
Panels:Clone(pos-1)
end
Items = prepareMenuItems()
end},
{BreakKey="C+F4 F4", action=function(pos)
if pos==1 then
Panels:Restore(ACTIVE,Items[1].pnl)
else
table.remove(Panels,pos-1)
end
Items = prepareMenuItems()
end},
{BreakKey="CS+F9 S+F9",action=saveSettings},
{BreakKey="C+F9 F9", action=function()
if far.Message("Reload settings?",nfo.name,";OkCancel") then
loadSettings()
Items = prepareMenuItems()
end
end},
{BreakKey="C+F8 F8", action=function()
if far.Message("Delete saved settings?",nfo.name,";OkCancel") then
delSettings()
Panels = nil
loadSettings()
Items = prepareMenuItems()
end
end},
{BreakKey="C+F1 F1", action=function()
nfo:help()
return closeOnRelease and ModState(anyCtrl)==0
end},
}
local Props = {Title="MultiPanel",Id=win.Uuid(MPid),Flags=F.FMENU_WRAPMODE}
local Bottom = "F3:Clone, F4:Close, Release:Switch, Ctrl-Enter:..on passive"
local function showMultiPanel(option)
Items = prepareMenuItems()
local item = Items[1]
local pos = 1
if not (item.grayed or option) then
pos = _align(2)
end
if not (option=="donotcloseOnRelease" or ModState(anyCtrl)==0) then closeOnRelease = true end
repeat
Props.Bottom = Bottom
Props.SelectIndex = pos
item,pos = far.Menu(Props, Items, BreakKeys)
if not item.BreakKey then
item = BreakKeys[1] --Enter
end
local br = item.action and item.action(pos)
until br or item.br
closeOnRelease = false
return pos
end
nfo.execute=function()
showMultiPanel"donotcloseOnRelease"
end;
-- todo
-- sticky folders
-- Help
-- Info QView Tree
-- autoswitch?
-- Multi-panel plugin specific features?
--local allPanels = "Shell Info QView Tree Search"
local allPanels = "Shell Search"
Macro { description="[MultiPanel] switch";
area=allPanels; key="/^LCtrl(Shift)?("..O.macroKey..")$/";
action=function()
showMultiPanel()
if O.macroKey=="CapsLock" then LockState("Caps",0) end
end;
}
NoMacro { description="[MultiPanel] toggle";
area=allPanels; key="LCtrl";
action=function()
showMultiPanel"donotcloseOnRelease"
end;
}
Macro { description="[MultiPanel] goto Panels";
area="Editor Viewer"; key="/^LCtrl(Shift)?("..O.macroKey..")$/";
action=function()
Keys"F12 1"
if Area.Shell then showMultiPanel"current" end
if O.macroKey=="CapsLock" then LockState("Caps",0) end
end;
}
local function Push(p)
local success, pnl = Panels:Push(p,nil,p==ACTIVE)
if success then
MessagePopup("Storing panel...")
else
local msg = ("Unable to store this plugin panel: %s"):format(pnl.Format)
MessagePopup(msg,nil,"w")
end
end
Macro { description="[MultiPanel] store active panel";
area=allPanels; key="LCtrlF3:Hold";
action=function()
Push(ACTIVE)
end;
}
Macro { description="[MultiPanel] store passive panel";
area=allPanels; key="LCtrlShiftF3:Hold";
action=function()
Push(PASSIVE)
end;
}
Macro { description="[MultiPanel] restore prev. panel";
area=allPanels; key="LCtrlF4:Hold";
action=function()
Panels:Restore(ACTIVE)
MessagePopup("Closing panel...")
end;
}
Macro { description="[MultiPanel] restore prev. panel as passive";
area=allPanels; key="LCtrlShiftF4:Hold";
action=function()
Panels:Restore(PASSIVE)
MessagePopup("Closing panel...")
end;
}
Macro { description="[MultiPanel] close active panel";
area=allPanels; key="LCtrlF8:Hold";
action=function()
Panels:Close(ACTIVE)
end;
}
Macro { description="[MultiPanel] close passive panel";
area=allPanels; key="LCtrlShiftF8:Hold";
action=function()
Panels:Close(PASSIVE)
end;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment