Last active
July 3, 2021 15:46
Star
You must be signed in to star a gist
[Far Manager macro] MultiPanel
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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