SciTE GUI Extensions for Windows

SciTE is already a very powerful and customizable source editor, but extensions lack the ability to interact with the user. SciTE-GUI is a binary extension written in C++ which provides a useful set of GUI controls that can be used by SciTE Lua scripts.

To use this DLL, you need to call require("gui"); putting gui.dll next to your SciTE.exe should work fine.

Running Programs

Lua provides os.execute, but it suffers from two serious limitations; first, you will get the dreaded flashing black box in a GUI application like SciTE, and second, it blocks until the program completes. gui.run allows you to launch a program or open a document in a straightforward fashion, and it returns immediately. The arguments are:

# File name of program or document # Parameters, if running a program that requires parameters # Default directory (defaults to running in current directory)

For example, assuming that SciTE is on your path:

gui.run("SciTE.exe","c:/test/hello.c")

If .c files are associated with SciTE on your machine, then this will also work:

gui.run("c:/test/hello.c")

And ditto for any document with an associated application:

gui.run("c:/test/docs/help.html")
gui.run [[c:\test\docs\writeup.doc]]

Extensions to SciTE Lua

This DLL provides three new named callbacks that Lua scripts can define, in the same way as OnClose, etc. These are OnActivate, which will be called with either 0 or 1, depending on whether SciTE is moving to the background or the foreground; OnClosing will be called just before SciTE closes down completely, and OnCommand will be called whenever a SciTE menu command is dispatched.

The last function will receive the menu command id, such as IDM_GO, which are currently available as Lua constants (see CommandValues.html in the documentation for a full list of these constants.) If this function returns true, then the command will not be handled by SciTE.

Message Boxes and Prompting for Input

gui.message(msg,kind) will put up a simple message box with the desired message text; kind is one of the following strings: "message", "warning", "query" or "error" - the default is "message". It will return true or false depending on which button is pressed, 'Yes' for query message and 'OK' for the others.

gui.prompt_value(prompt,default) will ask the user to enter a single string value, with a default specified (the default value of default is ""). If the user presses OK or , then the entered string is returned, otherwise nil.

File Open and Colour Dialogs

gui.open_dlg(caption,filter) will present a standard Open File Dialog to the user, with the specified caption and filter. If no caption is given, "Open File" will be used, and if no filter is given, "All (.)|." is the default filter. (A more elaborate filter would look like this: "Text (.txt)|.txt|All (.)|.")

The chosen file name will be returned, otherwise nil if canceled.

gui.colour_dlg(default_colour) will open the Windows Colour choser. You need to specificy default_colour because it's used to determine what form you want the result in. If you pass a 32-bit integer, it's assumed to actually be a 24-bit colour, and that will be the type of the result as well. If it's a HTML-style "#RRGGBB" string, then you will receive the result like that. This form is convenient for SciTE since all colour properties are specified in this format.

Floating Toolbars

These are always on-top toolbars which can be used to call global Lua functions or SciTE menu commands. The arguments to gui.toolbar are:

# The caption of the window # A table of items # The size of the images # A path to search for the images

(If the last parameter isn't specified, then the current directory is used, which is probably not what you want generally.)

The items are of the form "IMAGE:TOOLTIP|ACTION", where the IMAGE is a bitmap file (default extension .bmp), the TOOLTIP is the text displayed when the mouse hovers over the item, and ACTION is either the name of a global Lua function or one of the IDM_ SciTE menu constants (as listed in commands.html)

tt = gui.toolbar("toolbar!", {
    "watch.bmp:watch this!|joy",
    "stop.bmp:stop doin that!|sorrow",
    "run.bmp:run,run,run|IDM_OPEN",
},16,[[c:\Program Files\scite]])
tt:position(500,300)

The IMAGE can also be one of the following standard icons: COPY,CUT,DELETE,FILENEW,FILEOPEN,FILESAVE,FIND,HELP,PASTE,PRINT,REDOW,REPLACE,UNDO.

Note that this window understands the general window methods discussed in the next section. In particular, you can hide, show and position the toolbar.

Windows

The SciTE GUI extensions allow you to create new top-level windows, and define a few controls that can be placed inside them. They all understand the show, hide, size and position methods; the last two take two integers as arguments. The client method is used to wrap one of the available controls in a top-level window.

w = gui.window "next"
w:size(200,200)
w:position(400,240)
w:context_menu {
    'some joy|joy',
    'some pain|sorrow',
}
w:show()

A context menu (on right click) can be attached to windows; the context_menu method is passed a table of items of the form "TEXT|ACTION", where ACTION has the same meaning as for toolbars.

You can pack more than one control into a window using the add method, which takes an extra alignment argument, which is one of "top","bottom","left","right" or "client", and a initial size in that direction. A control with "top" alignment will dock itself to the top of the window, and so forth; "client" alignment makes a control take over all available space. In that case, it isn't necessary to provide a size.

By default an additional splitter control will be added, so that users can adjust their relative sizes interactively.

w:add(ls,"top",70)
w:add(txt,"top",70)
w:add(ls2,"client")
w:show()

It's important that any controls be created immediately after the form, so they pick up the correct parent window.

Finally, the bounds method will return the visibility and the bounds of any window or control. These are returned as five values; note how the bounds are specified by width and height:

visible,x,y,width,height = w:bounds()

This is useful if you wish to remember the configuration of toolwindows when restoring a session.

All such windows (and toolbars) will be automatically hidden when SciTE ceases to be the foreground application.

Panels

Panels are windows without frames which can be either attached to the left or the right side of the editor pane. Users often prefer their UI uncluttered by arbitrary windows floating about, and gui.set_panel is a way to embed them in the SciTE frame itself.

local w = gui.panel(100)
local ls = gui.list()
local dirs = gui.list()
local bookmarks = gui.list()
gui.set_panel(w,"right")
ls:size(100,100)
w:add(dirs,"top",150)
w:add(bookmarks,"top",150)
w:client(ls)
w:show()

List View Control

Given a top-level window, you can make it contain a standard list view like so:

wls = gui.list()
wls:add_item "stuff"
wls:add_item "nonsense"
w:client(wls)
w:show()

There are two callbacks which you can set on list views; on selection and on double click. These can call any Lua function like so:

wls:on_double_click (print)
wls:on_select(function(i)
    print('selected',i)
end)

By default, list views are single-column, but you can create a more general list view and specify column titles and default column sizes. Note that in general the add_item method takes a list of strings, one for each column in the list:

ls = gui.list(true)
ls:add_column('Firstname',100)
ls:add_column('Lastname',100)
ls:add_item {'jan','botha'}
ls:add_item {'joan','taylor'}

The index passed to on_double_click and on_select is zero-based, so watch out if you are also using Lua tables (the total number of items is ls:count().) To find the text of an item, pass this index to the get_item_text list method.

It is sometimes the case where the item text is descriptive only, so it's useful to associate Lua data with an item. add_item takes an optional second parameter, which is some arbitrary Lua data; could be a table, string, function, etc. Then use the get_item_data method to retrieve this data using the index as above.

Tab Control

A tabbar is a control which provides a set of named tabs for accessing different tab pages, which can be any other control. This is useful if you have a number of lists and don't wish to clutter up your display unnecessarily.

t = gui.tabbar(w)
t:add_tab("column list",ls)
t:add_tab("another",wls)
t:on_select(function(idx)
    print('tab',idx)
end)
w:client(t)
w:show()

Rich Text Control

gui.memo creates a Windows Rich Text control. You can set its text using the set_text method; note that this text can contain any RTF markup, if it starts with the special '{\rtf' RTF marker.

txt = gui.memo()
txt:set_text [[
{\rtf
{\colortbl;\red0\green0\blue0;\red0\green0\blue255;\red0\green255\blue255;
\red0\green255\blue0;}
{\f1\cb0\cf2 This is colored text. The background is color
1 and the foreground is color 2.}\line
{\f1\cb0\cf3 More text!}\line
Regular old stuff
]]

Although it may seem silly to make a limited text control available to SciTE, when Scintilla is so much more capable, text boxes are useful sometimes to present information - particularly if you basically want a list where the items are differently coloured.

Some Worked Examples

Context-sensitive Toolbars

Floating toolbars can potentially be very irritating, especially if their commands are specialized and only apply to files of a particular type. cpp_toolbar.lua shows how to define a toolbar which is only visible when you are editing C/C++ files, although it would be even easier to do this for single extensions.

The first task is to receive notification when the current file has changed:

local oldOnSwitchFile = OnSwitchFile
local oldOnOpen = OnOpen

function OnSwitchFile(file)
    on_switch()
    if oldOnSwitchFile then oldOnSwitchFile(file) end
end

function OnOpen(file)
    on_switch()
    if oldOnOpen then oldOnOpen(file) end
end

Note that it is good practice to save the old function values, and call them as well, if they exist. If you're using extman, this can be more simply expressed as:

scite_OnOpenSwitch(on_switch)

We can then define on_switch:

local header_ext = {h = true, hpp = true, hh = true, hxx = true}

local source_ext = {c = true, cpp = true, cc = true, cxx = true}

local function set_paths ()
    path = props['FileDir']
    name = props['FileName']
    ext = props['FileExt']
end

local function on_switch ()
    set_paths()
    if header_ext[ext] or source_ext[ext] then -- is a C/C++ file
        tt:show()
    else
        tt:hide()
    end
end

I've organized the extension-checking like this because one of the functions called by this toolbar will switch between source and header files:

function try_open (exts)
    for ext,v in pairs(exts) do
        local file = path..'\\'..name..'.'..ext
        local f = io.open(file,'r')
        if f then
            f:close()
            scite.Open(file)
            return
        end
    end
end

function switch_header ()
    if header_ext[ext] then
        try_open(source_ext)
    elseif source_ext[ext] then
        try_open(header_ext)
    end
end

The other function on this toolbar will create a new named header file:

function new_header ()
    name = gui.prompt_value("Give header base name",name)
    local file = path..'\\'..name..'.h'
    local f = io.open(file,'w')
    local guard = '__'..name:upper()..'_H'
    f:write('#ifndef '..guard..'\n')
    f:write('#define '..guard..'\n\n')
    f:write('#endif\n')
    f:close()
    scite.Open(file)
end

This is a nice example of a function which does a specific job for a specific kind of source file which is tedious to do manually, and shows how gui.prompt_value can finally give you the power to ask the user meaningful questions.

A Useful Side Panel

Although the standard file open dialog on Windows is not difficult to use, it would be quicker if there were a list of available files down the side. The last example showed how to find out when the file changes; what we need to track is the directory and the current extension. So when either of these change, we update the list with all files of that extension. gui.files is what you need here to get a list of files; extman does provide scite_Files but it relies on another extension DLL to execute a dir command silently; gui.files asks the file system directly. The files.lua example shows how to keep track of all this.

Note that it is very useful to put the actual filename as data associated with the item. This allows you to use an appropriate representation for the item text. For example, we present the filenames without any extension, if there are any matches, otherwise make a listing of all files with their extensions. Or you could associate the full path of the filename with its basename, which could be used to do a history list, where the files may come from many directories.

Keeping Track of Bookmarks

Also contained in files.lua, is a useful little list which contains information about each bookmark defined in SciTE. SciTE bookmarks are very useful, but only apply within the file they are defined in. This bookmark list shows the line text at which the bookmark was defined, together with a little table which contains the actual file and line number.

To do this we have to catch the bookmark toggle command using OnCommand. (It's possible to do this without OnCommand, but it is awkward.) We don't know if the toggle is creating or deleting a bookmark, so we have to scan the list for a matching line of text first.

function OnCommand (id)
    if id == IDM_BOOKMARK_TOGGLE then
        local line = editor:GetCurLine()
        -- is this line already in the list?
        local inserting = true
        for i = 0,bookmarks:count() - 1 do
            if bookmarks:get_item_text(i) == line then
                bookmarks:delete_item(i)
                inserting = false
                break
            end
        end
        if inserting then
            local lno = editor:LineFromPosition(editor.CurrentPos)
            bookmarks:add_item(line,{props['FilePath'],lno})
        end
    end
end

bookmarks:on_double_click(function(idx)
    local pos = bookmarks:get_item_data(idx)
    if pos then
        scite.Open(pos[1])
        editor:GotoLine(pos[2])
    end
end)

Editing Colours in Property Files

One complaint that people do have about SciTE is that all customization is done with the editor itself. Personally, I can't look at a hex colour specification and tell you what colour is represented, which is one reason I included a colour dialog in SciTE-GUI.

edit_colour.lua shows how to use gui.colour_dlg to view and change the colour specifications found in SciTE property files, CSS styles, etc. The mildly tricky part is to extract the colour specification at the current editor position:

function edit_colour ()
    local function getch (i)
        return editor:textrange(i,i+1)
    end
    local function hexdigit (c)
        return c:find('[0-9A-F]')==1
    end
    local i = editor.CurrentPos
    -- let's find the extents of this colour field...
    local ch = getch(i)
    while i > 0 and ch ~= '#' and hexdigit(ch) do
        i = i - 1
        ch = getch(i)
    end
    if i == 0 then return end
    local istart = i
    i = i + 1 -- skip the '#'
    while hexdigit(getch(i)) do i = i + 1 end
    -- extract the colour!
    local colour = editor:textrange(istart,i)
    colour = gui.colour_dlg(colour)
    if colour then -- replace the colour in the document
        editor:SetSel(istart,i)
        editor:ReplaceSel(colour)
    end
end

No doubt there is an easier way, perhaps using Scintilla's ability to search for patterns.

Limitations and Further Possibilities

There are some bugs which I haven't been able to squash yet:

SciTE-GUI is intended to be a useful but minimal set of GUI functionality. This current implementation is built on my own Win32 class libraries (Yet Another Windows Library, or YAWL for short) and there's a lot more that could be exposed to Lua. In particular, tree views would be very useful; this would also involve exposing image lists, which would allow icons to be attached to the list views as well.

One of the cuter features of YAWL is that dialogs can be created using automatic layout, given a set of labels and variable bindings. This is currently only used to bring up a single-string prompt box, but it would not be difficult to make fairly arbitrary input forms accessible from Lua.

It would be desirable to make exactly the same GUI API available for SciTE on GTK platforms. While this isn't fully possible (e.g. RTF is generally not a Unix thing) most of this functionality can be done using the Lua-GTK bindings of Wolfgang Oertl together with some Lua wrapping code. This is very much on my mind, since the SciTE GUI toolkit is designed to make the scite-debug extensions more easier to use on both platforms.