-- Copyright 2011-16 Paul Kulchenko, ZeroBrane LLC -- authors: Lomtik Software (J. Winwood & John Labenski) -- Luxinia Dev (Eike Decker & Christoph Kubisch) --------------------------------------------------------- local ide = ide local layoutlabel = { UIMANAGER = "uimgrlayout", NOTEBOOK = "nblayout", NOTEBOOKOUTPUT = "nbbtmlayout", NOTEBOOKPROJECT = "nbprojlayout", DOCKNOTEBOOK = "nbdocklayout", DOCKNOTEBOOKOUTPUT = "nbbtmdocklayout", DOCKNOTEBOOKPROJECT = "nbprojdocklayout", STATUSBAR = "statusbar", } -- ---------------------------------------------------------------------------- -- Initialize the wxConfig for loading/saving the preferences local ini = ide.config.ini -- if ini path is relative and includes a directory name, make it relative to the IDE location ini = ini and (not wx.wxIsAbsolutePath(ini) and wx.wxFileName(ini):GetDirCount() > 0 and MergeFullPath(GetPathWithSep(ide.editorFilename), ini) or ini) -- check that the ini file doesn't point to a directory if ini and (wx.wxFileName(ini):IsDir() or wx.wxIsAbsolutePath(ini) and wx.wxDirExists(ini)) then ide:Print(("Can't use 'ini' configuration setting '%s' that points to a directory instead of a file; ignored.") :format(ini)) ini = nil end -- check that the directory is writable if ini and wx.wxIsAbsolutePath(ini) and not wx.wxFileName(ini):IsDirWritable() then ide:Print(("Can't use 'ini' configuration setting '%s' that points to a non-writable directory; ignored.") :format(ini)) ini = nil end local settings = wx.wxFileConfig( ide:GetProperty("settingsapp"), ide:GetProperty("settingsvendor"), ini or "") ide.settings = settings local function settingsReadSafe(settings,what,default) local cr,out = settings:Read(what,default) return cr and out or default end -- ---------------------------------------------------------------------------- -- wxConfig load/save preferences functions function SettingsRestoreFramePosition(window, windowName) local path = settings:GetPath() settings:SetPath("/"..windowName) local clientX, clientY, clientWidth, clientHeight = wx.wxClientDisplayRect() local s = tonumber(select(2,settings:Read("s", -1))) local w = tonumber(select(2,settings:Read("w", math.floor(clientWidth*0.8)))) local h = tonumber(select(2,settings:Read("h", math.floor(clientHeight*0.8)))) local x = tonumber(select(2,settings:Read("x", clientX+math.floor(clientWidth-w)/2))) local y = tonumber(select(2,settings:Read("y", clientY+math.floor(clientHeight-h)/2))) if (s ~= -1) then local clientX, clientY, clientWidth, clientHeight = wx.wxClientDisplayRect() -- if left-top corner outside of the left-top side, reset it to the screen side if x < clientX then x = clientX end if y < clientY then y = clientY end -- if the window is too wide for the screen, reset it to the screen size if w > clientWidth then w = clientWidth end if h > clientHeight then h = clientHeight end -- if the right-bottom corner is still outside and there is only one display, -- then reposition left-top corner, keeping the window centered if wx.wxDisplay():GetCount() == 1 then local outx = (x + w) - (clientX + clientWidth) local outy = (y + h) - (clientY + clientHeight) if outx > 0 then x = math.floor(0.5+(x - outx)/2) end if outy > 0 then y = math.floor(0.5+(y - outy)/2) end end window:SetSize(x, y, w, h) end -- maximize after setting window position to make sure it's maximized on the correct monitor if s == 1 then window:Maximize(true) end settings:SetPath(path) end function SettingsSaveFramePosition(window, windowName) local path = settings:GetPath() settings:SetPath("/"..windowName) local s = 0 local w, h = window:GetSizeWH() local x, y = window:GetPositionXY() if window:IsMaximized() then s = 1 elseif window:IsIconized() then s = 2 end settings:Write("s", s==2 and 0 or s) -- iconized maybe - but that shouldnt be saved settings:Write("x", x) settings:Write("y", y) settings:Write("w", w) settings:Write("h", h) settings:SetPath(path) end --- -- (table) SettingsRestoreFileHistory (function) -- restores a list of recently loaded documents from the settings table -- a table is returned which contains tables each with a filename key, pointing to -- the filename function SettingsRestoreFileHistory(fntab) local path = settings:GetPath() local listname = "/filehistory" settings:SetPath(listname) local outtab = {} local inlist = {} for id=1,ide.config.filehistorylength do local couldread, name = settings:Read(tostring(id), "") if not couldread or name == "" then break end if not inlist[name] then inlist[name] = true table.insert(outtab,{filename = name}) end end if fntab then fntab(outtab) end settings:SetPath(path) return outtab end function SettingsSaveFileHistory (filehistory) local listname = "/filehistory" local path = settings:GetPath() settings:DeleteGroup(listname) settings:SetPath(listname) for i,doc in ipairs(filehistory) do settings:Write(tostring(i), doc.filename) end settings:SetPath(path) end --- -- () SettingsRestoreFileSession (function [, string section]) -- restores a list of opened files from the file settings -- calls the given function with the restored table, a list -- of tables containing tables like -- {filename = "filename", cursorpos = } function SettingsRestoreFileSession(fntab, section) local listname = section or "/session" local path = settings:GetPath() settings:SetPath(listname) local outtab = {} local params = {} local ismore, key, index = settings:GetFirstEntry("", 0) while (ismore) do local couldread, value = settings:Read(key, "") if tonumber(key) then local fname,cursorpos = value:match("^(.+);(.-)$") if (couldread and value ~= "") then outtab[tonumber(key)] = {filename = fname or value, cursorpos = tonumber(cursorpos) or 0} end else params[key] = tonumber(value) or value end ismore, key, index = settings:GetNextEntry(index) end if fntab then fntab(outtab, params) end settings:SetPath(path) return outtab end --- -- () SettingsSaveFileSession (table opendocs, table params [, string section]) -- saves the list of currently opened documents (passed in the opendocs table) -- in the settings. function SettingsSaveFileSession(opendocs, params, section) local listname = section or "/session" local path = settings:GetPath() settings:DeleteGroup(listname) settings:SetPath(listname) for i,doc in ipairs(opendocs) do settings:Write(tostring(i), doc.filename..";"..doc.cursorpos) end -- save all other parameters for k,v in pairs(params) do settings:Write(k, v) end settings:SetPath(path) end --- -- () SettingsRestoreProjectSession (function) function SettingsRestoreProjectSession(fntab) local listname = "/projectsession" local path = settings:GetPath() settings:SetPath(listname) local outtab = {} local couldread = true local id = 1 local name while (couldread) do couldread, name = settings:Read(tostring(id), "") couldread = couldread and name ~= "" if (couldread) then if (wx.wxDirExists(name)) then table.insert(outtab,name) local function projsession(...) ProjectConfig(name, {...}) end SettingsRestoreFileSession(projsession, listname .. "/" .. tostring(id)) end id = id + 1 end end if fntab then fntab(outtab) end settings:SetPath(path) return outtab end --- -- () SettingsSaveProjectSession (table projdirs) -- saves the list of currently active projects -- in the settings. function SettingsSaveProjectSession(projdirs) local listname = "/projectsession" local path = settings:GetPath() settings:DeleteGroup(listname) settings:SetPath(listname) for i,dir in ipairs(projdirs) do settings:Write(tostring(i), dir) local opendocs, params = ProjectConfig(dir) if opendocs then SettingsSaveFileSession(opendocs, params, listname .. "/" .. tostring(i)) end end settings:SetPath(path) end function SettingsRestorePackage(package) local packagename = "/package/"..package local path = settings:GetPath() settings:SetPath(packagename) local outtab = {} local ismore, key, index = settings:GetFirstEntry("", 0) while (ismore) do local couldread, value = settings:Read(key, "") if couldread then local ok, res = LoadSafe("return "..value) if ok then outtab[key] = res else outtab[key] = nil ide:Print(("Couldn't load and ignored '%s' settings for package '%s': %s") :format(key, package, res)) end end ismore, key, index = settings:GetNextEntry(index) end settings:SetPath(path) return outtab end function SettingsSavePackage(package, values, opts) local packagename = "/package/"..package local path = settings:GetPath() settings:DeleteGroup(packagename) settings:SetPath(packagename) for k,v in pairs(values or {}) do settings:Write(k, DumpPlain(v, opts)) end settings:SetPath(path) end ----------------------------------- local function saveNotebook(nb) local cnt = nb:GetPageCount() local function addTo(tab,key,value) local out = tab[key] or {} table.insert(out,value) tab[key] = out end local pagesX = {} local pagesY = {} local str = "nblayout|" for i=1,cnt do local pg = nb:GetPage(i-1) local doc = ide:GetDocument(pg) local id = doc and doc:GetTabText() or nb:GetPageText(i-1) local x,y = pg:GetPosition():GetXY() addTo(pagesX,x,id) addTo(pagesY,y,id) end local function sortedPages(tab) local t = {} for i in pairs(tab) do table.insert(t,i) end table.sort(t) return t end local sortedX = sortedPages(pagesX) local sortedY = sortedPages(pagesY) -- for now only support "1D" splits and prefer -- dimension which has more, anything else -- requires a more complex algorithm, yet to do local pagesUse local sortedUse local split if ( #sortedX >= #sortedY) then pagesUse = pagesX sortedUse = sortedX split = "" else pagesUse = pagesY sortedUse = sortedY split = "" end for _, v in ipairs(sortedUse) do local pages = pagesUse[v] for _, id in ipairs(pages) do str = str..id.."|" end str = str..split.."|" end return str end local function loadNotebook(nb,str,fnIdConvert) str = str:match("nblayout|(.+)") if (not str) then return end local cnt = nb:GetPageCount() local sel = nb:GetSelection() -- store old pages local currentpages, order = {}, {} for i=1,cnt do local pg = nb:GetPage(i-1) local doc = ide:GetDocument(pg) local id = doc and doc:GetTabText() or nb:GetPageText(i-1) local newid = fnIdConvert and fnIdConvert(id) or id currentpages[newid] = currentpages[newid] or {} table.insert(currentpages[newid], {page = pg, text = id, index = i-1}) order[i] = newid end -- remove them for i=cnt,1,-1 do nb:RemovePage(i-1) end -- read them and perform splits local t = 0 local newsel local function finishPage(page) if (page.index == sel) then newsel = t end t = t + 1 end local direction local splits = { X = wx.wxRIGHT, Y = wx.wxBOTTOM } for cmd in str:gmatch("([^|]+)") do local instr = cmd:match("<(%w)>") if (not instr) then local id = fnIdConvert and fnIdConvert(cmd) or cmd local pageind = next(currentpages[id] or {}) if (pageind) then local page = currentpages[id][pageind] currentpages[id][pageind] = nil nb:AddPage(page.page, page.text) if (direction) then nb:Split(t, direction) end finishPage(page) end end direction = instr and splits[instr] end -- add anything we forgot; make sure page groups are in the order specified for i=1,cnt do local pagelist = currentpages[order[i]] for _,page in pairs(pagelist) do nb:AddPage(page.page, page.text) finishPage(page) end end -- set the active page as it was before if (newsel) then nb:SetSelection(newsel) end end function SettingsRestoreView() local listname = "/view" local path = settings:GetPath() settings:SetPath(listname) local frame = ide:GetMainFrame() local uimgr = ide:GetUIManager() local layoutcur = uimgr:SavePerspective() local layout = settingsReadSafe(settings,layoutlabel.UIMANAGER,"") if (layout ~= layoutcur) then -- save the current toolbar besth and re-apply after perspective is loaded -- bestw and besth have two separate issues: -- (1) layout includes bestw that is only as wide as the toolbar size, -- this leaves default background on the right side of the toolbar; -- fix it by explicitly replacing with the screen width. -- (2) besth may be wrong after icon size changes. local toolbar = uimgr:GetPane("toolbar") local besth = toolbar:IsOk() and tonumber(uimgr:SavePaneInfo(toolbar):match("besth=([^;]+)")) -- reload the perspective if the saved one is not empty as it's different from the default if #layout > 0 then uimgr:LoadPerspective(layout, true) end local screenw = frame:GetClientSize():GetWidth() if toolbar:IsOk() then toolbar:BestSize(screenw, besth or -1) end -- check if debugging panes are not mentioned and float them for _, name in pairs({"stackpanel", "watchpanel"}) do local pane = uimgr:GetPane(name) if pane:IsOk() and not layout:find(name) then pane:Float() end end -- check if the toolbar is not mentioned in the layout and show it for _, name in pairs({"toolbar"}) do local pane = uimgr:GetPane(name) if pane:IsOk() and not layout:find(name) then pane:Show() end end -- remove captions from all panes local panes = uimgr:GetAllPanes() for index = 0, panes:GetCount()-1 do uimgr:GetPane(panes:Item(index).name):CaptionVisible(false) end end frame:GetStatusBar():Show(settingsReadSafe(settings,layoutlabel.STATUSBAR,true)) uimgr:Update() layoutcur = saveNotebook(ide:GetOutputNotebook()) layout = settingsReadSafe(settings,layoutlabel.NOTEBOOKOUTPUT,layoutcur) if (layout ~= layoutcur) then loadNotebook(ide:GetOutputNotebook(),layout, -- treat "Output (running)" same as "Output" function(name) return name:match(TR("Output")) or name:match("Output") or name end) end layoutcur = saveNotebook(ide:GetProjectNotebook()) layout = settingsReadSafe(settings,layoutlabel.NOTEBOOKPROJECT,layoutcur) if (layout ~= layoutcur) then loadNotebook(ide:GetProjectNotebook(),layout) end -- always select Output tab local bottomnotebook = ide:GetOutputNotebook() local index = bottomnotebook:GetPageIndex(bottomnotebook.errorlog) if index >= 0 then bottomnotebook:SetSelection(index) end layoutcur = saveNotebook(frame.notebook) layout = settingsReadSafe(settings,layoutlabel.NOTEBOOK,layoutcur) if (layout ~= layoutcur) then loadNotebook(ide.frame.notebook,layout) local nb = frame.notebook local cnt = nb:GetPageCount() for i=0,cnt-1 do ide:GetDocument(nb:GetPage(i)):SetTabText(nb:GetPageText(i)) end end -- restore configuration for notebook pages that have been split; -- load saved dock_size values and update current values with saved ones -- where dock_size configuration matches for l, m in pairs({ [layoutlabel.DOCKNOTEBOOK] = ide:GetEditorNotebook():GetAuiManager(), [layoutlabel.DOCKNOTEBOOKOUTPUT] = ide:GetOutputNotebook():GetAuiManager(), [layoutlabel.DOCKNOTEBOOKPROJECT] = ide:GetProjectNotebook():GetAuiManager(), }) do -- ...|dock_size(5,0,0)=20|dock_size(2,1,0)=200|... local prevlayout = settingsReadSafe(settings, l, "") local curlayout = m:SavePerspective() local newlayout = curlayout:gsub('(dock_size[^=]+=)(%d+)', function(t,v) local val = prevlayout:match(EscapeMagic(t)..'(%d+)') return t..(val or v) end) if newlayout ~= curlayout then m:LoadPerspective(newlayout) end end local editor = ide:GetEditor() if editor then editor:SetFocus() end settings:SetPath(path) end function SettingsSaveView() local listname = "/view" local path = settings:GetPath() settings:DeleteGroup(listname) settings:SetPath(listname) local frame = ide.frame local uimgr = frame.uimgr settings:Write(layoutlabel.UIMANAGER, uimgr:SavePerspective()) settings:Write(layoutlabel.NOTEBOOK, saveNotebook(ide:GetEditorNotebook())) settings:Write(layoutlabel.NOTEBOOKOUTPUT, saveNotebook(ide:GetOutputNotebook())) settings:Write(layoutlabel.NOTEBOOKPROJECT, saveNotebook(ide:GetProjectNotebook())) settings:Write(layoutlabel.DOCKNOTEBOOK, ide:GetEditorNotebook():GetAuiManager():SavePerspective()) settings:Write(layoutlabel.DOCKNOTEBOOKOUTPUT, ide:GetOutputNotebook():GetAuiManager():SavePerspective()) settings:Write(layoutlabel.DOCKNOTEBOOKPROJECT, ide:GetProjectNotebook():GetAuiManager():SavePerspective()) settings:Write(layoutlabel.STATUSBAR, frame:GetStatusBar():IsShown()) settings:SetPath(path) end function SettingsRestoreEditorSettings() local listname = "/editor" local path = settings:GetPath() settings:SetPath(listname) local interpreter = settingsReadSafe(settings, "interpreter", ide.config.interpreter or ide.config.default.interpreter) ProjectSetInterpreter(interpreter) settings:SetPath(path) end function SettingsSaveEditorSettings() local listname = "/editor" local path = settings:GetPath() settings:DeleteGroup(listname) settings:SetPath(listname) settings:Write("interpreter", ide.interpreter and ide.interpreter.fname or ide.config.default.interpreter) settings:SetPath(path) end function SettingsSaveAll() SettingsSaveFileSession(GetOpenFiles()) SettingsSaveEditorSettings() SettingsSaveProjectSession(FileTreeGetProjects()) SettingsSaveFileHistory(GetFileHistory()) SettingsSaveView() SettingsSaveFramePosition(ide.frame, "MainFrame") end