Files
OpenRA/src/editor/commands.lua

975 lines
32 KiB
Lua

-- Copyright 2011-14 Paul Kulchenko, ZeroBrane LLC
-- authors: Lomtik Software (J. Winwood & John Labenski)
-- Luxinia Dev (Eike Decker & Christoph Kubisch)
---------------------------------------------------------
local ide = ide
local frame = ide.frame
local notebook = frame.notebook
local openDocuments = ide.openDocuments
local uimgr = frame.uimgr
local unpack = table.unpack or unpack
local CURRENT_LINE_MARKER = StylesGetMarker("currentline")
local CURRENT_LINE_MARKER_VALUE = 2^CURRENT_LINE_MARKER
function NewFile(filename)
filename = filename or ide.config.default.fullname
local editor = CreateEditor()
SetupKeywords(editor, GetFileExt(filename))
local doc = AddEditor(editor, filename)
if doc then
PackageEventHandle("onEditorNew", editor)
SetEditorSelection(doc.index)
end
return editor
end
-- Find an editor page that hasn't been used at all, eg. an untouched NewFile()
local function findUnusedEditor()
local editor
for _, document in pairs(openDocuments) do
if (document.editor:GetLength() == 0) and
(not document.isModified) and (not document.filePath) and
not (document.editor:GetReadOnly() == true) then
editor = document.editor
break
end
end
return editor
end
function LoadFile(filePath, editor, file_must_exist, skipselection)
local filePath = wx.wxFileName(filePath)
filePath:Normalize() -- make it absolute and remove all .. and . if possible
filePath = filePath:GetFullPath()
-- prevent files from being reopened again
if (not editor) then
local doc = ide:FindDocument(filePath)
if doc then
if not skipselection and doc.index ~= notebook:GetSelection() then
-- selecting the same tab doesn't trigger PAGE_CHANGE event,
-- but moves the focus to the tab bar, which needs to be avoided.
notebook:SetSelection(doc.index)
end
return doc.editor
end
end
-- if not opened yet, try open now
local file_text = FileRead(filePath)
if file_text then
if GetConfigIOFilter("input") then
file_text = GetConfigIOFilter("input")(filePath,file_text)
end
elseif file_must_exist then
return nil
end
local current = editor and editor:GetCurrentPos()
editor = editor or findUnusedEditor() or CreateEditor()
editor:Freeze()
SetupKeywords(editor, GetFileExt(filePath))
editor:MarkerDeleteAll(-1)
-- remove BOM from UTF-8 encoded files; store BOM to add back when saving
editor.bom = string.char(0xEF,0xBB,0xBF)
if file_text and editor:GetCodePage() == wxstc.wxSTC_CP_UTF8
and file_text:find("^"..editor.bom) then
file_text = file_text:gsub("^"..editor.bom, "")
else
-- set to 'false' as checks for nil on wxlua objects may fail at run-time
editor.bom = false
end
editor:SetText(file_text or "")
-- check the editor as it can be empty if the file has malformed UTF8;
-- skip binary files with unknown extensions as they may have any sequences;
-- can't show them anyway.
if file_text and #file_text > 0 and #(editor:GetText()) == 0
and (editor.spec ~= ide.specs.none or not isBinary(file_text)) then
local replacement, invalid = "\022"
file_text, invalid = FixUTF8(file_text, replacement)
if #invalid > 0 then
editor:AppendText(file_text)
local lastline = nil
for _, n in ipairs(invalid) do
local line = editor:LineFromPosition(n)
if line ~= lastline then
DisplayOutputLn(("%s:%d: %s")
:format(filePath, line+1, TR("Replaced an invalid UTF8 character with %s."):format(replacement)))
lastline = line
end
end
end
end
editor:Colourise(0, -1)
editor:Thaw()
if current then editor:GotoPos(current) end
if (file_text and ide.config.editor.autotabs) then
local found = string.find(file_text,"\t") ~= nil
editor:SetUseTabs(found)
end
if (file_text and ide.config.editor.checkeol) then
-- Auto-detect CRLF/LF line-endings
local foundcrlf = string.find(file_text,"\r\n") ~= nil
local foundlf = (string.find(file_text,"[^\r]\n") ~= nil)
or (string.find(file_text,"^\n") ~= nil) -- edge case: file beginning with LF and having no other LF
if foundcrlf and foundlf then -- file with mixed line-endings
DisplayOutputLn(("%s: %s")
:format(filePath, TR("Mixed end-of-line encodings detected.")..' '..
TR("Use '%s' to show line endings and '%s' to convert them.")
:format("GetEditor():SetViewEOL(1)", "GetEditor():ConvertEOLs(GetEditor():GetEOLMode())")))
elseif foundcrlf then
editor:SetEOLMode(wxstc.wxSTC_EOL_CRLF)
elseif foundlf then
editor:SetEOLMode(wxstc.wxSTC_EOL_LF)
-- else (e.g. file is 1 line long or uses another line-ending): use default EOL mode
end
end
editor:EmptyUndoBuffer()
local id = editor:GetId()
if openDocuments[id] then -- existing editor; switch to the tab
notebook:SetSelection(openDocuments[id].index)
else -- the editor has not been added to notebook
AddEditor(editor, wx.wxFileName(filePath):GetFullName()
or ide.config.default.fullname)
end
openDocuments[id].filePath = filePath
openDocuments[id].fileName = wx.wxFileName(filePath):GetFullName()
openDocuments[id].modTime = GetFileModTime(filePath)
SetDocumentModified(id, false, openDocuments[id].fileName)
-- activate the editor; this is needed for those cases when the editor is
-- created from some other element, for example, from a project tree.
if not skipselection then SetEditorSelection() end
PackageEventHandle("onEditorLoad", editor)
return editor
end
function ReLoadFile(filePath, editor, ...)
if not editor then return LoadFile(filePath, editor, ...) end
-- save all markers
local maskany = 2^24-1
local markers = {}
local line = editor:MarkerNext(0, maskany)
while line > -1 do
table.insert(markers, {line, editor:MarkerGet(line), editor:GetLine(line)})
line = editor:MarkerNext(line + 1, maskany)
end
local lines = editor:GetLineCount()
-- load file into the same editor
editor = LoadFile(filePath, editor, ...)
if not editor then return end
if #markers > 0 then -- restore all markers
local samelinecount = lines == editor:GetLineCount()
for _, marker in ipairs(markers) do
local line, mask, text = unpack(marker)
if samelinecount then
-- restore marker at the same line number
editor:MarkerAddSet(line, mask)
else
-- find matching line in the surrounding area and restore marker there
for _, l in ipairs({line, line-1, line-2, line+1, line+2}) do
if text == editor:GetLine(l) then
editor:MarkerAddSet(l, mask)
break
end
end
end
end
end
return editor
end
local function getExtsString()
local knownexts = ""
for _,spec in pairs(ide.specs) do
if (spec.exts) then
for _,ext in ipairs(spec.exts) do
knownexts = knownexts.."*."..ext..";"
end
end
end
knownexts = knownexts:len() > 0 and knownexts:sub(1,-2) or nil
local exts = knownexts and TR("Known Files").." ("..knownexts..")|"..knownexts.."|" or ""
return exts..TR("All files").." (*)|*"
end
function ReportError(msg)
return wx.wxMessageBox(msg, TR("Error"), wx.wxICON_ERROR + wx.wxOK + wx.wxCENTRE, ide.frame)
end
function OpenFile(event)
local editor = GetEditor()
local path = editor and ide:GetDocument(editor):GetFilePath() or nil
local fileDialog = wx.wxFileDialog(ide.frame, TR("Open file"),
(path and GetPathWithSep(path) or FileTreeGetDir() or ""),
"",
getExtsString(),
wx.wxFD_OPEN + wx.wxFD_FILE_MUST_EXIST)
if fileDialog:ShowModal() == wx.wxID_OK then
if not LoadFile(fileDialog:GetPath(), nil, true) then
ReportError(TR("Unable to load file '%s'."):format(fileDialog:GetPath()))
end
end
fileDialog:Destroy()
end
-- save the file to filePath or if filePath is nil then call SaveFileAs
function SaveFile(editor, filePath)
-- this event can be aborted
-- as SaveFileAs calls SaveFile, this event may be called two times:
-- first without filePath and then with filePath
if PackageEventHandle("onEditorPreSave", editor, filePath) == false then
return false
end
if not filePath then
return SaveFileAs(editor)
else
if ide.config.savebak then
local ok, err = FileRename(filePath, filePath..".bak")
if not ok then
ReportError(TR("Unable to save file '%s': %s"):format(filePath..".bak", err))
return
end
end
local st = (editor:GetCodePage() == wxstc.wxSTC_CP_UTF8 and editor.bom or "")
.. editor:GetText()
if GetConfigIOFilter("output") then
st = GetConfigIOFilter("output")(filePath,st)
end
local ok, err = FileWrite(filePath, st)
if ok then
editor:SetSavePoint()
local id = editor:GetId()
openDocuments[id].filePath = filePath
openDocuments[id].fileName = wx.wxFileName(filePath):GetFullName()
openDocuments[id].modTime = GetFileModTime(filePath)
SetDocumentModified(id, false, openDocuments[id].fileName)
SetAutoRecoveryMark()
PackageEventHandle("onEditorSave", editor)
return true
else
ReportError(TR("Unable to save file '%s': %s"):format(filePath, err))
end
end
return false
end
function ApproveFileOverwrite()
return wx.wxMessageBox(
TR("File already exists.").."\n"..TR("Do you want to overwrite it?"),
GetIDEString("editormessage"),
wx.wxYES_NO + wx.wxCENTRE, ide.frame) == wx.wxYES
end
function SaveFileAs(editor)
local id = editor:GetId()
local saved = false
local filePath = (openDocuments[id].filePath
or ((FileTreeGetDir() or "")
..(openDocuments[id].fileName or ide.config.default.name)))
local fn = wx.wxFileName(filePath)
fn:Normalize() -- want absolute path for dialog
local ext = fn:GetExt()
if (not ext or #ext == 0) and editor.spec and editor.spec.exts then
ext = editor.spec.exts[1]
-- set the extension on the file if assigned as this is used by OSX/Linux
-- to present the correct default "save as type" choice.
if ext then fn:SetExt(ext) end
end
local fileDialog = wx.wxFileDialog(ide.frame, TR("Save file as"),
fn:GetPath(wx.wxPATH_GET_VOLUME),
fn:GetFullName(),
-- specify the current extension plus all other extensions based on specs
(ext and #ext > 0 and "*."..ext.."|*."..ext.."|" or "")..getExtsString(),
wx.wxFD_SAVE)
if fileDialog:ShowModal() == wx.wxID_OK then
local filePath = fileDialog:GetPath()
-- check if there is another tab with the same name and prepare to close it
local existing = (ide:FindDocument(filePath) or {}).index
local cansave = fn:GetFullName() == filePath -- saving into the same file
or not wx.wxFileName(filePath):FileExists() -- or a new file
or ApproveFileOverwrite()
if cansave and SaveFile(editor, filePath) then
SetEditorSelection() -- update title of the editor
FileTreeMarkSelected(filePath)
if ext ~= GetFileExt(filePath) then
-- new extension, so setup new keywords and re-apply indicators
editor:ClearDocumentStyle() -- remove styles from the document
SetupKeywords(editor, GetFileExt(filePath))
IndicateAll(editor)
MarkupStyle(editor)
end
saved = true
if existing then
-- save the current selection as it may change after closing
local current = notebook:GetSelection()
ClosePage(existing)
-- restore the selection if it changed
if current ~= notebook:GetSelection() then
notebook:SetSelection(current)
end
end
end
end
fileDialog:Destroy()
return saved
end
function SaveAll(quiet)
for _, document in pairs(openDocuments) do
local editor = document.editor
local filePath = document.filePath
if (document.isModified or not document.filePath) -- need to save
and (document.filePath or not quiet) then -- have path or can ask user
SaveFile(editor, filePath) -- will call SaveFileAs if necessary
end
end
end
local function removePage(index)
local prevIndex = nil
local nextIndex = nil
-- try to preserve old selection
local selectIndex = notebook:GetSelection()
selectIndex = selectIndex ~= index and selectIndex
local delid = nil
for id, document in pairsSorted(openDocuments,
function(a, b) -- sort by document index
return openDocuments[a].index < openDocuments[b].index
end) do
local wasselected = document.index == selectIndex
if document.index < index then
prevIndex = document.index
elseif document.index == index then
delid = id
document.editor:Destroy()
elseif document.index > index then
document.index = document.index - 1
if nextIndex == nil then
nextIndex = document.index
end
end
if (wasselected) then
selectIndex = document.index
end
end
if (delid) then
openDocuments[delid] = nil
end
notebook:RemovePage(index)
if selectIndex then
notebook:SetSelection(selectIndex)
elseif nextIndex then
notebook:SetSelection(nextIndex)
elseif prevIndex then
notebook:SetSelection(prevIndex)
end
-- need to set editor selection as it's called *after* PAGE_CHANGED event
SetEditorSelection()
end
function ClosePage(selection)
local editor = GetEditor(selection)
local id = editor:GetId()
if SaveModifiedDialog(editor, true) ~= wx.wxID_CANCEL then
DynamicWordsRemoveAll(editor)
local debugger = ide.debugger
-- check if the window with the scratchpad running is being closed
if debugger and debugger.scratchpad and debugger.scratchpad.editors
and debugger.scratchpad.editors[editor] then
DebuggerScratchpadOff()
end
-- check if the debugger is running and is using the current window;
-- abort the debugger if the current marker is in the window being closed
if debugger and debugger.server and
(editor:MarkerNext(0, CURRENT_LINE_MARKER_VALUE) >= 0) then
debugger.terminate()
end
PackageEventHandle("onEditorClose", editor)
removePage(ide.openDocuments[id].index)
-- disable full screen if the last tab is closed
if not (notebook:GetSelection() >= 0) then ShowFullScreen(false) end
end
end
function CloseAllPagesExcept(selection)
local toclose = {}
for _, document in pairs(ide.openDocuments) do
table.insert(toclose, document.index)
end
table.sort(toclose)
-- close pages for those files that match the project in the reverse order
-- (as ids shift when pages are closed)
for i = #toclose, 1, -1 do
if toclose[i] ~= selection then ClosePage(toclose[i]) end
end
end
-- Show a dialog to save a file before closing editor.
-- returns wxID_YES, wxID_NO, or wxID_CANCEL if allow_cancel
function SaveModifiedDialog(editor, allow_cancel)
local result = wx.wxID_NO
local id = editor:GetId()
local document = openDocuments[id]
local filePath = document.filePath
local fileName = document.fileName
if document.isModified then
local message = TR("Do you want to save the changes to '%s'?")
:format(fileName or ide.config.default.name)
local dlg_styles = wx.wxYES_NO + wx.wxCENTRE + wx.wxICON_QUESTION
if allow_cancel then dlg_styles = dlg_styles + wx.wxCANCEL end
local dialog = wx.wxMessageDialog(ide.frame, message,
TR("Save Changes?"),
dlg_styles)
result = dialog:ShowModal()
dialog:Destroy()
if result == wx.wxID_YES then
if not SaveFile(editor, filePath) then
return wx.wxID_CANCEL -- cancel if canceled save dialog
end
end
end
return result
end
function SaveOnExit(allow_cancel)
for _, document in pairs(openDocuments) do
if (SaveModifiedDialog(document.editor, allow_cancel) == wx.wxID_CANCEL) then
return false
end
end
-- if all documents have been saved or refused to save, then mark those that
-- are still modified as not modified (they don't need to be saved)
-- to keep their tab names correct
for id, document in pairs(openDocuments) do
if document.isModified then SetDocumentModified(id, false) end
end
return true
end
-- circle through "fold all" => "hide base lines" => "unfold all"
function FoldSome()
local editor = GetEditor()
editor:Colourise(0, -1) -- update doc's folding info
local foldall = false -- at least on header unfolded => fold all
local hidebase = false -- at least one base is visible => hide all
for ln = 0, editor.LineCount - 1 do
local foldRaw = editor:GetFoldLevel(ln)
local foldLvl = foldRaw % 4096
local foldHdr = (math.floor(foldRaw / 8192) % 2) == 1
-- at least one header is expanded
foldall = foldall or (foldHdr and editor:GetFoldExpanded(ln))
-- at least one base can be hidden
hidebase = hidebase or (
not foldHdr
and ln > 1 -- first line can't be hidden, so ignore it
and foldLvl == wxstc.wxSTC_FOLDLEVELBASE
and bit.band(foldRaw, wxstc.wxSTC_FOLDLEVELWHITEFLAG) == 0
and editor:GetLineVisible(ln))
end
-- shows lines; this doesn't change fold status for folded lines
if not foldall and not hidebase then editor:ShowLines(0, editor.LineCount-1) end
for ln = 0, editor.LineCount-1 do
local foldRaw = editor:GetFoldLevel(ln)
local foldLvl = foldRaw % 4096
local foldHdr = (math.floor(foldRaw / 8192) % 2) == 1
if foldall then
if foldHdr and editor:GetFoldExpanded(ln) then
editor:ToggleFold(ln)
end
elseif hidebase then
if not foldHdr and (foldLvl == wxstc.wxSTC_FOLDLEVELBASE) then
editor:HideLines(ln, ln)
end
else -- unfold all
if foldHdr and not editor:GetFoldExpanded(ln) then
editor:ToggleFold(ln)
end
end
end
editor:EnsureCaretVisible()
end
function EnsureRangeVisible(posStart, posEnd)
local editor = GetEditor()
if posStart > posEnd then
posStart, posEnd = posEnd, posStart
end
local lineStart = editor:LineFromPosition(posStart)
local lineEnd = editor:LineFromPosition(posEnd)
for line = lineStart, lineEnd do
editor:EnsureVisibleEnforcePolicy(line)
end
end
function SetAllEditorsReadOnly(enable)
for _, document in pairs(openDocuments) do
document.editor:SetReadOnly(enable)
end
end
-----------------
-- Debug related
function ClearAllCurrentLineMarkers()
for _, document in pairs(openDocuments) do
document.editor:MarkerDeleteAll(CURRENT_LINE_MARKER)
end
end
-- remove shebang line (#!) as it throws a compilation error as
-- loadstring() doesn't allow it even though lua/loadfile accepts it.
-- replace with a new line to keep the number of lines the same.
function StripShebang(code) return (code:gsub("^#!.-\n", "\n")) end
local compileOk, compileTotal = 0, 0
function CompileProgram(editor, params)
local params = { jumponerror = (params or {}).jumponerror ~= false,
reportstats = (params or {}).reportstats ~= false }
local id = editor:GetId()
local filePath = DebuggerMakeFileName(editor, openDocuments[id].filePath)
local func, err = loadstring(StripShebang(editor:GetText()), '@'..filePath)
local line = not func and tonumber(err:match(":(%d+)%s*:")) or nil
if ide.frame.menuBar:IsChecked(ID_CLEAROUTPUT) then ClearOutput() end
compileTotal = compileTotal + 1
if func then
compileOk = compileOk + 1
if params.reportstats then
DisplayOutputLn(TR("Compilation successful; %.0f%% success rate (%d/%d).")
:format(compileOk/compileTotal*100, compileOk, compileTotal))
end
else
DisplayOutputLn(TR("Compilation error").." "..TR("on line %d"):format(line)..":")
DisplayOutputLn((err:gsub("\n$", "")))
-- check for escapes invalid in LuaJIT/Lua 5.2 that are allowed in Lua 5.1
if err:find('invalid escape sequence') then
local s = editor:GetLine(line-1)
local cleaned = s
:gsub('\\[abfnrtv\\"\']', ' ')
:gsub('(\\x[0-9a-fA-F][0-9a-fA-F])', function(s) return string.rep(' ', #s) end)
:gsub('(\\%d%d?%d?)', function(s) return string.rep(' ', #s) end)
:gsub('(\\z%s*)', function(s) return string.rep(' ', #s) end)
local invalid = cleaned:find("\\")
if invalid then
DisplayOutputLn(TR("Consider removing backslash from escape sequence '%s'.")
:format(s:sub(invalid,invalid+1)))
end
end
if line and params.jumponerror and line-1 ~= editor:GetCurrentLine() then
editor:GotoLine(line-1)
end
end
return func ~= nil -- return true if it compiled ok
end
------------------
-- Save & Close
function SaveIfModified(editor)
local id = editor:GetId()
if openDocuments[id].isModified then
local saved = false
if not openDocuments[id].filePath then
local ret = wx.wxMessageBox(
TR("You must save the program first.").."\n"..TR("Press cancel to abort."),
TR("Save file?"), wx.wxOK + wx.wxCANCEL + wx.wxCENTRE, ide.frame)
if ret == wx.wxOK then
saved = SaveFileAs(editor)
end
else
saved = SaveFile(editor, openDocuments[id].filePath)
end
if saved then
openDocuments[id].isModified = false
else
return false -- not saved
end
end
return true -- saved
end
function GetOpenFiles()
local opendocs = {}
for _, document in pairs(ide.openDocuments) do
if (document.filePath) then
local wxfname = wx.wxFileName(document.filePath)
wxfname:Normalize()
table.insert(opendocs, {filename=wxfname:GetFullPath(),
id=document.index, cursorpos = document.editor:GetCurrentPos()})
end
end
-- to keep tab order
table.sort(opendocs,function(a,b) return (a.id < b.id) end)
local id = GetEditor()
id = id and id:GetId()
return opendocs, {index = (id and openDocuments[id].index or 0)}
end
function SetOpenFiles(nametab,params)
for _, doc in ipairs(nametab) do
local editor = LoadFile(doc.filename,nil,true,true) -- skip selection
if editor then editor:GotoPosDelayed(doc.cursorpos or 0) end
end
notebook:SetSelection(params and params.index or 0)
SetEditorSelection()
end
local beforeFullScreenPerspective
local statusbarShown
function ShowFullScreen(setFullScreen)
if setFullScreen then
beforeFullScreenPerspective = uimgr:SavePerspective()
local panes = frame.uimgr:GetAllPanes()
for index = 0, panes:GetCount()-1 do
local name = panes:Item(index).name
if name ~= "notebook" then frame.uimgr:GetPane(name):Hide() end
end
uimgr:Update()
SetEditorSelection() -- make sure the focus is on the editor
elseif beforeFullScreenPerspective then
uimgr:LoadPerspective(beforeFullScreenPerspective, true)
beforeFullScreenPerspective = nil
end
-- On OSX, status bar is not hidden when switched to
-- full screen: http://trac.wxwidgets.org/ticket/14259; do manually.
-- need to turn off before showing full screen and turn on after,
-- otherwise the window is restored incorrectly and is reduced in size.
if ide.osname == 'Macintosh' and setFullScreen then
statusbarShown = frame:GetStatusBar():IsShown()
frame:GetStatusBar():Hide()
end
-- protect from systems that don't have ShowFullScreen (GTK on linux?)
pcall(function() frame:ShowFullScreen(setFullScreen) end)
if ide.osname == 'Macintosh' and not setFullScreen then
if statusbarShown then
frame:GetStatusBar():Show()
-- refresh AuiManager as the statusbar may be shown below the border
uimgr:Update()
end
end
end
function ProjectConfig(dir, config)
if config then ide.session.projects[dir] = config
else return unpack(ide.session.projects[dir] or {}) end
end
function SetOpenTabs(params)
local recovery, nametab = LoadSafe("return "..params.recovery)
if not recovery then
DisplayOutputLn(TR("Can't process auto-recovery record; invalid format: %s."):format(nametab))
return
end
DisplayOutputLn(TR("Found auto-recovery record and restored saved session."))
for _,doc in ipairs(nametab) do
local editor = doc.filename and LoadFile(doc.filename,nil,true,true) or NewFile()
local opendoc = openDocuments[editor:GetId()]
if doc.content then
notebook:SetPageText(opendoc.index, doc.tabname)
editor:SetText(doc.content)
if doc.filename and opendoc.modTime and doc.modified < opendoc.modTime:GetTicks() then
DisplayOutputLn(TR("File '%s' has more recent timestamp than restored '%s'; please review before saving.")
:format(doc.filename, doc.tabname))
end
end
editor:GotoPosDelayed(doc.cursorpos or 0)
end
notebook:SetSelection(params and params.index or 0)
SetEditorSelection()
end
local function getOpenTabs()
local opendocs = {}
for _, document in pairs(ide.openDocuments) do
table.insert(opendocs, {
filename = document.filePath,
tabname = notebook:GetPageText(document.index),
modified = document.modTime and document.modTime:GetTicks(), -- get number of seconds
content = document.isModified and document.editor:GetText() or nil,
id = document.index, cursorpos = document.editor:GetCurrentPos()})
end
-- to keep tab order
table.sort(opendocs, function(a,b) return (a.id < b.id) end)
local id = GetEditor()
id = id and id:GetId()
return opendocs, {index = (id and openDocuments[id].index or 0)}
end
function SetAutoRecoveryMark()
ide.session.lastupdated = os.time()
end
local function saveAutoRecovery(event)
local lastupdated = ide.session.lastupdated
if not ide.config.autorecoverinactivity or not lastupdated then return end
if lastupdated < (ide.session.lastsaved or 0) then return end
local now = os.time()
if lastupdated + ide.config.autorecoverinactivity > now then return end
-- find all open modified files and save them
local opentabs, params = getOpenTabs()
if #opentabs > 0 then
params.recovery = require('mobdebug').line(opentabs, {comment = false})
SettingsSaveAll()
SettingsSaveFileSession({}, params)
ide.settings:Flush()
end
ide.session.lastsaved = now
ide.frame.statusBar:SetStatusText(
TR("Saved auto-recover at %s."):format(os.date("%H:%M:%S")), 1)
end
local function fastWrap(func, ...)
-- ignore SetEditorSelection that is not needed as `func` may work on
-- multipe files, but editor needs to be selected once.
local SES = SetEditorSelection
SetEditorSelection = function() end
func(...)
SetEditorSelection = SES
end
function StoreRestoreProjectTabs(curdir, newdir)
local win = ide.osname == 'Windows'
local interpreter = ide.interpreter.fname
local current, closing, restore = notebook:GetSelection(), 0, false
if ide.osname ~= 'Macintosh' then notebook:Freeze() end
if curdir and #curdir > 0 then
local lowcurdir = win and string.lower(curdir) or curdir
local lownewdir = win and string.lower(newdir) or newdir
local projdocs, closdocs = {}, {}
for _, document in ipairs(GetOpenFiles()) do
local dpath = win and string.lower(document.filename) or document.filename
-- check if the filename is in the same folder
if dpath:find(lowcurdir, 1, true) == 1
and dpath:find("^[\\/]", #lowcurdir+1) then
table.insert(projdocs, document)
closing = closing + (document.id < current and 1 or 0)
-- only close if the file is not in new project as it would be reopened
if not dpath:find(lownewdir, 1, true)
or not dpath:find("^[\\/]", #lownewdir+1) then
table.insert(closdocs, document)
end
elseif document.id == current then restore = true end
end
-- adjust for the number of closing tabs on the left from the current one
current = current - closing
-- save opened files from this project
ProjectConfig(curdir, {projdocs,
{index = notebook:GetSelection() - current, interpreter = interpreter}})
-- close pages for those files that match the project in the reverse order
-- (as ids shift when pages are closed)
for i = #closdocs, 1, -1 do fastWrap(ClosePage, closdocs[i].id) end
end
local files, params = ProjectConfig(newdir)
if files then
-- provide fake index so that it doesn't activate it as the index may be not
-- quite correct if some of the existing files are already open in the IDE.
fastWrap(SetOpenFiles, files, {index = #files + notebook:GetPageCount()})
end
if params and params.interpreter then
ProjectSetInterpreter(params.interpreter) -- set the interpreter
end
if ide.osname ~= 'Macintosh' then notebook:Thaw() end
local index = params and params.index
if notebook:GetPageCount() == 0 then NewFile()
elseif restore and current >= 0 then notebook:SetSelection(current)
elseif index and index >= 0 and files[index+1] then
-- move the editor tab to the front with the file from the config
LoadFile(files[index+1].filename, nil, true)
SetEditorSelection() -- activate the editor in the active tab
end
-- remove current config as it may change; the current configuration is
-- stored with the general config.
-- The project configuration will be updated when the project is changed.
ProjectConfig(newdir, {})
end
local function closeWindow(event)
-- if the app is already exiting, then help it exit; wxwidgets on Windows
-- is supposed to report Shutdown/logoff events by setting CanVeto() to
-- false, but it doesn't happen. We simply leverage the fact that
-- CloseWindow is called several times in this case and exit. Similar
-- behavior has been also seen on Linux, so this logic applies everywhere.
if ide.exitingProgram then os.exit() end
ide.exitingProgram = true -- don't handle focus events
if not SaveOnExit(event:CanVeto()) then
event:Veto()
ide.exitingProgram = false
return
end
ShowFullScreen(false)
PackageEventHandle("onAppClose")
-- first need to detach all processes IDE has launched as the current
-- process is likely to terminate before child processes are terminated,
-- which may lead to a crash when EVT_END_PROCESS event is called.
DetachChildProcess()
DebuggerShutdown()
SettingsSaveAll()
ide.settings:Flush()
do -- hide all floating panes first
local panes = frame.uimgr:GetAllPanes()
for index = 0, panes:GetCount()-1 do
local pane = frame.uimgr:GetPane(panes:Item(index).name)
if pane:IsFloating() then pane:Hide() end
end
end
frame.uimgr:Update() -- hide floating panes
frame.uimgr:UnInit()
frame:Hide() -- hide the main frame while the IDE exits
if ide.session.timer then ide.session.timer:Stop() end
event:Skip()
end
frame:Connect(wx.wxEVT_CLOSE_WINDOW, closeWindow)
frame:Connect(wx.wxEVT_TIMER, saveAutoRecovery)
-- in the presence of wxAuiToolbar, when (1) the app gets focus,
-- (2) a floating panel is closed or (3) a toolbar dropdown is closed,
-- the focus is always on the toolbar when the app gets focus,
-- so to restore the focus correctly, need to track where the control is
-- and to set the focus to the last element that had focus.
-- it would be easier to track KILL_FOCUS events, but controls on OSX
-- don't always generate KILL_FOCUS events (see relevant wxwidgets
-- tickets: http://trac.wxwidgets.org/ticket/14142
-- and http://trac.wxwidgets.org/ticket/14269)
local infocus
ide.editorApp:Connect(wx.wxEVT_SET_FOCUS, function(event)
local win = ide.frame:FindFocus()
if win then
local class = win:GetClassInfo():GetClassName()
-- don't set focus on the main frame or toolbar
if infocus and (class == 'wxAuiToolBar' or class == 'wxFrame') then
infocus:SetFocus()
return
end
-- keep track of the current control in focus, but only on the main frame
local grandparent = win:GetGrandParent()
if grandparent and grandparent:GetId() == ide.frame:GetId() then
infocus = win
end
end
event:Skip()
end)
ide.editorApp:Connect(wx.wxEVT_ACTIVATE_APP,
function(event)
if not ide.exitingProgram then
if ide.osname == 'Macintosh' and infocus and event:GetActive() then
-- restore focus to the last element that received it;
-- wrap into pcall in case the element has disappeared
-- while the application was out of focus
pcall(function() infocus:SetFocus() end)
end
local event = event:GetActive() and "onAppFocusSet" or "onAppFocusLost"
PackageEventHandle(event, ide.editorApp)
end
event:Skip()
end)
if ide.config.autorecoverinactivity then
ide.session.timer = wx.wxTimer(frame)
-- check at least 5s to be never more than 5s off
ide.session.timer:Start(math.min(5, ide.config.autorecoverinactivity)*1000)
end
function PaneFloatToggle(window)
local pane = uimgr:GetPane(window)
if pane:IsFloating() then
pane:Dock()
else
pane:Float()
pane:FloatingPosition(pane.window:GetScreenPosition())
pane:FloatingSize(pane.window:GetSize())
end
uimgr:Update()
end