Implementing a GUI with REBOL/View

In my overview of REBOL, I skimmed across the capabilities of the REBOL/Core package. In this article I will give you a quick tour of the graphical version: REBOL/View.

The main addition to the basic functionality of REBOL/Core is the inclusion of a graphics engine that allows the programmer to produce a graphical user interface (GUI) in REBOL. The graphics engine provides an array of GUI widgets and the capacity for some nifty effects. A standard feature of REBOL/View is the Visual Interface Dialect (VID), which gives the programmer easy access to the graphics engine.

Face up to some jargon

In VID, standard GUI widgets such as buttons, labels, and images are called “faces”. Attributes of faces like text, colour, size and actions are called “facets”. Each face may have one or many facets, or none. Specific “keywords” set the general layout attributes. All of this information is wrapped up in a block and fed into the layout function.

For example, in my previous article I included a one-line script that loads the Melb PC logo into a window:

Image of Melb PC logo

view layout
  [image http://www.melbpc.org.au/pict/mpclogox.gif]

The view function is responsible for the actual screen display, in this case taking input from the layout function. Everything inside the layout block is VID code. The image face displays an image, and in this example it has one facet - the url http://www.melbpc.org.au/pict/mpclogox.gif.

The result of the layout function can also be stored in a variable instead of being passed straight into view, so that the following operates identically to the previous example:

melbpc_logo: layout [image http://www.melbpc.org.au/pict/mpclogox.gif] view melbpc_logo

A basic interface design

The previous article included source code for a REBOL file renamer that operated from the command line. Now I will put that basic functionality behind the GUI interface shown below.

Figure 1

What is this interface going to do?

  • display the current working folder across the top
  • display available files/folders in the left-hand scroll pane
  • update current directory and file list when a new folder is chosen
  • accept user input in search and replace boxes
  • trigger the search-and-replace operation using the Go button
  • display results of the search-and-replace in the right-hand scroll panel
  • terminate the program using the Quit button

The first task is to describe the interface itself. Download rebname1.r, listed here:

REBOL [Title: "REBOL Renamer"]

my_path: system/options/path

files_data: []
feedback_data: []                                

change-dir my_path
if exists? %../ [append files_data %../]
append files_data sort read my_path              


view layout [
  across                                         
  label "Current Folder:"                        
  lab_path: label to-string my_path              
  return                                         
  list_files: text-list data files_data          
  list_feedback: text-list data feedback_data
  return
  label "Search pattern:"
  fld_search: field                              
  return
  label "Replace pattern:"
  fld_replace: field
  return
  button "Go"                                    
  button "Quit" [quit]                           
]
The REBOL header.
REBOL [Title: "REBOL Renamer"]

To save space I’ve kept the REBOL header to a minimum - just some text for the title bar of the window. Usually you should include more information here (see the example in my previous article).

Set a variable my_path to the startup folder.
my_path: system/options/path

Retrieve a copy of the startup folder from the system variable system/options/path.

Initialise some variables.
files_data: []
feedback_data: []

The variables files_data and feedback_data are used as facets providing data to the text-lists list_files and list_feedback respectively. Both are initialised to empty blocks.

Set files_data to hold a list of files in the current working folder.

First use the change-dir function to make the current working folder the same as the folder stored in my_path. Now if the current working folder has a parent folder (that is, if the folder ../ exists) then add ../ to files_data. Finally append a sorted list of files in the current working folder to the files_data.

Change layout direction.
  across

By default, REBOL/View adds each new face vertically below the previous one. Using the across keyword changes the layout so that new faces are added horizontally, to the right of the previous.

Add a label face.
  label "Current Folder:"

A label face simply displays some text.

Make a variable lab_path to hold a face.
  lab_path: label to-string my_path

In the next version, I will need to refer to the value of some faces by name, so I can assign them to variable names now. Notice the other faces also have variable names: list_files, list_feedback, fld_search, and fld_replace.

Display the data.

Use my_path as a facet for the lab_path text label. The to-string function converts the file value in my_path into a string suitable for display.

Make a line break.

Think of return as similar in function to the return key on the keyboard. In this case it makes a new row and moves the insertion point to the left - the next face will sit at the left of a new row. But if the layout direction is set to vertical, return makes a new column and moves the insertion point to the top.

<p class="noteheading">Time to experiment! </p>

Change the across keyword to below and see what happens.

What happens when you take out one or two returns?

Add a text-list face.
  list_files: text-list data files_data

The text-list is a scrolling text panel.

Enable some user input.
  fld_search: field

The user can type text into input fields.

And it wouldn’t be a GUI with out a button or two.
  button "Go"                                    
  button "Quit" [quit]
Slip in some interactivity.

I will come to full interactivity in the next version, but this one is so simple I might as well do it now. Adding a facet including the command quit in a block adds this action to the click action of the button. Now when you click the Quit button, the window closes and the script terminates.

How do you run the program?

You’ll need REBOL/View to run the example - download from http://www.rebol.com/view-platforms.html.

There’s a couple of options. Try double-clicking on the rebname1.r filename in Windows Explorer and if REBOL/View is associated with the .r extension it will start the script in REBOL automatically. Otherwise open a command (DOS) window, change to the folder where you saved rebname1.r and enter rebol -s rebname1.r.

All being well, the screen should show similar to the screen capture above.

I think you’ll agree that this is a very quick and easy GUI to make. Of course, there’s a catch - it doesn’t do much, so it’s time to add some interactivity.

Now make it interact

That’s about it for the purely VID part of the example, the rest is ordinary REBOL code used to provide actions to some of the faces. Actions are blocks of REBOL code added as a facet to a face, and they are triggered whenever the default action of that face is triggered - usually a mouse click. I have already shown this in the first version above where the action block for the Quit button is [quit]. To avoid cluttering the VID code, I prefer to put as much action code as possible into separate functions.

A quick look at functions in REBOL

In their simplest form, functions are declared in REBOL according to the following template:

function-name: func [specification block] [code block]

The specification lists the arguments to the function. You may remember this example from my previous article:

add-up: func [this that] [this + that]

Function arguments may also be specified with a data type:

add-up: func 
    [this [number!] that [number!]] [this + that]

When a data type is declared, the function will only attempt to execute the code block if the right types of arguments are passed to it.

<p class="noteheading">Experiment again </p>

Type the last example in at the REBOL command line then try add-up 5 10 then add-up “five” “ten”. Do they both work?

The final revision

The final working version is included in the next listing (Download rebname2.r). Note that it is considerably longer than the first version because I have added several new functions to provide the actions.

REBOL [Title: "REBOL Renamer"]

my_path: copy system/options/path
files_data: []
feedback_data: []

do-update-files-data: func []                   
[
  change-dir my_path
  if exists? %../ [append files_data %../]
  append files_data sort read my_path
]

update-text-list: func [current_list [object!]] 
[
  current_list/sld/data: 0
  current_list/sn: 0
  current_list/sld/redrag 
    current_list/lc / max 1 length? 
    head current_list/lines
  show current_list
]

do-change-folder: func [selected [file!]]       
[
  test_selection: to-file rejoin 
    [my_path selected]
  if dir? test_selection 
  [
    my_path: copy test_selection
    lab_path/text: clean-path my_path
    show lab_path
    clear files_data
    do-update-files-data
    update-text-list list_files
  ]
]

do-rename: func []                              
[
  messages: copy []
  foreach file read my_path
  [
    if not dir? file
    [
      if not none? find file fld_search/text
      [
        new-file: to-file replace to-string 
          file fld_search/text fld_replace/text
        either exists? new-file
        [
          append messages rejoin 
            ["FILENAME " new-file " ALREADY IN USE"]
        ]
        [
          append messages rejoin 
            ["Renamed " file " to " new-file]
          rename file new-file
        ]
      ]
    ]
  ]
  return messages
]

do-go-action: func []                           
[
  clear feedback_data
  insert feedback_data sort do-rename
  update-text-list list_feedback        
  clear files_data
  do-update-files-data
  update-text-list list_files
]

do-update-files-data                            

view layout [
  across
  label "Current Folder:"
  lab_path: label to-string my_path 300x50 top  
  return
  list_files: text-list data files_data         
    [do-change-folder value]
  list_feedback: text-list data feedback_data
  return
  label "Search pattern:"
  fld_search: field
  return
  label "Replace pattern:"
  fld_replace: field
  return
  btn_go: button "Go" [do-go-action]            
  btn_quit: button "Quit" [quit]
]
New function, old code.
do-update-files-data: func []

The group of code to change current working folder and set a value for files_data has been made into a function. This will be called every time the user changes folders. Note that it is acceptable to have an empty function specification block.

A kludge.
update-text-list: func [current_list [object!]]

Incredibly the text-list face does not update when its data source is changed. This function fiddles with some of the innards of the text-list declaration to reset it. I borrowed this function from www.codeconscious.com/rebol/vid-notes.html (thanks to Brett for his help).

When the user clicks on the file list…
do-change-folder: func [selected [file!]]

If the user has clicked on a folder name this function changes to that folder and updates the display. Note the call to update-text-list to clear the file list.

You may recognise this function.
do-rename: func []

The code here was lifted from the example in my previous article, with a few modifications - it doesn’t refer to the command line arguments anymore, it uses the text from fld_search and fld_replace.

Make everything happen.
do-go-action: func []

When the user clicks the Go button, the rename takes place and file and feedback boxes are updated.

Initialisation.
do-update-files-data

This function call initialises the file list data before the GUI is created.

Two extra facets.
label "Current Folder:"
lab_path: label to-string my_path 300x50 top

Because file paths can vary in length, I have allocated some extra space with a pre-set size for this label using a tuple 300×50 (that is width x height). The facet top ensures that text is always vertically aligned to the top in the space allocated.

Click in the file list.
list_files: text-list data files_data

This triggers a call to do-change-folder.

Click the Go button.
btn_go: button "Go" [do-go-action]

This triggers a call to do-go-action.

Run it with a double-click on the rebname2.r issue the command rebol -s rebname2.r at the DOS command line. Hopefully it will look like the program in action in the image below.

Figure 2

REBOL/View resources on the web

First published: PC Update June 2003 (online version updated)