-- Copyright 2013-17 Paul Kulchenko, ZeroBrane LLC --------------------------------------------------------- local ide = ide local iscaseinsensitive = wx.wxFileName("A"):SameAs(wx.wxFileName("a")) local unpack = table.unpack or unpack local q = EscapeMagic local function eventHandle(handlers, event, ...) local success for package, handler in pairs(handlers) do local ok, res = pcall(handler, package, ...) if ok then if res == false then success = false end else ide:GetOutput():Error(TR("%s event failed: %s"):format(event, res)) end end return success end local function getEventHandlers(packages, event) local handlers = {} for _, package in pairs(packages) do if package[event] then handlers[package] = package[event] end end return handlers end function PackageEventHandle(event, ...) return eventHandle(getEventHandlers(ide.packages, event), event, ...) end function PackageEventHandleOnce(event, ...) -- copy packages as the event that is handled only once needs to be removed local handlers = getEventHandlers(ide.packages, event) -- remove all handlers as they need to be called only once -- this allows them to be re-installed if needed for _, package in pairs(ide.packages) do package[event] = nil end return eventHandle(handlers, event, ...) end local function PackageEventHandleOne(file, event, ...) local package = ide.packages[file] if package and type(package[event]) == 'function' then local ok, res = pcall(package[event], package, ...) if ok then if res == false then return false end else ide:GetOutput():Error(TR("%s event failed: %s"):format(event, res)) end end end function PackageUnRegister(file, ...) PackageEventHandleOne(file, "onUnRegister", ...) -- remove from the list of installed packages local package = ide.packages[file] ide.packages[file] = nil return package end function PackageRegister(file, ...) if not ide.packages[file] then local packages = {} local package = MergeFullPath( GetPathWithSep(ide.editorFilename), "packages/"..file..".lua") LoadLuaFileExt(packages, package, ide.proto.Plugin) packages[file].fname = file ide.packages[file] = packages[file] end return PackageEventHandleOne(file, "onRegister", ...) end function ide:GetProperty(keyword, default) return self.app.stringtable[keyword] or default end function ide:GetRootPath(path) return MergeFullPath(GetPathWithSep(self.editorFilename), path or '') end function ide:GetPackagePath(packname) return MergeFullPath( self.oshome and MergeFullPath(self.oshome, '.'..self:GetAppName()..'/') or self:GetRootPath(), MergeFullPath('packages', packname or '') ) end function ide:GetLaunchPath(addparams) local path = self.editorFilename if self.osname == "Macintosh" then -- find .app folder in the path; there are two options: -- 1. `/Applications/ZeroBraneStudio.app/Contents/ZeroBraneStudio/zbstudio`(installed path) -- 2. `...ZeroBraneStudio/zbstudio` (cloned repository path) local app = path:match("(.+%.app)/") if app then -- check if the application is already in the path path = app else local apps = ide:GetFileList(path, true, "Info.plist", {ondirectory = function(dir) -- don't recurse for more than necessary return dir:find("%.app/Contents/.+") == nil end} ) if #apps == 0 then return nil, "Can't find application path." end local fn = wx.wxFileName(apps[1]) fn:RemoveLastDir() path = fn:GetPath(wx.wxPATH_GET_VOLUME) end -- generate command with `-n` (start a new copy of the application) path = ([[open -n -a "%s" --args]]):format(path) elseif self.osname == "Unix" then path = ([["%s.sh"]]):format(path) else path = ([["%s"]]):format(path) end if addparams then for n, val in ipairs(self.arg) do if val == "-cfg" and #self.arg > n then path = path .. ([[ %s "%s"]]):format(self.arg[n], self.arg[n+1]) end end end return path end function ide:IsExiting(newval) if newval == nil then return self.exitingProgram end self.exitingProgram = newval return end function ide:Exit(hotexit) if hotexit then self.config.hotexit = true end self:GetMainFrame():Close() end function ide:Restart(hotexit) self:AddPackage("core.restart", { onAppShutdown = function() wx.wxExecute(self:GetLaunchPath(true), wx.wxEXEC_ASYNC) end }) if self.singleinstanceserver then self.singleinstanceserver:close() end self:Exit(hotexit) end function ide:GetApp() return self.editorApp end function ide:GetAppName() return self.appname end function ide:GetDefaultFileName() local default = self.config.default local ext = default.extension local ed = self:GetEditor() if ed and default.usecurrentextension then ext = self:GetDocument(ed):GetFileExt() end return default.name..(ext and ext > "" and "."..ext or "") end local function isCtrlFocused(e) local ctrl = e and e:FindFocus() return ctrl and (ctrl:GetId() == e:GetId() or ide.osname == 'Macintosh' and ctrl:GetParent():GetId() == e:GetId()) and ctrl or nil end function ide:GetEditor() local notebook = self:GetEditorNotebook() local win = notebook:GetCurrentPage() local editor if win and win:GetClassInfo():GetClassName()=="wxStyledTextCtrl" then editor = win:DynamicCast("wxStyledTextCtrl") end -- return the editor if it has focus if isCtrlFocused(editor) then return editor end -- check the rest of the documents (those not in the EditorNotebook) for _, doc in pairs(ide:GetDocuments()) do local _, nb = doc:GetTabIndex() if nb ~= notebook and isCtrlFocused(doc:GetEditor()) then return doc:GetEditor() end end -- return the current editor in the notebook, even if it's not focused return editor end function ide:GetEditorWithFocus(...) -- need to distinguish GetEditorWithFocus() and GetEditorWithFocus(nil) -- as the latter may happen when GetEditor() is passed and returns `nil` if select('#', ...) > 0 then local ed = ... return isCtrlFocused(ed) and ed or nil end local editor = self:GetEditor() if isCtrlFocused(editor) then return editor end local nb = ide:GetOutputNotebook() for p = 0, nb:GetPageCount()-1 do local ctrl = nb:GetPage(p) if ctrl:GetClassInfo():GetClassName() == "wxStyledTextCtrl" and isCtrlFocused(ctrl) then return ctrl:DynamicCast("wxStyledTextCtrl") end end return nil end function ide:GetEditorWithLastFocus() -- make sure ide.infocus is still a valid component return (self:IsValidCtrl(self.infocus) and self.infocus:GetClassInfo():GetClassName() == "wxStyledTextCtrl" and self.infocus:DynamicCast("wxStyledTextCtrl") or nil) end function ide:GetMenuBar() return self.frame and self.frame.menuBar end function ide:GetStatusBar() return self.frame and self.frame.statusBar end function ide:GetToolBar() return self.frame and self.frame.toolBar end function ide:GetDebugger() return self.debugger end function ide:SetDebugger(deb) self.debugger = deb -- if the remote console is already assigned, then assign it based on the new debugger local console = self:GetConsole() -- `SetDebugger` may be called before console is set, so need to check if it's available if self:IsValidProperty(console, 'GetRemote') and console:GetRemote() then console:SetRemote(deb:GetConsole()) end return deb end function ide:GetContentScaleFactor() if not self:IsValidProperty(self.frame, "GetContentScaleFactor") then return 1 end local scale = self.frame:GetContentScaleFactor() if scale == -1 then return 1 end -- special value indicating "no information" -- convert `y` such that `x+0.75 <= y < x+1.75` to `x+1` return math.floor(0.25+scale) end function ide:GetMainFrame() if not self.frame then local screen = wx.wxDisplay():GetClientArea() self.frame = wx.wxFrame(wx.NULL, wx.wxID_ANY, self:GetProperty("editor"), wx.wxDefaultPosition, wx.wxSize(math.floor(screen:GetWidth()*0.8), math.floor(screen:GetHeight()*0.8))) -- transparency range: 0 == invisible -> 255 == opaque -- set lower bound of 50 to prevent accidental invisibility local transparency = tonumber(self:GetConfig().transparency) if transparency then self.frame:SetTransparent(math.max(50, transparency)) end end return self.frame end function ide:GetUIManager() return self.frame.uimgr end function ide:GetDocument(ed) return self:IsValidCtrl(ed) and self.openDocuments[ed:GetId()] end function ide:CreateDocument(ed, name) if not self:IsValidCtrl(ed) or self.openDocuments[ed:GetId()] then return false end local document = setmetatable({editor = ed}, self.proto.Document) document:SetFileName(name) self.openDocuments[ed:GetId()] = document return document end function ide:RemoveDocument(ed) if not self:IsValidCtrl(ed) or not self.openDocuments[ed:GetId()] then return false end local index, notebook = self:GetDocument(ed):GetTabIndex() if not notebook:RemovePage(index) then return false end -- if the notebook is in a floating pane and has no pages close the pane if notebook ~= ide:GetEditorNotebook() and notebook:GetPageCount() == 0 then local mgr = self:GetUIManager() local pane = mgr:GetPane(notebook) if pane:IsOk() then mgr:DetachPane(notebook) end end self.openDocuments[ed:GetId()] = nil return true end function ide:GetDocuments() return self.openDocuments end function ide:GetDocumentList() local a = {} for _, doc in pairs(self.openDocuments) do table.insert(a, doc) end table.sort(a, function(a, b) return a:GetTabIndex() < b:GetTabIndex() end) return a end function ide:GetKnownExtensions(ext) local knownexts, extmatch = {}, ext and ext:lower() for _, spec in pairs(self.specs) do for _, ext in ipairs(spec.exts or {}) do if not extmatch or extmatch == ext:lower() then table.insert(knownexts, ext) end end end table.sort(knownexts) return knownexts end function ide:DoWhenIdle(func, group) -- check if there are any group leftovers and remove them if #self.onidle == 0 and next(self.onidle) then self.onidle = {} end if group then -- if there is already an element for the same group if self.onidle[group] then -- find and remove it, as it's not needed anymore for i = 1, #self.onidle do if self.onidle[i] == self.onidle[group] then table.remove(self.onidle, i) break end end end self.onidle[group] = func end table.insert(self.onidle, func) end function ide:FindTopMenu(item) local index = self:GetMenuBar():FindMenu((TR)(item)) return self:GetMenuBar():GetMenu(index), index end function ide:FindMenuItem(itemid, menu) local menubar = self:GetMenuBar() if not menubar then return end -- no associated menu local item, imenu = menubar:FindItem(itemid, menu) if menu and not item then item = menu:FindItem(itemid) end if not item then return end menu = menu or imenu for pos = 0, menu:GetMenuItemCount()-1 do if menu:FindItemByPosition(pos):GetId() == itemid then return item, menu, pos end end return end function ide:AttachMenu(...) -- AttachMenu([targetmenu,] id, submenu) -- `targetmenu` is only needed for menus not attached to the main menubar local menu, id, submenu = ... if select('#', ...) == 2 then menu, id, submenu = nil, ... end local item, menu, pos = self:FindMenuItem(id, menu) if not item then return end menu:Remove(item) item:SetSubMenu(submenu) return menu:Insert(pos, item), pos end function ide:CloneMenu(menu) if not menu then return end local newmenu = wx.wxMenu({}) local ok, node = pcall(function() return menu:GetMenuItems():GetFirst() end) -- some wxwidgets versions may not have GetFirst, so return an empty menu in this case if not ok then return newmenu end while node do local item = node:GetData():DynamicCast("wxMenuItem") newmenu:Append(item:GetId(), item:GetItemLabel(), item:GetHelp(), item:GetKind()) node = node:GetNext() end return newmenu end function ide:MakeMenu(t) local menu = wx.wxMenu({}) local menuicon = self.config.menuicon -- menu items need to have icons local iconmap = self.config.toolbar.iconmap for p = 1, #(t or {}) do if type(t[p]) == "table" then if #t[p] == 0 then -- empty table signals a separator menu:AppendSeparator() else local id, label, help, kind = unpack(t[p]) local submenu if type(kind) == "table" then submenu, kind = self:MakeMenu(kind) elseif type(kind) == "userdata" then submenu, kind = kind end if submenu then menu:Append(id, label, submenu, help or "") else local item = wx.wxMenuItem(menu, id, label, help or "", kind or wx.wxITEM_NORMAL) if menuicon and type(iconmap[id]) == "table" -- only add icons to "normal" items (OSX can take them on checkbox items too), -- otherwise this causes asert on Linux (http://trac.wxwidgets.org/ticket/17123) and (ide.osname == "Macintosh" or item:GetKind() == wx.wxITEM_NORMAL) then local bitmap = ide:GetBitmap(iconmap[id][1], "TOOLBAR", wx.wxSize(16*ide:GetContentScaleFactor(), 16*ide:GetContentScaleFactor())) item:SetBitmap(bitmap) end menu:Append(item) end end end end return menu end function ide:SetTitle(title) if not self:IsValidCtrl(self.frame) then return end self.frame:SetTitle(title or self:ExpandPlaceholders(self.config.format.apptitle)) end function ide:FindDocument(path) local fileName = wx.wxFileName(path) for _, doc in pairs(self:GetDocuments()) do local path = doc:GetFilePath() if path and fileName:SameAs(wx.wxFileName(path)) then return doc end end return end function ide:FindDocumentsByPartialPath(path) local seps = "[\\/]" -- add trailing path separator to make sure full directory match if not path:find(seps.."$") then path = path .. GetPathSeparator() end local pattern = "^"..q(path):gsub(seps, seps) local lpattern = pattern:lower() local docs = {} for _, doc in pairs(self:GetDocuments()) do local path = doc:GetFilePath() if path and (path:find(pattern) or iscaseinsensitive and path:lower():find(lpattern)) then table.insert(docs, doc) end end return docs end function ide:SetInterpreter(name) return ProjectSetInterpreter(name) end function ide:GetInterpreter(name) return name == nil and self.interpreter or name and self.interpreters[name] or nil end function ide:GetInterpreters() return self.interpreters end function ide:GetConfig() return self.config end function ide:GetOutput() return self.frame.bottomnotebook.errorlog end function ide:GetConsole() return self.frame.bottomnotebook.shellbox end function ide:GetEditorNotebook() return self.frame.notebook end function ide:GetOutputNotebook() return self.frame.bottomnotebook end function ide:GetOutline() return self.outline end function ide:GetProjectNotebook() return self.frame.projnotebook end function ide:GetProject() local dir = ide.filetree and ide.filetree.projdir return dir and #dir > 0 and wx.wxFileName.DirName(dir):GetFullPath() or nil end function ide:SetProject(projdir,skiptree) -- strip trailing spaces as this may create issues with "path/ " on Windows projdir = projdir:gsub("%s+$","") local dir = wx.wxFileName.DirName(FixDir(projdir)) dir:Normalize() -- turn into absolute path if needed if not wx.wxDirExists(dir:GetFullPath()) then return self.filetree:updateProjectDir(projdir) end projdir = dir:GetPath(wx.wxPATH_GET_VOLUME) -- no trailing slash self.config.path.projectdir = projdir ~= "" and projdir or nil self:SetStatus(projdir) self.frame:SetTitle(self:ExpandPlaceholders(self.config.format.apptitle)) if skiptree then return true end return self.filetree:updateProjectDir(projdir) end function ide:GetProjectStartFile() local projectdir = self:GetProject() local startfile = self.filetree.settings.startfile[projectdir] return MergeFullPath(projectdir, startfile), startfile end function ide:GetLaunchedProcess() return self.debugger and self.debugger.pid end function ide:SetLaunchedProcess(pid) if self.debugger then self.debugger.pid = pid; return pid end end function ide:GetProjectTree() return self.filetree.projtreeCtrl end function ide:GetOutlineTree() return self.outline.outlineCtrl end function ide:GetWatch() return self.debugger and self.debugger.watchCtrl end function ide:GetStack() return self.debugger and self.debugger.stackCtrl end function ide:GetTextFromUser(message, caption, value) local dlg = wx.wxTextEntryDialog(self.frame, message, caption, value) local res = dlg:ShowModal() return res == wx.wxID_OK and dlg:GetValue() or nil, res end function ide:GetTabArt() local tabart = wxaui.wxAuiGenericTabArt and wxaui.wxAuiGenericTabArt() or wxaui.wxAuiDefaultTabArt() -- editor tab height is off by 1 pixel on macOS between tabs with images and not -- (as the height of the image is 16 pixels, but height of the font line is 15), -- so increase the measuring font a bit to make all tabs of the same height if ide.osname == "Macintosh" then local font = wx.wxFont(wx.wxNORMAL_FONT) font:SetWeight(wx.wxFONTWEIGHT_BOLD) font:SetPointSize(font:GetPointSize()+1) tabart:SetMeasuringFont(font) end return tabart end local statusreset function ide:SetStatusFor(text, interval, field) field = field or 0 interval = interval or 2 local statusbar = self:GetStatusBar() if not self.timers.status then self.timers.status = self:AddTimer(statusbar, function(event) if statusreset then statusreset() end end) end statusreset = function() if statusbar:GetStatusText(field) == text then statusbar:SetStatusText("", field) end end self.timers.status:Start(interval*1000, wx.wxTIMER_ONE_SHOT) statusbar:SetStatusText(text, field) end function ide:SetStatus(text, field) self:GetStatusBar():SetStatusText(text, field or 0) end function ide:GetStatus(field) return self:GetStatusBar():GetStatusText(field or 0) end function ide:PushStatus(text, field) self:GetStatusBar():PushStatusText(text, field or 0) end function ide:PopStatus(field) self:GetStatusBar():PopStatusText(field or 0) end function ide:Yield() wx.wxYield() end function ide:CreateBareEditor() return CreateEditor(true) end function ide:ShowCommandBar(...) return ShowCommandBar(...) end function ide:RequestAttention() local ide = self -- first check if the active editor has focus (it may be in a floating panel) local ed = ide:GetEditor() local frame = ide:GetMainFrame() if ed and isCtrlFocused(ed) then local frameci = frame:GetClassInfo() local parent = ed:GetParent() while parent do if parent:GetClassInfo():IsKindOf(frameci) and parent:DynamicCast("wxFrame"):IsActive() then parent:Raise() return true end parent = parent:GetParent() end end -- then check if the main frame should have the focus if not frame:IsActive() then frame:RequestUserAttention() if ide.osname == "Macintosh" then local cmd = [[osascript -e 'tell application "%s" to activate']] wx.wxExecute(cmd:format(ide.editorApp:GetAppName()), wx.wxEXEC_ASYNC) elseif ide.osname == "Unix" then if frame:IsIconized() then frame:Iconize(false) end elseif ide.osname == "Windows" then if frame:IsIconized() then frame:Iconize(false) end frame:Raise() -- raise the window local ok, winapi = pcall(require, 'winapi') if ok then local pid = winapi.get_current_pid() local wins = winapi.find_all_windows(function(w) return w:get_process():get_pid() == pid and w:get_class_name() == 'wxWindowNR' end) if wins and #wins > 0 then -- found the window, now need to activate it: -- send some input to the window and then -- bring our window to foreground (doesn't work without some input) -- send Attn key twice (down and up) winapi.send_to_window(0xF6, false) winapi.send_to_window(0xF6, true) for _, w in ipairs(wins) do w:set_foreground() end end end end end end function ide:ReportError(msg) self:RequestAttention() -- request attention first in case the app is minimized or in the background return wx.wxMessageBox(msg, TR("Error"), wx.wxICON_ERROR + wx.wxOK + wx.wxCENTRE, self.frame) end local rawMethods = {"AddTextDyn", "InsertTextDyn", "AppendTextDyn", "SetTextDyn", "GetTextDyn", "GetLineDyn", "GetSelectedTextDyn", "GetTextRangeDyn", "ReplaceTargetDyn", -- this method is not available in wxlua 3.1, so it's simulated } local useraw = nil local invalidUTF8, invalidLength local suffix = "\0" local DF_TEXT = wx.wxDataFormat(wx.wxDF_TEXT) function ide:CreateStyledTextCtrl(...) local editor = wxstc.wxStyledTextCtrl(...) if not editor then return end if useraw == nil then useraw = true for _, m in ipairs(rawMethods) do if not pcall(function() return editor[m:gsub("Dyn", "Raw")] end) then useraw = false; break end end end if not self:IsValidProperty(editor, "ReplaceTargetRaw") then editor.ReplaceTargetRaw = function(self, ...) self:ReplaceTarget("") self:InsertTextDyn(self:GetTargetStart(), ...) end end -- `AppendTextRaw` and `AddTextRaw` methods may accept the length of text, -- which is important for appending binary strings that may include zeros. -- Add text length when it's not provided. for _, m in ipairs(useraw and {"AppendTextRaw", "AddTextRaw"} or {}) do local orig = editor[m] editor[m] = function(self, text, length) return orig(self, text, length or #text) end end -- map all `GetTextDyn` to `GetText` or `GetTextRaw` if `*Raw` methods are present editor.useraw = useraw for _, m in ipairs(rawMethods) do -- some `*Raw` methods return `nil` instead of `""` as their "normal" calls do -- (for example, `GetLineRaw` and `GetTextRangeRaw` for parameters outside of text) local def = m:find("^Get") and "" or nil editor[m] = function(...) return editor[m:gsub("Dyn", useraw and "Raw" or "")](...) or def end end function editor:CopyDyn() invalidUTF8 = nil if not self.useraw then return self:Copy() end -- check if selected fragment is a valid UTF-8 sequence local text = self:GetSelectedTextRaw() if text == "" or wx.wxString.FromUTF8(text) ~= "" then return self:Copy() end local tdo = wx.wxTextDataObject() -- append suffix as wxwidgets (3.1+ on Windows) truncates last char for odd-length strings local workaround = ide.osname == "Windows" and (#text % 2 > 0) and suffix or "" tdo:SetData(DF_TEXT, text..workaround) invalidUTF8, invalidLength = text, tdo:GetDataSize() local clip = wx.wxClipboard.Get() clip:Open() clip:SetData(tdo) clip:Close() end function editor:PasteDyn() if not self.useraw then return self:Paste() end local tdo = wx.wxTextDataObject() local clip = wx.wxClipboard.Get() clip:Open() clip:GetData(tdo) clip:Close() local ok, text = tdo:GetDataHere(DF_TEXT) -- check if the fragment being pasted is a valid UTF-8 sequence if ide.osname == "Windows" then text = text and text:gsub("%z+$", "") end if not ok or wx.wxString.FromUTF8(text) ~= "" or not invalidUTF8 or invalidLength ~= tdo:GetDataSize() then return self:Paste() end self:AddTextRaw(ide.osname ~= "Windows" and invalidUTF8 or text) self:GotoPos(self:GetCurrentPos()) end function editor:GotoPosEnforcePolicy(pos) self:GotoPos(pos) self:EnsureVisibleEnforcePolicy(self:LineFromPosition(pos)) end function editor:MarginFromPoint(x) if x < 0 then return nil end local pos = 0 for m = 0, ide.MAXMARGIN do pos = pos + self:GetMarginWidth(m) if x < pos then return m end end return nil -- position outside of margins end function editor:CanFold() for m = 0, ide.MAXMARGIN do if self:GetMarginWidth(m) > 0 and self:GetMarginMask(m) == wxstc.wxSTC_MASK_FOLDERS then return true end end return false end -- cycle through "fold all" => "hide base lines" => "unfold all" function editor:FoldSome(line) local foldall = false -- at least one header unfolded => fold all local hidebase = false -- at least one base is visible => hide all local header = line and bit.band(self:GetFoldLevel(line), wxstc.wxSTC_FOLDLEVELHEADERFLAG) == wxstc.wxSTC_FOLDLEVELHEADERFLAG local from = line and (header and line or self:GetFoldParent(line)) or 0 local to = line and from > -1 and self:GetLastChild(from, -1) or self:GetLineCount()-1 for ln = from, to do local foldRaw = self: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 self: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 self:GetLineVisible(ln)) end -- shows lines; this doesn't change fold status for folded lines if not foldall and not hidebase then self:ShowLines(from, to) end for ln = from, to do local foldRaw = self:GetFoldLevel(ln) local foldLvl = foldRaw % 4096 local foldHdr = (math.floor(foldRaw / 8192) % 2) == 1 if foldall then if foldHdr and self:GetFoldExpanded(ln) then self:ToggleFold(ln) end elseif hidebase then if not foldHdr and (foldLvl == wxstc.wxSTC_FOLDLEVELBASE) then self:HideLines(ln, ln) end else -- unfold all if foldHdr and not self:GetFoldExpanded(ln) then self:ToggleFold(ln) end end end -- if the entire file is being un/folded, make sure the cursor is on the screen -- (although it may be inside a folded fragment) if not line then self:EnsureCaretVisible() end end function editor:GetAllMarginWidth() local width = 0 for m = 0, ide.MAXMARGIN do width = width + self:GetMarginWidth(m) end return width end function editor:ShowPosEnforcePolicy(pos) local line = self:LineFromPosition(pos) self:EnsureVisibleEnforcePolicy(line) -- skip the rest if line wrapping is on if self:GetWrapMode() ~= wxstc.wxSTC_WRAP_NONE then return end local xwidth = self:GetClientSize():GetWidth() - self:GetAllMarginWidth() local xoffset = self:GetTextExtent(self:GetLineDyn(line):sub(1, pos-self:PositionFromLine(line)+1)) self:SetXOffset(xoffset > xwidth and xoffset-xwidth or 0) end function editor:GetLineWrapped(pos, direction) local function getPosNear(editor, pos, direction) local point = editor:PointFromPosition(pos) local height = editor:TextHeight(editor:LineFromPosition(pos)) return editor:PositionFromPoint(wx.wxPoint(point:GetX(), point:GetY() + direction * height)) end direction = tonumber(direction) or 1 local line = self:LineFromPosition(pos) if self:WrapCount(line) < 2 or direction < 0 and line == 0 or direction > 0 and line == self:GetLineCount()-1 then return false end return line == self:LineFromPosition(getPosNear(self, pos, direction)) end -- wxSTC included with wxlua didn't have ScrollRange defined, so substitute if not present if not ide:IsValidProperty(editor, "ScrollRange") then function editor:ScrollRange() end end -- ScrollRange moves to the correct position, but doesn't unfold folded region function editor:ShowRange(secondary, primary) self:ShowPosEnforcePolicy(primary) self:ScrollRange(secondary, primary) end function editor:ClearAny() local length = self:GetLength() local selections = ide.wxver >= "2.9.5" and self:GetSelections() or 1 self:Clear() -- remove selected fragments -- check if the modification has failed, which may happen -- if there is "invisible" text in the selected fragment. -- if there is only one selection, then delete manually. if length == self:GetLength() and selections == 1 then self:SetTargetStart(self:GetSelectionStart()) self:SetTargetEnd(self:GetSelectionEnd()) self:ReplaceTarget("") end end function editor:MarkerGetAll(mask, from, to) mask = mask or ide.ANYMARKERMASK local markers = {} local line = self:MarkerNext(from or 0, mask) while line ~= wx.wxNOT_FOUND do table.insert(markers, {line, self:MarkerGet(line)}) if to and line > to then break end line = self:MarkerNext(line + 1, mask) end return markers end function editor:IsLineEmpty(line) local text = self:GetLineDyn(line or self:GetCurrentLine()) local lc = self.spec and self.spec.linecomment return not text:find("%S") or (lc and text:find("^%s*"..q(lc)) ~= nil) end function editor:Activate(force) -- check for `activateoutput` if the current component is the same as `Output` if self == ide:GetOutput() and not ide.config.activateoutput and not force then return end local nb = self:GetParent() -- check that the parent is of the correct type if nb:GetClassInfo():GetClassName() ~= "wxAuiNotebook" then return end nb = nb:DynamicCast("wxAuiNotebook") local uimgr = ide:GetUIManager() local pane = uimgr:GetPane(nb) if pane:IsOk() and not pane:IsShown() then pane:Show(true) uimgr:Update() end -- activate output/errorlog window local index = nb:GetPageIndex(self) if nb:GetSelection() == index then return false end nb:SetSelection(index) return true end function editor:GetModifiedTime() return self.updated end function editor:SetupKeywords(...) return SetupKeywords(self, ...) end -- this is a workaround for the auto-complete popup showing large font -- when Settechnology(1) is used to enable DirectWrite support. -- See https://trac.wxwidgets.org/ticket/17804#comment:32 for _, method in ipairs({"AutoCompShow", "UserListShow"}) do local orig = editor[method] editor[method] = function (editor, ...) local tech = editor:GetTechnology() if tech ~= 0 then editor:SetTechnology(wxstc.wxSTC_TECHNOLOGY_DEFAULT) end orig(editor, ...) if tech ~= 0 then editor:SetTechnology(tech) end end end editor:Connect(wx.wxEVT_KEY_DOWN, function (event) local keycode = event:GetKeyCode() local mod = event:GetModifiers() if (keycode == wx.WXK_DELETE and mod == wx.wxMOD_SHIFT) or (keycode == wx.WXK_INSERT and mod == wx.wxMOD_CONTROL) or (keycode == wx.WXK_INSERT and mod == wx.wxMOD_SHIFT) then local id = keycode == wx.WXK_DELETE and ID.CUT or mod == wx.wxMOD_SHIFT and ID.PASTE or ID.COPY ide.frame:AddPendingEvent(wx.wxCommandEvent(wx.wxEVT_COMMAND_MENU_SELECTED, id)) elseif keycode == wx.WXK_CAPITAL and mod == wx.wxMOD_CONTROL then -- ignore Ctrl+CapsLock else event:Skip() end end) return editor end function ide:CreateNotebook(...) local ctrl = wxaui.wxAuiNotebook(...) if not ctrl then return end if not self:IsValidProperty(ctrl, "GetCurrentPage") then -- versions of wxlua prior to 3.1 may not have GetCurrentPage function ctrl:GetCurrentPage() local index = self:GetSelection() return index >= 0 and self:GetPage(index) or nil end end return ctrl end function ide:CreateTreeCtrl(...) local ctrl = wx.wxTreeCtrl(...) if not ctrl then return end -- explicitly disable lines on macOS and Linux (wxwidgets v3.1.3+) if ide.osname == "Unix" or ide.osname == "Macintosh" then local flags = ctrl:GetWindowStyleFlag() if bit.band(flags, wx.wxTR_NO_LINES) == 0 then ctrl:SetWindowStyleFlag(flags + wx.wxTR_NO_LINES) end end if not self:IsValidProperty(ctrl, "SetFocusedItem") then -- versions of wxlua prior to 3.1 may not have SetFocuseditem function ctrl:SetFocusedItem(item) self:UnselectAll() -- unselect others in case MULTIPLE selection is allowed return self:SelectItem(item) end end local hasGetFocused = self:IsValidProperty(ctrl, "GetFocusedItem") if not hasGetFocused then -- versions of wxlua prior to 3.1 may not have GetFocusedItem function ctrl:GetFocusedItem() return self:GetSelections()[1] end end -- LeftArrow on Linux doesn't collapse expanded nodes as it does on Windows/OSX; do it manually if ide.osname == "Unix" and hasGetFocused then ctrl:Connect(wx.wxEVT_KEY_DOWN, function (event) local keycode = event:GetKeyCode() local mod = event:GetModifiers() local item = ctrl:GetFocusedItem() if keycode == wx.WXK_LEFT and mod == wx.wxMOD_NONE and item:IsOk() and ctrl:IsExpanded(item) then ctrl:Collapse(item) else event:Skip() end end) end return ctrl end function ide:CreateFont(size, family, style, weight, underline, name, encoding) local font = wx.wxFont(size, family, style, weight, underline, "", encoding) if name > "" then -- assign the face name separately to detect when it fails to load the font font:SetFaceName(name) if ide:IsValidProperty(font, "IsOk") and not font:IsOk() then -- assign default font from the same family if the exact font is not loaded font = wx.wxFont(size, family, style, weight, underline, "", encoding) end end return font end function ide:LoadFile(...) return LoadFile(...) end function ide:CopyToClipboard(text) if wx.wxClipboard:Get():Open() then wx.wxClipboard:Get():SetData(wx.wxTextDataObject(text)) wx.wxClipboard:Get():Close() return true end return false end function ide:GetSetting(path, setting) local settings = self.settings local curpath = settings:GetPath() settings:SetPath(path) local ok, value = settings:Read(setting) settings:SetPath(curpath) return ok and value or nil end function ide:RemoveMenuItem(id, menu) local _, menu, pos = self:FindMenuItem(id, menu) if menu then self:GetMainFrame():Disconnect(id, wx.wxID_ANY, wx.wxEVT_COMMAND_MENU_SELECTED) self:GetMainFrame():Disconnect(id, wx.wxID_ANY, wx.wxEVT_UPDATE_UI) menu:Disconnect(id, wx.wxID_ANY, wx.wxEVT_COMMAND_MENU_SELECTED) menu:Disconnect(id, wx.wxID_ANY, wx.wxEVT_UPDATE_UI) menu:Remove(id) local positem = menu:FindItemByPosition(pos) if (not positem or positem:GetKind() == wx.wxITEM_SEPARATOR) and pos > 0 and (menu:FindItemByPosition(pos-1):GetKind() == wx.wxITEM_SEPARATOR) then menu:Destroy(menu:FindItemByPosition(pos-1)) -- remove last or double separator elseif positem and pos == 0 and positem:GetKind() == wx.wxITEM_SEPARATOR then menu:Destroy(menu:FindItemByPosition(pos)) -- remove first separator end return true end return false end function ide:ExecuteCommand(cmd, wdir, callback, endcallback) local proc = wx.wxProcess(self:GetOutput()) proc:Redirect() local cwd if (wdir and #wdir > 0) then -- ignore empty directory cwd = wx.wxFileName.GetCwd() cwd = wx.wxFileName.SetCwd(wdir) and cwd end local _ = wx.wxLogNull() -- disable error reporting; will report as needed local pid = wx.wxExecute(cmd, wx.wxEXEC_ASYNC, proc) pid = pid ~= -1 and pid ~= 0 and pid or nil if cwd then wx.wxFileName.SetCwd(cwd) end -- restore workdir if not pid then return pid, wx.wxSysErrorMsg() end OutputSetCallbacks(pid, proc, callback or function() end, endcallback) return pid end function ide:GetBestIconSize() -- use large icons by default on OSX and on large screens local iconsize = tonumber(ide.config.toolbar and ide.config.toolbar.iconsize) local scale = ide:GetContentScaleFactor() return (iconsize and (iconsize % 8) == 0 and iconsize or ((ide.osname == 'Macintosh' or wx.wxGetClientDisplayRect():GetWidth() >= 1280) and scale*24 or (scale>3 and 48 or scale*16))) end function ide:CreateImageList(group, ...) local _ = wx.wxLogNull() -- disable error reporting in popup local scaledsize = 16*ide:GetContentScaleFactor() local size = wx.wxSize(scaledsize, scaledsize) local imglist = wx.wxImageList(scaledsize, scaledsize) for i = 1, select('#', ...) do local icon, file = self:GetBitmap(select(i, ...), group, size) if imglist:Add(icon) == -1 then self:Print(("Failed to add image '%s' to the image list."):format(file or select(i, ...))) end end return imglist end local tintdef = 100 local function iconFilter(bitmap, tint) if type(tint) == 'function' then return tint(bitmap) end if type(tint) ~= 'table' or #tint ~= 3 then return bitmap end local tr, tg, tb = tint[1]/255, tint[2]/255, tint[3]/255 local pi = 0.299*tr + 0.587*tg + 0.114*tb -- pixel intensity local perc = (tint[0] or tintdef)/tintdef tr, tg, tb = tr*perc, tg*perc, tb*perc local img = bitmap:ConvertToImage() for x = 0, img:GetWidth()-1 do for y = 0, img:GetHeight()-1 do if not img:IsTransparent(x, y) then local r, g, b = img:GetRed(x, y)/255, img:GetGreen(x, y)/255, img:GetBlue(x, y)/255 local gs = (r + g + b) / 3 local weight = 1-4*(gs-0.5)*(gs-0.5) r = math.max(0, math.min(255, math.floor(255 * (gs + (tr-pi) * weight)))) g = math.max(0, math.min(255, math.floor(255 * (gs + (tg-pi) * weight)))) b = math.max(0, math.min(255, math.floor(255 * (gs + (tb-pi) * weight)))) img:SetRGB(x, y, r, g, b) end end end return wx.wxBitmap(img) end function ide:GetTintedColor(color, tint) if type(tint) == 'function' then return tint(color) end if type(tint) ~= 'table' or #tint ~= 3 then return color end if type(color) ~= 'table' then return color end local tr, tg, tb = tint[1]/255, tint[2]/255, tint[3]/255 local pi = 0.299*tr + 0.587*tg + 0.114*tb -- pixel intensity local perc = (tint[0] or tintdef)/tintdef tr, tg, tb = tr*perc, tg*perc, tb*perc local r, g, b = color[1]/255, color[2]/255, color[3]/255 local gs = (r + g + b) / 3 local weight = 1-4*(gs-0.5)*(gs-0.5) r = math.max(0, math.min(255, math.floor(255 * (gs + (tr-pi) * weight)))) g = math.max(0, math.min(255, math.floor(255 * (gs + (tg-pi) * weight)))) b = math.max(0, math.min(255, math.floor(255 * (gs + (tb-pi) * weight)))) return {r, g, b} end local icons = {} -- icon cache to avoid reloading the same icons function ide:GetBitmap(id, client, size) local im = self.config.imagemap local width = size:GetWidth() local key = width.."/"..id local keyclient = key.."-"..client local mapped = im[keyclient] or im[id.."-"..client] or im[key] or im[id] -- mapped may be a file name/path or wxImage object; take that into account if type(im[id.."-"..client]) == 'string' then keyclient = width.."/"..im[id.."-"..client] elseif type(im[keyclient]) == 'string' then keyclient = im[keyclient] elseif type(im[id]) == 'string' then id = im[id] key = width.."/"..id keyclient = key.."-"..client end local fileClient = self:GetAppName() .. "/res/" .. keyclient .. ".png" local fileKey = self:GetAppName() .. "/res/" .. key .. ".png" local isImage = type(mapped) == 'userdata' and mapped:GetClassInfo():GetClassName() == 'wxImage' local scale = self:GetContentScaleFactor() local file, bmp if mapped and (isImage or wx.wxFileName(mapped):FileExists()) then file = mapped elseif wx.wxFileName(fileClient):FileExists() then file = fileClient elseif wx.wxFileName(fileKey):FileExists() then file = fileKey else if width > 16 and scale > 1 and width % scale == 0 then local _, f = self:GetBitmap(id, client, wx.wxSize(width/scale, width/scale)) if f then local img = wx.wxBitmap(f):ConvertToImage() bmp = wx.wxBitmap(img:Rescale(width, width, wx.wxIMAGE_QUALITY_NEAREST)) file = fileClient end end if not file then bmp = wx.wxArtProvider.GetBitmap(id, client, size) file = fileClient end end local icon = icons[file] or iconFilter(bmp or wx.wxBitmap(file), self.config.imagetint) -- convert bitmap to set proper scaling on it, but only if scaling is supported; -- this requires special constructor that acceps additional (scale) parameter if ide.wxver >= "3.1.2" and scale > 1 then icon = wx.wxBitmap(icon:ConvertToImage(), icon:GetDepth(), scale) end icons[file] = icon return icon, file end local function str2rgb(str) local a = ('a'):byte() -- `red`/`blue` are more prominent colors; use them for the first two letters; suppress `green` local r = (((str:sub(1,1):lower():byte() or a)-a) % 27)/27 local b = (((str:sub(2,2):lower():byte() or a)-a) % 27)/27 local g = (((str:sub(3,3):lower():byte() or a)-a) % 27)/27/3 local ratio = 256/(r + g + b + 1e-6) return {math.floor(r*ratio), math.floor(g*ratio), math.floor(b*ratio)} end local iconfont function ide:CreateFileIcon(ext) local iconmap = ide.config.filetree.iconmap local mac = ide.osname == "Macintosh" local color = type(iconmap)=="table" and type(iconmap[ext])=="table" and iconmap[ext].fg local scale = ide:GetContentScaleFactor() local size = 16 local bitmap = ide:GetBitmap("FILE-NORMAL-CLR", "PROJECT", wx.wxSize(size*scale,size*scale)) -- macOS does its own scaling for drawing on DC surface, so set to no scaling if mac then scale = 1 end bitmap = wx.wxBitmap(bitmap:GetSubBitmap(wx.wxRect(0, 0, size*scale, size*scale))) iconfont = iconfont or ide:CreateFont(mac and 6 or 5, wx.wxFONTFAMILY_MODERN, wx.wxFONTSTYLE_NORMAL, wx.wxFONTWEIGHT_NORMAL, false, ide.config.filetree.iconfontname or ide.config.editor.fontname or "", wx.wxFONTENCODING_DEFAULT) local mdc = wx.wxMemoryDC() mdc:SelectObject(bitmap) mdc:SetFont(iconfont) mdc:SetTextForeground(wx.wxColour(0, 0, 32)) -- used fixed neutral color for text -- take first three letters of the extension local text = ext:sub(1,3) local topstripe = 3*scale local topborder = 2*scale local w, h = mdc:GetTextExtent(text) mdc:DrawText(text, (size*scale-w)/2, topstripe+topborder+(size*scale-topstripe-topborder-h-1)/2) if #ext > 0 then local clr = wx.wxColour(unpack(type(color)=="table" and color or str2rgb(ext))) mdc:SetPen(wx.wxPen(clr, 1, wx.wxSOLID)) mdc:SetBrush(wx.wxBrush(clr, wx.wxSOLID)) mdc:DrawRectangle(1*scale, topborder, (size-(mac and 1 or 2))*scale, topstripe) end mdc:SetFont(wx.wxNullFont) mdc:SelectObject(wx.wxNullBitmap) bitmap:SetMask(wx.wxMask(bitmap, wx.wxBLACK)) -- set transparent background return bitmap end function ide:AddPackage(name, package) self.packages[name] = setmetatable(package, self.proto.Plugin) self.packages[name].fname = name return self.packages[name] end function ide:RemovePackage(name) self.packages[name] = nil end function ide:GetPackage(name) return self.packages[name] end function ide:AddWatch(watch, value) local mgr = self.frame.uimgr local pane = mgr:GetPane("watchpanel") if (pane:IsOk() and not pane:IsShown()) then pane:Show() mgr:Update() end local watchCtrl = self.debugger.watchCtrl if not watchCtrl then return end local root = watchCtrl:GetRootItem() if not root or not root:IsOk() then return end local item = watchCtrl:GetFirstChild(root) while true do if not item:IsOk() then break end if watchCtrl:GetItemExpression(item) == watch then if value then watchCtrl:SetItemText(item, watch .. ' = ' .. tostring(value)) end return item end item = watchCtrl:GetNextSibling(item) end item = watchCtrl:AppendItem(root, watch, 1) watchCtrl:SetItemExpression(item, watch, value) return item end function ide:AddInterpreter(name, interpreter) self.interpreters[name] = setmetatable(interpreter, self.proto.Interpreter) ProjectUpdateInterpreters() end function ide:RemoveInterpreter(name) self.interpreters[name] = nil ProjectUpdateInterpreters() end function ide:AddSpec(name, spec) self.specs[name] = spec UpdateSpecs() if spec.apitype then ReloadAPIs(spec.apitype) end end function ide:RemoveSpec(name) self.specs[name] = nil end function ide:FindSpec(ext, firstline) if not ext then return end for _,curspec in pairs(self.specs) do for _,curext in ipairs(curspec.exts or {}) do if curext == ext then return curspec end end end -- check for extension to spec mapping and create the spec on the fly if present local edcfg = self.config.editor local name = type(edcfg.specmap) == "table" and edcfg.specmap[ext] local shebang = false if not name and firstline then name = firstline:match("#!.-(%w+)%s*$") name = type(edcfg.specmap) == "table" and edcfg.specmap[name] or name shebang = true end if name then -- check if there is already spec with this name, but doesn't have this extension registered; -- don't register the extension if the format was set based on the shebang if self.specs[name] then if not self.specs[name].exts then self.specs[name].exts = {} end if not shebang then table.insert(self.specs[name].exts, ext) end return self.specs[name] end local spec = { exts = shebang and {} or {ext}, lexer = "lexlpeg."..name } self:AddSpec(name, spec) return spec end end function ide:AddAPI(type, name, api) self.apis[type] = self.apis[type] or {} self.apis[type][name] = api ReloadAPIs(type) end function ide:RemoveAPI(type, name) self.apis[type][name] = nil end function ide:AddConsoleAlias(alias, table) return ShellSetAlias(alias, table) end function ide:RemoveConsoleAlias(alias) return ShellSetAlias(alias, nil) end function ide:AddMarker(...) return StylesAddMarker(...) end function ide:GetMarker(marker) return StylesGetMarker(marker) end function ide:RemoveMarker(marker) StylesRemoveMarker(marker) end local styles = {} function ide:AddStyle(style, num) num = num or styles[style] if not num then -- new style; find the smallest available number local nums = {} for _, stylenum in pairs(styles) do nums[stylenum] = true end num = wxstc.wxSTC_STYLE_MAX while nums[num] and num > wxstc.wxSTC_STYLE_LASTPREDEFINED do num = num - 1 end if num <= wxstc.wxSTC_STYLE_LASTPREDEFINED then return end end styles[style] = num return num end function ide:GetStyle(style) return styles[style] end function ide:GetStyles() return styles end function ide:RemoveStyle(style) styles[style] = nil end local indicators = {} function ide:AddIndicator(indic, num) num = num or indicators[indic] if not num then -- new indicator; find the smallest available number local nums = {} for _, indicator in pairs(indicators) do -- wxstc.wxSTC_INDIC_CONTAINER is the first available style if indicator >= wxstc.wxSTC_INDIC_CONTAINER then nums[indicator-wxstc.wxSTC_INDIC_CONTAINER+1] = true end end -- can't do `#nums + wxstc.wxSTC_INDIC_CONTAINER` as #nums can be calculated incorrectly -- on tables that have gaps before 2^n values (`1,2,nil,4`) num = wxstc.wxSTC_INDIC_CONTAINER for _ in ipairs(nums) do num = num + 1 end if num > wxstc.wxSTC_INDIC_MAX then return end end indicators[indic] = num return num end function ide:GetIndicator(indic) return indicators[indic] end function ide:GetIndicators() return indicators end function ide:RemoveIndicator(indic) indicators[indic] = nil end -- this provides a simple stack for saving/restoring current configuration local configcache = {} function ide:AddConfig(name, files) if not name or configcache[name] then return end -- don't overwrite existing slots if type(files) ~= "table" then files = {files} end -- allow to pass one value configcache[name] = { config = require('mobdebug').dump(self.config, {nocode = true}), configmeta = getmetatable(self.config), packages = {}, overrides = {}, } -- build a list of existing packages local packages = {} for package in pairs(self.packages) do packages[package] = true end -- load config file(s) for _, file in pairs(files) do LoadLuaConfig(MergeFullPath(name, file)) end -- register newly added packages (if any) for package in pairs(self.packages) do if not packages[package] then -- this is a newly added package PackageEventHandleOne(package, "onRegister") configcache[name].packages[package] = true end end ReApplySpecAndStyles() -- apply current config to the UI end local function setLongKey(tbl, key, value) local paths = {} for path in key:gmatch("([^%.]+)") do table.insert(paths, path) end while #paths > 0 do local lastkey = table.remove(paths, 1) if #paths > 0 then if tbl[lastkey] == nil then tbl[lastkey] = {} end tbl = tbl[lastkey] if type(tbl) ~= "table" then return end else tbl[lastkey] = value end end end function ide:RemoveConfig(name) if not name or not configcache[name] then return end -- unregister cached packages for package in pairs(configcache[name].packages) do PackageUnRegister(package) end -- load original config local ok, res = LoadSafe(configcache[name].config) if ok then self.config = res -- restore overrides for key, value in pairs(configcache[name].overrides) do setLongKey(self.config, key, value) end if configcache[name].configmeta then setmetatable(self.config, configcache[name].configmeta) end else ide:Print(("Error while restoring configuration: '%s'."):format(res)) end configcache[name] = nil -- clear the slot after use ReApplySpecAndStyles() -- apply current config to the UI end function ide:SetConfig(key, value, name) setLongKey(self.config, key, value) -- set config["foo.bar"] as config.foo.bar if not name or not configcache[name] then return end configcache[name].overrides[key] = value end local panels = {} function ide:AddPanel(ctrl, panel, name, conf) if not self:IsValidCtrl(ctrl) then return end local width, height = 360, 200 local notebook = ide:CreateNotebook(self.frame, wx.wxID_ANY, wx.wxDefaultPosition, wx.wxDefaultSize, wxaui.wxAUI_NB_DEFAULT_STYLE + wxaui.wxAUI_NB_TAB_EXTERNAL_MOVE - wxaui.wxAUI_NB_CLOSE_ON_ACTIVE_TAB + wx.wxNO_BORDER) notebook:SetArtProvider(ide:GetTabArt()) notebook:AddPage(ctrl, name, true) notebook:Connect(wxaui.wxEVT_COMMAND_AUINOTEBOOK_BG_DCLICK, function() PaneFloatToggle(notebook) end) notebook:Connect(wxaui.wxEVT_COMMAND_AUINOTEBOOK_PAGE_CLOSE, function(event) event:Veto() end) local mgr = self.frame.uimgr mgr:AddPane(notebook, wxaui.wxAuiPaneInfo(): Name(panel):Float():CaptionVisible(false):PaneBorder(false): MinSize(width/2,height/2): BestSize(width,height):FloatingSize(width,height): PinButton(true):Hide()) if type(conf) == "function" then conf(mgr:GetPane(panel)) end mgr.defaultPerspective = mgr:SavePerspective() -- resave default perspective panels[name] = {ctrl, panel, name, conf} return mgr:GetPane(panel), notebook end function ide:RemovePanel(panel) local mgr = self.frame.uimgr local pane = mgr:GetPane(panel) if pane:IsOk() then local win = pane.window mgr:DetachPane(win) win:Destroy() mgr:Update() end end function ide:IsPanelDocked(panel) local layout = self:GetSetting("/view", "uimgrlayout") return layout and not layout:find(panel) end function ide:AddPanelDocked(notebook, ctrl, panel, name, conf, activate) notebook:AddPage(ctrl, name, activate ~= false) panels[name] = {ctrl, panel, name, conf} return notebook end function ide:AddPanelFlex(notebook, ctrl, panel, name, conf) local nb if self:IsPanelDocked(panel) then nb = self:AddPanelDocked(notebook, ctrl, panel, name, conf, false) else self:AddPanel(ctrl, panel, name, conf) end return nb end function ide:IsValidCtrl(ctrl) return ctrl and pcall(function() ctrl:GetId() end) end function ide:IsValidProperty(ctrl, prop) -- some control may return `nil` values for non-existing properties, so check for that return pcall(function() return ctrl[prop] end) and ctrl[prop] ~= nil end function ide:IsValidHotKey(ksc) return ksc and wx.wxAcceleratorEntry():FromString(ksc) end function ide:IsWindowShown(win) while win do if not win:IsShown() then return false end win = win:GetParent() end return true end function ide:RestorePanelByLabel(name) if not panels[name] then return end return self:AddPanel(unpack(panels[name])) end local function tool2id(name) return ID("tools.exec."..name) end function ide:AddTool(name, command, updateui) local toolMenu = self:FindTopMenu('&Tools') if not toolMenu then local helpMenu, helpindex = self:FindTopMenu('&Help') if not helpMenu then helpindex = self:GetMenuBar():GetMenuCount() end toolMenu = self:MakeMenu {} self:GetMenuBar():Insert(helpindex, toolMenu, TR("&Tools")) end local id = tool2id(name) toolMenu:Append(id, name) if command then toolMenu:Connect(id, wx.wxEVT_COMMAND_MENU_SELECTED, function (event) local editor = self:GetEditor() if not editor then return end command(self:GetDocument(editor):GetFilePath(), self:GetProject()) return true end) toolMenu:Connect(id, wx.wxEVT_UPDATE_UI, updateui or function(event) event:Enable(self:GetEditor() ~= nil) end) end return id, toolMenu end function ide:RemoveTool(name) self:RemoveMenuItem(tool2id(name)) local toolMenu, toolindex = self:FindTopMenu('&Tools') if toolMenu and toolMenu:GetMenuItemCount() == 0 then self:GetMenuBar():Remove(toolindex) end end local lexers = {} function ide:AddLexer(name, lexer) lexers[name] = lexer end function ide:RemoveLexer(name) lexers[name] = nil end function ide:GetLexer(name) return lexers[name] end local timers = {} local function evhandler(event) local callback = timers[event:GetId()] if callback then callback(event) end end function ide:AddTimer(ctrl, callback) table.insert(timers, callback or function() end) ctrl:Connect(wx.wxEVT_TIMER, evhandler) return wx.wxTimer(ctrl, #timers) end local function setAcceleratorTable(accelerators) local at = {} for id, ksc in pairs(accelerators) do local ae = wx.wxAcceleratorEntry(); ae:FromString(ksc) table.insert(at, wx.wxAcceleratorEntry(ae:GetFlags(), ae:GetKeyCode(), id)) end ide:GetMainFrame():SetAcceleratorTable(#at > 0 and wx.wxAcceleratorTable(at) or wx.wxNullAcceleratorTable) end local at = {} function ide:SetAccelerator(id, ksc) if (not id) or (ksc and not self:IsValidHotKey(ksc)) then return false end at[id] = ksc setAcceleratorTable(at) return true end function ide:GetAccelerator(id) return at[id] end function ide:GetAccelerators() return at end function ide:GetHotKey(idOrKsc) if not idOrKsc then return nil, "GetHotKey requires id or key shortcut." end local id, ksc = idOrKsc if type(idOrKsc) == type("") then id, ksc = ksc, id end local accelerators = ide:GetAccelerators() local keymap = self.config.keymap if id then ksc = keymap[id] or accelerators[id] else -- ksc is provided -- search the keymap for the match local kscpat = "^"..(ksc:gsub("[+-]", "[+-]"):lower()).."$" for gid, ksc in pairs(keymap) do if ksc:lower():find(kscpat) then id = gid break end end -- if `SetHotKey` is used, there shouldn't be any conflict between keymap and accelerators, -- but accelerators can be set directly and will take precedence, so search them as well. -- this will overwrite the value from the keymap for gid, ksc in pairs(accelerators) do if ksc:lower():find(kscpat) then id = gid break end end end if id and ksc then return id, ksc end return -- couldn't find the match end function ide:SetHotKey(id, ksc) if ksc and not self:IsValidHotKey(ksc) then self:Print(("Can't set invalid hotkey value: '%s'."):format(ksc)) return end -- this function handles several cases -- 1. shortcut is assigned to an ID listed in keymap -- 2. shortcut is assigned to an ID used in a menu item -- 3. shortcut is assigned to an ID linked to an item (but not present in keymap or menu) -- 4. shortcut is assigned to a function (passed instead of ID) local keymap = self.config.keymap if ksc then -- remove any potential conflict with this hotkey -- since the hotkey can be written as `Ctrl+A` and `Ctrl-A`, account for both -- this doesn't take into account different order in `Ctrl-Shift-F1` and `Shift-Ctrl-F1`. local kscpat = "^"..(ksc:gsub("[+-]", "[+-]"):lower()).."$" for gid, ksc in pairs(keymap) do -- if the same hotkey is used elsewhere (not one of IDs being checked) if ksc:lower():find(kscpat) then keymap[gid] = "" -- try to find a menu item with this ID (if any) to remove the hotkey local item = self:FindMenuItem(gid) if item then item:SetItemLabel(item:GetItemLabelText()) end end -- continue with the loop as there may be multiple associations with the same hotkey end -- remove an existing accelerator (if any) local acid = self:GetHotKey(ksc) if acid then self:SetAccelerator(acid) end -- if the hotkey is associated with a function, handle it first if type(id) == "function" then local fakeid = NewID() self:GetMainFrame():Connect(fakeid, wx.wxEVT_COMMAND_MENU_SELECTED, function() id() end) self:SetAccelerator(fakeid, ksc) return fakeid, ksc end end -- if the keymap is already asigned, then reassign it -- if not, then it may need an accelerator, which will be set later if keymap[id] then keymap[id] = ksc end local item = self:FindMenuItem(id) if item then -- get the item text and replace the shortcut; make sure to add the accelerator (if any) item:SetItemLabel(item:GetItemLabelText()..KSC(nil, ksc)) end -- if there is no keymap or menu item, then use the accelerator if not keymap[id] and not item then self:SetAccelerator(id, ksc) end return id, ksc end function ide:IsProjectSubDirectory(dir) local projdir = self:GetProject() if not projdir then return end -- normalize and check if directory when cut is the same as the project directory; -- this relies on the project directory ending in a path separator. local path = wx.wxFileName(dir:sub(1, #projdir)) path:Normalize() return path:SameAs(wx.wxFileName(projdir)) end function ide:IsSameDirectoryPath(s1, s2) return s1 and s2 and wx.wxFileName.DirName(s1):SameAs(wx.wxFileName.DirName(s2)) or false end function ide:SetCommandLineParameters(params) if not params then return end self:SetConfig("arg.any", #params > 0 and params or nil, self:GetProject()) if #params > 0 then self:GetPackage("core.project"):AddCmdLine(params) end local interpreter = self:GetInterpreter() if interpreter then interpreter:UpdateStatus() end end function ide:ActivateFile(filename) if wx.wxDirExists(filename) then self:SetProject(filename) return true end 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(self: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 if not opened then self:Print(TR("Can't open file '%s': %s"):format(filename, wx.wxSysErrorMsg())) end return opened end function ide:MergePath(...) return MergeFullPath(...) end function ide:GetFileList(...) return FileSysGetRecursive(...) end function ide:AnalyzeString(...) return AnalyzeString(...) end function ide:AnalyzeFile(...) return AnalyzeFile(...) end --[[ format placeholders - %f -- full project name (project path) - %s -- short project name (directory name) - %i -- interpreter name - %S -- file name - %F -- file path - %n -- line number - %c -- line content - %T -- application title - %v -- application version - %t -- current tab name --]] function ide:ExpandPlaceholders(msg, ph) ph = ph or {} if type(msg) == 'function' then return msg(ph) end local editor = self:GetEditor() local proj = self:GetProject() or "" local dirs = wx.wxFileName(proj):GetDirs() local doc = editor and self:GetDocument(editor) local index, nb if doc then index, nb = doc:GetTabIndex() end local def = { f = proj, s = dirs[#dirs] or "", i = self:GetInterpreter():GetName() or "", S = doc and doc:GetFileName() or "", F = doc and doc:GetFilePath() or "", n = editor and editor:GetCurrentLine()+1 or 0, c = editor and editor:GetLineDyn(editor:GetCurrentLine()) or "", T = self:GetProperty("editor") or "", v = self.VERSION, t = index and nb:GetPageText(index) or "", } return(msg:gsub('%%(%w)', function(p) return ph[p] or def[p] or '?' end)) end do local codepage function ide:GetCodePage() if self.osname ~= "Windows" then return end if codepage == nil then codepage = tonumber(self.config.codepage) or self.config.codepage if codepage == true then -- auto-detect the codepage; -- this is done asynchronously, so the current method may still return `nil` self:ExecuteCommand("cmd /C chcp", nil, function(s) codepage = s:match(":%s*(%d+)") end) end end return tonumber(codepage) end end function ide:GetShortFilePath(filepath) -- if running on Windows and can't open the file, this may mean that -- the file path includes unicode characters that need special handling -- when passing to applications not set up to handle them if ide.osname == 'Windows' and pcall(require, "winapi") then local fh = io.open(filepath, "r") if fh then fh:close() end if not fh and wx.wxFileExists(filepath) then winapi.set_encoding(winapi.CP_UTF8) local shortpath = winapi.short_path(filepath) if shortpath ~= filepath then return shortpath end ide:Print( ("Can't get short path for a Unicode file name '%s' to use the file.") :format(filepath)) ide:Print( ("You can enable short names by using `fsutil 8dot3name set %s: 0` and recreate the file or directory.") :format(wx.wxFileName(filepath):GetVolume())) end end return filepath end do local beforeFullScreenPerspective local statusbarShown function ide:ShowFullScreen(setFullScreen) local uimgr = self:GetUIManager() local frame = self:GetMainFrame() if setFullScreen then beforeFullScreenPerspective = uimgr:SavePerspective() local panes = uimgr:GetAllPanes() for index = 0, panes:GetCount()-1 do local name = panes:Item(index).name if name ~= "notebook" then uimgr:GetPane(name):Hide() end end uimgr:Update() local ed = ide:GetEditor() if ed then ide:GetDocument(ed):SetActive() end 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 self.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 not setFullScreen and beforeFullScreenPerspective then uimgr:LoadPerspective(beforeFullScreenPerspective, true) beforeFullScreenPerspective = nil end if self.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 -- accelerator table gets removed on Linux when setting full screen mode, so put it back; -- see wxwidgets ticket https://trac.wxwidgets.org/ticket/18053 if self.osname == 'Unix' and setFullScreen then self:SetAccelerator(-1) -- only refresh the accelerator table after setting full screen end end end