diff options
Diffstat (limited to 'love2dToAPK/tools/tools/zbstudio-old-win/src/editor/commands.lua')
-rw-r--r-- | love2dToAPK/tools/tools/zbstudio-old-win/src/editor/commands.lua | 1054 |
1 files changed, 1054 insertions, 0 deletions
diff --git a/love2dToAPK/tools/tools/zbstudio-old-win/src/editor/commands.lua b/love2dToAPK/tools/tools/zbstudio-old-win/src/editor/commands.lua new file mode 100644 index 0000000..8695659 --- /dev/null +++ b/love2dToAPK/tools/tools/zbstudio-old-win/src/editor/commands.lua @@ -0,0 +1,1054 @@ +-- 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() + editor:SetupKeywords(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) + filePath = filePath:gsub("%s+$","") + filePath = wx.wxFileName(filePath) + filePath:Normalize() -- make it absolute and remove all .. and . if possible + filePath = filePath:GetFullPath() + + -- if the file name is empty or is a directory, don't do anything + if filePath == '' or wx.wxDirExists(filePath) then return nil end + + -- 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 + + local filesize = FileSize(filePath) + if not filesize and file_must_exist then return nil end + + local current = editor and editor:GetCurrentPos() + editor = editor or findUnusedEditor() or CreateEditor() + editor:Freeze() + editor:SetupKeywords(GetFileExt(filePath)) + editor:MarkerDeleteAll(-1) + if filesize then editor:Allocate(filesize) end + editor:SetTextDyn("") + editor.bom = string.char(0xEF,0xBB,0xBF) + + local inputfilter = GetConfigIOFilter("input") + local file_text + ide:PushStatus("") + FileRead(filePath, 1024*1024, function(s) + if not file_text then + -- remove BOM from UTF-8 encoded files; store BOM to add back when saving + if s and editor:GetCodePage() == wxstc.wxSTC_CP_UTF8 and s:find("^"..editor.bom) then + s = s:gsub("^"..editor.bom, "") + else + -- set to 'false' as checks for nil on wxlua objects may fail at run-time + editor.bom = false + end + file_text = s + end + if inputfilter then s = inputfilter(filePath, s) end + local expected = editor:GetLength() + #s + editor:AppendTextDyn(s) + -- if the length is not as expected, then either it's a binary file or invalid UTF8 + if editor:GetLength() ~= expected then + -- skip binary files with unknown extensions as they may have any sequences + -- when using Raw methods, this can only happen for binary files (that include \0 chars) + if editor.useraw or editor.spec == ide.specs.none and IsBinary(s) then + DisplayOutputLn(("%s: %s"):format(filePath, + TR("Binary file is shown as read-only as it is only partially loaded."))) + file_text = '' + editor:SetReadOnly(true) + return false + end + + -- handle invalid UTF8 characters + -- fix: doesn't handle characters split by callback buffer + local replacement, invalid = "\022" + s, invalid = FixUTF8(s, replacement) + if #invalid > 0 then + editor:AppendTextDyn(s) + 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 + if filesize and filesize > 0 then + ide:PopStatus() + ide:PushStatus(TR("%s%% loaded..."):format(math.floor(100*editor:GetLength()/filesize))) + end + end) + ide:PopStatus() + + editor:Colourise(0, -1) + editor:ResetTokenList() -- reset list of tokens if this is a reused editor + editor:Thaw() + + local edcfg = ide.config.editor + if current then editor:GotoPos(current) end + if (file_text and edcfg.autotabs) then + -- use tabs if they are already used + -- or if "usetabs" is set and no space indentation is used in a file + editor:SetUseTabs(string.find(file_text, "\t") ~= nil + or edcfg.usetabs and (file_text:find("%f[^\r\n] ") or file_text:find("^ ")) == nil) + end + + if (file_text and edcfg.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("ide:GetEditor():SetViewEOL(1)", "ide:GetEditor():ConvertEOLs(ide: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 doc = ide:GetDocument(editor) + if doc then -- existing editor; switch to the tab + notebook:SetSelection(doc:GetTabIndex()) + else -- the editor has not been added to notebook + doc = AddEditor(editor, wx.wxFileName(filePath):GetFullName() + or ide.config.default.fullname) + end + doc.filePath = filePath + doc.fileName = wx.wxFileName(filePath):GetFullName() + doc.modTime = GetFileModTime(filePath) + + doc:SetModified(false) + doc:SetTabText(doc:GetFileName()) + + -- 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 markers = editor:MarkerGetAll() + -- add the current line content to retrieved markers to compare later if needed + for _, marker in ipairs(markers) do marker[3] = editor:GetLineDyn(marker[1]) 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 + -- delete all markers as they may be restored by a different mechanism, + -- which may be limited to only restoring some markers + editor:MarkerDeleteAll(-1) + 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:GetLineDyn(l) then + editor:MarkerAddSet(l, mask) + break + end + end + end + end + PackageEventHandle("onEditorMarkerUpdate", editor) + end + + return editor +end + +function ActivateFile(filename) + local name, suffix, value = filename:match('(.+):([lLpP]?)(%d+)$') + if name and not wx.wxFileExists(filename) then filename = name end + + -- check if non-existing file can be loaded from the project folder; + -- this is to handle: "project file" used on the command line + if not wx.wxFileExists(filename) and not wx.wxIsAbsolutePath(filename) then + filename = GetFullPathIfExists(ide:GetProject(), filename) or filename + end + + local opened = LoadFile(filename, nil, true) + if opened and value then + if suffix:upper() == 'P' then opened:GotoPosDelayed(tonumber(value)) + else opened:GotoPosDelayed(opened:PositionFromLine(value-1)) + end + end + return opened +end + +local function getExtsString(ed) + local exts = ed and ed.spec and ed.spec.exts or {} + local knownexts = #exts > 0 and "*."..table.concat(exts, ";*.") or nil + return (knownexts and TR("Known Files").." ("..knownexts..")|"..knownexts.."|" or "") + .. 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(editor), + wx.wxFD_OPEN + wx.wxFD_FILE_MUST_EXIST + wx.wxFD_MULTIPLE) + if fileDialog:ShowModal() == wx.wxID_OK then + for _, path in ipairs(fileDialog:GetPaths()) do + if not LoadFile(path, nil, true) then + ReportError(TR("Unable to load file '%s'."):format(path)) + end + 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:GetTextDyn()) + if GetConfigIOFilter("output") then + st = GetConfigIOFilter("output")(filePath,st) + end + + local ok, err = FileWrite(filePath, st) + if ok then + editor:SetSavePoint() + local doc = ide:GetDocument(editor) + doc.filePath = filePath + doc.fileName = wx.wxFileName(filePath):GetFullName() + doc.modTime = GetFileModTime(filePath) + doc:SetModified(false) + doc:SetTabText(doc:GetFileName()) + SetAutoRecoveryMark() + FileTreeMarkSelected(filePath) + + 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(editor), + 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 + if ext ~= GetFileExt(filePath) then + -- new extension, so setup new keywords and re-apply indicators + editor:ClearDocumentStyle() -- remove styles from the document + editor:SetupKeywords(GetFileExt(filePath)) + IndicateAll(editor) + IndicateFunctionsOnly(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 PackageEventHandle("onEditorPreClose", editor) == false then + return false + end + + 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 + return true + end + return false +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 _, document in pairs(openDocuments) do + if document.isModified then document:SetModified(false) end + end + + return true +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) + document.editor:Refresh() -- needed for background markers that don't get refreshed (wx2.9.5) + 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, + keepoutput = (params or {}).keepoutput, + } + local doc = ide:GetDocument(editor) + local filePath = doc:GetFilePath() or doc:GetFileName() + local func, err = loadstring(StripShebang(editor:GetTextDyn()), '@'..filePath) + local line = not func and tonumber(err:match(":(%d+)%s*:")) or nil + + if not params.keepoutput 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 + ActivateOutput() + 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:GetLineDyn(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 or not nametab then + DisplayOutputLn(TR("Can't process auto-recovery record; invalid format: %s."):format(nametab)) + return + end + if not params.quiet then + DisplayOutputLn(TR("Found auto-recovery record and restored saved session.")) + end + for _,doc in ipairs(nametab) do + -- check for missing file if no content is stored + if doc.filepath and not doc.content and not wx.wxFileExists(doc.filepath) then + DisplayOutputLn(TR("File '%s' is missing and can't be recovered.") + :format(doc.filepath)) + else + local editor = (doc.filepath and LoadFile(doc.filepath,nil,true,true) + or findUnusedEditor() or NewFile(doc.filename)) + local opendoc = ide:GetDocument(editor) + if doc.content then + editor:SetTextDyn(doc.content) + if doc.filepath 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.filepath, opendoc:GetTabText())) + end + opendoc:SetModified(true) + end + editor:GotoPosDelayed(doc.cursorpos or 0) + end + end + notebook:SetSelection(params and params.index or 0) + SetEditorSelection() +end + +local function getOpenTabs() + local opendocs = {} + for _, document in pairs(ide.openDocuments) do + local editor = document:GetEditor() + table.insert(opendocs, { + filename = document:GetFileName(), + filepath = document:GetFilePath(), + tabname = document:GetTabText(), + modified = document:GetModTime() and document:GetModTime():GetTicks(), -- get number of seconds + content = document:IsModified() and editor:GetTextDyn() or nil, + id = document:GetTabIndex(), + cursorpos = editor:GetCurrentPos()}) + end + + -- to keep tab order + table.sort(opendocs, function(a,b) return (a.id < b.id) end) + + local ed = GetEditor() + local doc = ed and ide:GetDocument(ed) + return opendocs, {index = (doc and doc:GetTabIndex() or 0)} +end + +function SetAutoRecoveryMark() + ide.session.lastupdated = os.time() +end + +local function generateRecoveryRecord(opentabs) + return require('mobdebug').line(opentabs, {comment = false}) +end + +local function saveHotExit() + local opentabs, params = getOpenTabs() + if #opentabs > 0 then + params.recovery = generateRecoveryRecord(opentabs) + params.quiet = true + SettingsSaveFileSession({}, params) + end +end + +local function saveAutoRecovery(force) + if not ide.config.autorecoverinactivity then return end + + local lastupdated = ide.session.lastupdated + if not force then + if not lastupdated or lastupdated < (ide.session.lastsaved or 0) then return end + end + + local now = os.time() + if not force and 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 = generateRecoveryRecord(opentabs) + SettingsSaveAll() + SettingsSaveFileSession({}, params) + ide.settings:Flush() + end + ide.session.lastsaved = now + ide:SetStatus(TR("Saved auto-recover at %s."):format(os.date("%H:%M:%S"))) +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, intfname) + local win = ide.osname == 'Windows' + local interpreter = intfname or 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 + + -- either interpreter is chosen for the project or the default value is set + if (params and params.interpreter) or (not params and ide.config.interpreter) then + ProjectSetInterpreter(params and params.interpreter or ide.config.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 ide.config.hotexit and 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() + if ide.config.hotexit then saveHotExit() end + 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 + + -- stop all the timers + for _, timer in pairs(ide.timers) do timer:Stop() end + + event:Skip() + + PackageEventHandle("onAppDone") +end +frame:Connect(wx.wxEVT_CLOSE_WINDOW, closeWindow) + +frame:Connect(wx.wxEVT_TIMER, function() saveAutoRecovery() end) + +-- 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) + +ide.editorApp:Connect(wx.wxEVT_SET_FOCUS, function(event) + if ide.exitingProgram then return end + + local win = ide.frame:FindFocus() + if win then + local class = win:GetClassInfo():GetClassName() + -- don't set focus on the main frame or toolbar + if ide.infocus and (class == 'wxAuiToolBar' or class == 'wxFrame') then + -- check if the window is shown before returning focus to it, + -- as it may lead to a recursion in event handlers on OSX (wxwidgets 2.9.5). + pcall(function() if ide:IsWindowShown(ide.infocus) then ide.infocus:SetFocus() end end) + return + end + + -- keep track of the current control in focus, but only on the main frame + -- don't try to "remember" any of the focus changes on various dialog + -- windows as those will disappear along with their controls + local grandparent = win:GetGrandParent() + local frameid = ide.frame:GetId() + local mainwin = grandparent and grandparent:GetId() == frameid + local parent = win:GetParent() + while parent do + local class = parent:GetClassInfo():GetClassName() + if (class == 'wxFrame' or class:find('^wx.*Dialog$')) + and parent:GetId() ~= frameid then + mainwin = false; break + end + parent = parent:GetParent() + end + if mainwin then + if ide.infocus and ide.infocus ~= win and ide.osname == 'Macintosh' then + -- kill focus on the control that had the focus as wxwidgets on OSX + -- doesn't do it: http://trac.wxwidgets.org/ticket/14142; + -- wrap into pcall in case the window is already deleted + local ev = wx.wxFocusEvent(wx.wxEVT_KILL_FOCUS) + pcall(function() ide.infocus:GetEventHandler():ProcessEvent(ev) end) + end + ide.infocus = win + end + end + + event:Skip() +end) + +local updateInterval = 250 -- time in ms +wx.wxUpdateUIEvent.SetUpdateInterval(updateInterval) + +ide.editorApp:Connect(wx.wxEVT_ACTIVATE_APP, + function(event) + if not ide.exitingProgram then + if ide.osname == 'Macintosh' and ide.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() if ide:IsWindowShown(ide.infocus) then ide.infocus:SetFocus() end end) + end + + local active = event:GetActive() + -- save auto-recovery record when making the app inactive + if not active then saveAutoRecovery(true) end + + -- disable UI refresh when app is inactive, but only when not running + wx.wxUpdateUIEvent.SetUpdateInterval( + (active or ide:GetLaunchedProcess()) and updateInterval or -1) + + PackageEventHandle(active and "onAppFocusSet" or "onAppFocusLost", ide.editorApp) + end + event:Skip() + end) + +if ide.config.autorecoverinactivity then + ide.timers.session = wx.wxTimer(frame) + -- check at least 5s to be never more than 5s off + ide.timers.session: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 + +local cma, cman = 0, 1 +frame:Connect(wx.wxEVT_IDLE, + function(event) + local debugger = ide.debugger + if (debugger.update) then debugger.update() end + if (debugger.scratchpad) then DebuggerRefreshScratchpad() end + if IndicateIfNeeded() then event:RequestMore(true) end + PackageEventHandleOnce("onIdleOnce", event) + PackageEventHandle("onIdle", event) + + -- process onidle events if any + if #ide.onidle > 0 then table.remove(ide.onidle)() end + if #ide.onidle > 0 then event:RequestMore(true) end -- request more if anything left + + if ide.config.showmemoryusage then + local mem = collectgarbage("count") + local alpha = math.max(tonumber(ide.config.showmemoryusage) or 0, 1/cman) + cman = cman + 1 + cma = alpha * mem + (1-alpha) * cma + ide:SetStatus(("cur: %sKb; avg: %sKb"):format(math.floor(mem), math.floor(cma))) + end + + event:Skip() -- let other EVT_IDLE handlers to work on the event + end) |