-- Copyright 2011-17 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 uimgr = frame.uimgr local unpack = table.unpack or unpack local CURRENT_LINE_MARKER = StylesGetMarker("currentline") function NewFile(filename) filename = filename or ide:GetDefaultFileName() local editor = CreateEditor() local doc = AddEditor(editor, filename) if not doc then editor:Destroy() return end doc:SetActive() PackageEventHandle("onEditorNew", editor) return editor end -- Find an editor page that hasn't been used at all, eg. an untouched NewFile() local function findUnusedEditor() for _, document in pairs(ide:GetDocuments()) do local editor = document:GetEditor() if (editor:GetLength() == 0) and (not document:IsModified()) and document:IsNew() and not (editor:GetReadOnly() == true) then return editor end end return end function LoadFile(filePath, editor, file_must_exist, skipselection) filePath = filePath:gsub("%s+$","") -- if the file name is empty or is a directory or looks like a directory, don't do anything if filePath == '' or wx.wxDirExists(filePath) or filePath:find("[/\\]$") then return nil, "Invalid filename" end filePath = FileNormalizePath(filePath) -- on some Windows versions, normalization doesn't return "original" file name, -- so detect that and use LongPath instead if ide.osname == "Windows" and wx.wxFileExists(filePath) and FileNormalizePath(filePath:upper()) ~= FileNormalizePath(filePath:lower()) then filePath = FileGetLongPath(filePath) end -- prevent files from being reopened again if not editor then local doc = ide:FindDocument(filePath) if doc then if not skipselection then doc:SetActive() end return doc:GetEditor() end end local filesize = FileSize(filePath) if filesize == wx.wxInvalidOffset then -- invalid offset is also reported on empty files with no read access (at least on Windows) ide:ReportError(TR("Can't open file '%s': %s") :format(filePath, "symlink is broken or access is denied.")) return nil end 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:SetReadOnly(false) -- disable read-only status if set on the editor editor:BeginUndoAction() editor:SetTextDyn("") editor.bom = string.char(0xEF,0xBB,0xBF) local inputfilter = GetConfigIOFilter("input") local file_text ide:PushStatus("") local ok, err = FileRead(filePath, 1024*1024, function(s) -- callback is only called when the file exists 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.spec == ide.specs.none and IsBinary(s) then file_text = '' editor:SetReadOnly(true) return false, "Failed to load binary file." 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 ide:Print(("%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() -- empty or non-existing files don't have bom if not file_text then editor.bom = false end editor:EndUndoAction() -- try one more time with shebang if the type is not known yet if editor.spec == ide.specs.none then editor:SetupKeywords(GetFileExt(filePath)) end editor:Colourise(0, -1) editor:ResetTokenList() -- reset list of tokens if this is a reused editor editor:Thaw() -- only report errors on existing files if not ok and filesize then -- restore the changes in the editor, -- as it may be applied to some other content, for example, in preview editor:Undo() ide:ReportError(TR("Can't open file '%s': %s"):format(filePath, err)) return nil end 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 local isbinary = editor.spec == ide.specs.none and IsBinary(file_text) if (file_text and edcfg.checkeol and not isbinary) 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 ide:Print(("%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 if isbinary then editor:SetCaretStyle(wxstc.wxSTC_CARETSTYLE_BLOCK) 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:GetDefaultFileName()) 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 doc:SetActive() 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 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 OpenFile(event) local editor = ide: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 ide:GetProject() 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 ide: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 ide: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:SetFilePath(filePath) doc:SetFileName(wx.wxFileName(filePath):GetFullName()) doc:SetFileModifiedTime(GetFileModTime(filePath)) doc:SetTabText(doc:GetFileName()) ide:SetTitle() -- update title of the main window SetAutoRecoveryMark() FileTreeMarkSelected(filePath) PackageEventHandle("onEditorSave", editor) return true else ide: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?"), ide:GetProperty("editormessage"), wx.wxYES_NO + wx.wxCENTRE, ide.frame) == wx.wxYES end function SaveFileAs(editor) local saved = false local document = ide:GetDocument(editor) local filePath = (document and document:GetFilePath() or ((ide:GetProject() or "")..(document and document:GetFileName() or ide.config.default.name))) if document then document:SetActive() end 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 doc = ide:FindDocument(filePath) if doc then doc:SetActive() end 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 saved = true if doc then doc:Close() end end end fileDialog:Destroy() return saved end function SaveAll(quiet) for _, document in pairs(ide:GetDocuments()) do local filePath = document:GetFilePath() if (document:IsModified() or document:IsNew()) -- need to save and (filePath or not quiet) then -- have path or can ask user SaveFile(document:GetEditor(), filePath) -- will call SaveFileAs if necessary 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 document = ide:GetDocument(editor) if document and document:IsModified() then document:SetActive() local message = TR("Do you want to save the changes to '%s'?") :format(document:GetFileName() 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 document:Save() then return wx.wxID_CANCEL -- cancel if canceled save dialog end end end return result end function SaveOnExit(allow_cancel) for _, document in pairs(ide:GetDocuments()) do if (SaveModifiedDialog(document:GetEditor(), 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(ide:GetDocuments()) do if document:IsModified() then document:SetModified(false) end end return true end function SetAllEditorsReadOnly(enable) for _, document in pairs(ide:GetDocuments()) do document:GetEditor():SetReadOnly(enable) end end ----------------- -- Debug related function ClearAllCurrentLineMarkers() for _, document in pairs(ide:GetDocuments()) do document:GetEditor():MarkerDeleteAll(CURRENT_LINE_MARKER) document:GetEditor():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 loadstring = loadstring or load 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 ide:Print(TR("Compilation successful; %.0f%% success rate (%d/%d).") :format(compileOk/compileTotal*100, compileOk, compileTotal)) end else ide:GetOutput():Activate() ide:Print(TR("Compilation error").." "..TR("on line %d"):format(line)..":") ide:Print((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 ide:Print(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 doc = ide:GetDocument(editor) if doc and doc:IsModified() or doc:IsNew() then local saved = false if doc:IsNew() 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 = doc:Save() end return saved end return true -- saved end function GetOpenFiles() local opendocs = {} for _, document in ipairs(ide:GetDocumentList()) do if document:GetFilePath() then local wxfname = wx.wxFileName(document:GetFilePath()) wxfname:Normalize() table.insert(opendocs, {filename=wxfname:GetFullPath(), id=document:GetTabIndex(), cursorpos = document:GetEditor():GetCurrentPos()}) end end local ed = ide:GetEditor() local doc = ed and ide:GetDocument(ed) return opendocs, {index = (doc and doc:GetTabIndex() 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 local idx = params and params.index or 0 local doc = idx < notebook:GetPageCount() and ide:GetDocument(notebook:GetPage(idx)) if doc then doc:SetActive() 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 ide:Print(TR("Can't process auto-recovery record; invalid format: %s."):format(nametab or "unknown")) return end if not params.quiet then ide:Print(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 ide:Print(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 ide:Print(TR("File '%s' has more recent timestamp than restored '%s'; please review before saving.") :format(doc.filepath, opendoc:GetTabText())) end end editor:GotoPosDelayed(doc.cursorpos or 0) end end notebook:SetSelection(params and params.index or 0) end local function getOpenTabs() local opendocs = {} for _, document in pairs(ide:GetDocumentList()) do local editor = document:GetEditor() table.insert(opendocs, { filename = document:GetFileName(), filepath = document:GetFilePath(), tabname = document:GetTabText(), -- get number of seconds modified = document:GetFileModifiedTime() and document:GetFileModifiedTime():GetTicks(), content = document:IsModified() and editor:GetTextDyn() or nil, id = document:GetTabIndex(), cursorpos = editor:GetCurrentPos()}) end local ed = ide: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 saveHotExit() local opentabs, params = getOpenTabs() if #opentabs > 0 then params.recovery = DumpPlain(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 = DumpPlain(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 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 = {} 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) 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}}) local editor = ide:GetEditor() local doc = editor and ide:GetDocument(editor) if doc then doc:CloseAll({scope = "project"}) 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. 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) 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:IsExiting() then os.exit() end ide:IsExiting(true) -- don't handle focus events if not ide.config.hotexit and not SaveOnExit(event:CanVeto()) then event:Veto() ide:IsExiting(false) return end ide:ShowFullScreen(false) if ide:GetProject() then PackageEventHandle("onProjectClose", ide:GetProject()) end 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() ide:GetDebugger():Shutdown() 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 wx.wxClipboard:Get():Flush() -- keep the clipboard content after exit -- stop all the timers for _, timer in pairs(ide.timers) do timer:Stop() end wx.wxGetApp():Disconnect(wx.wxEVT_TIMER) event:Skip() PackageEventHandle("onAppShutdown") end frame:Connect(wx.wxEVT_CLOSE_WINDOW, closeWindow) local function restoreFocus() -- 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). if ide:IsWindowShown(ide.infocus) then ide.infocus:SetFocus() -- if switching to the editor, then also call SetSTCFocus, -- otherwise the cursor is not shown in the editor on OSX. if ide.infocus:GetClassInfo():GetClassName() == "wxStyledTextCtrl" then ide.infocus:DynamicCast("wxStyledTextCtrl"):SetSTCFocus(true) end end 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:IsExiting() 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 pcall(restoreFocus) 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.osname == "Macintosh" and ide:IsValidCtrl(ide.infocus) and ide.infocus:DynamicCast("wxWindow") ~= win 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:IsExiting() then local active = event:GetActive() -- 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 if ide.osname == "Macintosh" and active and ide.infocus then pcall(restoreFocus) end -- 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) frame:Connect(wx.wxEVT_SYS_COLOUR_CHANGED, function(event) event:Skip() local default = StylesGetDefault() local mt = getmetatable(ide.config.styles) if mt then for k in pairs(mt.__index) do mt.__index[k] = default[k] end end local mto = getmetatable(ide.config.stylesoutshell) if mto and mt ~= mto then for k in pairs(mto.__index) do mto.__index[k] = default[k] end end ReApplySpecAndStyles() end) if ide.config.autorecoverinactivity then ide.timers.session = ide:AddTimer(frame, function() saveAutoRecovery() end) -- 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) if ide:GetDebugger():Update() then event:RequestMore(true) end -- there is a chance that the current debugger can change after `Update` call -- (as the debugger may be suspended during initial socket connection), -- so retrieve the current debugger again to make sure it's properly set up. local debugger = ide:GetDebugger() if (debugger.scratchpad) then debugger:ScratchpadRefresh() 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, 1)() 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)