BeOS coding for all

If you happened to search BeBits for software that simply displays an image, you will find around 13 applications that have been written for that purpose. Some of them are pretty good, some other are less good, but it raises a question ... Why do so many people like to program this kind of software? Probably because, that's one of the easiest applications to code, after all BeOS was once called The Media OS, and that prove that the folks at Be, Inc have done a tremendous job designing the OS with emphasis on the C++ based Interface Kit and Translation Kit. But this reason can't stand by itself, people don't just start writing applications for no raison. Especialy not software that has already been written countless time, they need to have their coding hunger driven by something... and in most cases, people were motivated by learning the Be API and/or C++. But what if you don't know C++? What if you don't have the time and/or the ability to learn it? After all, C++ is quite a difficult programming language, even for professionals... what if you are a complete beginner in programming?

Since last year, Orchad, Bcc or Rebol have became available on BeOS, after several years of C++ domination. Lots of us, working in the software industry, usually overlook such tools as we don't need them: it is rather easy to just inherit from a BView and add the correct virtual methods to have a widget capable of drawing an image, as well, it's an elegant way to send a BMessage from the main window to the BView to ask it to draw a given image. But, this kind of view on programming put us definilty aside from 98% of the computer users... even in the software industry, tools like Visual Basic and Delphi have proven that for most software, ease of programming come first before raw power. We may argue over the fact that not all of the users want to be coders, but there is a good proportion of them that would like to do so, and if BeOS was to gain a wider audience, programming tools other then BeIDE and the C++ compiler will be mandatory. These users will need the right tool that best suit their programming skills, whoever they are. Failure to provide more democratic way of programming may be lethal to the growing BeOS community especially with the lack of commercial products available.

If you are fine with C++, you can still continue reading. If you are looking for a tool to do some prototyping or for rapid development, this article can show you a possibility.

Squirrel is one of the available tools that give the power back to the average Joe User (no offence intended). Although its audience may be perceived as mostly targetting kids, as it is a Logo's dialect, it is still a scripting language that can be used by all for a wide range of needs. In order to illustrate this point we going to write our own image viewer (oh, what an innovating idea! :). We won't emphasis much on the cool features we could have added to the software that we will call EzViewer, but on how easy it can be to create such an application using Squirrel. For example, the final application will be around 350 lines of code, where 2000 lines will be usualy requiered in C++ to make the same application.

The only feature that will make this application stand out from the other image viewers will be its ability to write a comment (stored in the file attributes) for a given image, this way looking back at your work's farewell party few years down the road, you will still be able to remember the name of the one that offered you that ugly tie. :)

Althought I believe Squirrel is rather easy to get into, it is probably a good idea to have the Squirrel Developer's Guide and GUI Building with Squirrel documents near you (available in PDF format), specialy if you are serious about using Squirrel.

Before we start coding, let's write down the requierements of EzViewer :

We will also add the following minor requierements :

This article is seperated into 8 parts:

Please note that for all the code listing presented in this article, the number on the left side is the line number. It is NOT part of Squirrel's dialect, it is just used here to make navigation between the listing and the comments a little bit user-friendly. Just click the line number to jump to the comment for that line. To get back to the listing, click on the comment label : Line X.

Our first Attempts


Starting from release 5.3, Squirrel can handle any kind of image from which there is installed translator on the computer running the script. For example, if we have a .Gif image we can access it like this:

make "my.image Image '/path/to/the/image/beach.gif'

Note that the Primitive Image will recognize and handle automaticaly the type of the image. If the file wasn't reconized as an image, my.image will be set as an invalid image.

Once an image is loaded from a file in the Image object, we can use the Viewer widget to display it:

$the.viewer~display :my.image

To illustrate this, let's write a very simple first version of EzViewer. This version will take a filename from the command line and display it:

1
use 'GUI' 'Imaging' 'List Processing'
2
if (llength :Args) = 2 { 
3
	make "my.image Image lindex :Args 2 
4
	if ($my.image~valid?) { 
5
		make "win Window "titled (lindex :Args 2) [100 100] "not.resizable
6
		make "the.viewer Viewer :my.image 
7
		Glue :win "top [] :the.viewer 
8
		$win~show    
9
  	} { 
10
		print 'Image file not reconized ....... maybe not an image' 
11
	} 
12
} { 
13
	print 'USAGE : ezviewer.sqi image' 
14
}

As you can see from the example bellow, the result is what we should expect from any simple image viewer, but with a simplier and lighter coding effort than any compiled programming language can offer.

Let's go over the script line by line :

Line 1. At the start of any Squirrel script, it is necessary to indicate all the Add-Ons we need. Here, we are using mostly the GUI and Imaging Add-Ons. As we will need to use some primitives that work on list, we need as well the List Processing Add-On. If we didn't add this Add-On, the script will have failed on the next instruction that call the primitive llength.

Line 2. When the script is starting, Squirrel automatically create the global variable Args that contain the list of arguments given to the script (by calling the script like : ezviewer.sqi /boot/home/Images/beach.gif). We check that the argument list contains two elements, using the primitive llength that returns the size of a list. If the user has given something as argument (not necessary a filename), the script will continue on line 3, else the line 13 will be executed and the script will terminate after that. The first element of the Args list is always the name of the script.

Line 3. The primitive Image (which is a standard primitive) will take as input the name given by the user, and will create an image object to be stored in the variable my.image. The primitive lindex allow us to get the first element of the Args list.

Line 4. We are not sure of what the user will give as argument to the script, so we need to check that what the Image primitive has returned is a valid image. If the image file is not found or can't be loaded, we will simple display an error message one line 13, else we will proceed and display the image in a window.

Line 5. To create a window, we call the primitive Window. Its title will simply be the path of the image. The window object is stored in the variable win. Note that the user won't have the ability to resize the window . This is specified by the not.resizable word.

Line 6. As we have already seen, to display an image we need a special widget. That's what we do on this line, by calling the primitive Viewer. We would have given a size in pixel for this widget if we didn't have an image to display, but as we have already the image we just passed it as input to the primitive. The viewer widget will display automaticaly the image and adapt it size to fit the image.

Line 7. The last step before showing the window is to "pack" the viewer widget on it. This is done by the primitive Glue that "glues" the widget the.viewer on the window. We will see & discuss later more primitives inputs.

Line 8. The final instruction on our script ask the window to be show on screen. When it will show, the window will automatically adapt it size to fit the viewer widget (which holds the image).

Line 10.Display an error message on the Terminal if something has gone wrong.

Line 13.Display an error message on the Terminal if something gone wrong.


There is in our first version, a small issue: we assume that the file given as argument is an image. In some case, like if the file is empty, the Image primitive may succeed creating an image but with an empty content when it should have failed. We going to modify our first version of EzViewer in order to take this into account. We will take the time also to allow the user to drag&drop an image on the window to display it.

The following code implement this upgraded version:


1
use 'GUI' 'Imaging' 'List Processing' 'Storage' 'String Processing'
2
to DragDrop :src :type :msg
3
	if :type = "simple {
4
		make.local "file $msg~find "refs
5
		make.local "m mime.get :file 	
6
		if (sfind :m 'image/') {
7
			make.local "img Image :file
8
			$the.viewer~display :img "adapt
9
			$win~config "title "set :file
10
		} {
11
			Info "stop ["ok] 'error' 'Hmmm ....... doesn\'t look like an image to me :-p'	
12
		}
13
	}
14
end
15
make "my.image false
16
if (llength :Args) = 2 { 
17
	make "m mime.get lindex :Args 2 
18
	if (sfind :m 'image/') {
19
		make "my.image Image lindex :Args 2 
20
		if ($my.image~valid?) { 
21
			make "win Window "titled (lindex :Args 2) [100 100] "not.resizable
22
			make "the.viewer Viewer :my.image 
23
		} { 
24
			print 'Image file not reconized ....... maybe not an image'       
25
	    	} 
26
	}
27
} 
28
if not ($i~valid?) {
29
	make "win Window "titled 'EzViewer' [100 100] "not.resizable
30
	make "the.viewer Viewer [200 200]	
31
}
32
Hook :the.viewer "drop "DragDrop
33
Glue :win "top [] :the.viewer 
34
$win~show


Line 1. We added 2 more Add-Ons to load than the first version, as we going to use few more primitives.

Line 2. We implement the function that will be later call each time a file is dropped on the Viewer widget. The inputs number and contents must respect what is specified in the GUI Programming with Squirrel for this kind of event's hook.

Line 3. The input type indicates if what we have received is a simple message (msg is a BMessage object) or a more complicated one. This can sound a bit complicated, but having access to the message allows you to actually exchange more complex data between applications. For our application, we just expect a simple message holding the file path that have been dropped.

Line 4. We extract from the BMessage the file path and we store it in the local variable file.

Line 5. We check that this file is an image, else we display an error message and we don't load the file. We make use of the primitive mime.get that returns in the local variable m the MIME type of the file.

Line 6. To test that we have an image, we check that the MIME type contains the string image/, using the primitive sfind. If the MIME type is not the one of an image, we will jump to line 11, else to line 7.

Line 7. We create a new Image object, that we store on the local variable img.

Line 8. Time to display the new image. The second input of the method display ask the Viewer widget to adapt its size to fit the full image. This will have the side effect to force the window to adapt also to the new size of its contents.

Line 9. Last things to do in the hook function is to change the title of the window. We use the config method of the window to set it to the path of the image we have just loaded.

Line 11. If the file is not an image, we display an error message using the Info primitive.

Line 16. As for the first version of the script, we check if an image has been specified in the argument.

Line 17. Store in the variable m the MIME type of the file.

Line 18. We check that the MIME type is the one of an image. If this is the case, we display the image else, we go on line 24.

Line 19. Create the Image object and store it in the variable my.image.

Line 20. If the variable my.image is not a valid image, we may have a trouble loading the file, we display an error message as shows on line 24.

Line 21. We create the window with a title that matches the file name. This window will not be resizable by the user.

Line 22. And then we create the Viewer widget. This time, we pass the Image in it and it will adapt its size to fit it.

Line 24. We display an error message, but the script will continue.

Line 28. Whether we have loaded the file given in the command-line, or not, we will execute those lines (from 28 to 34), so we check that the variable my.image is not an image, and then and only then, we create on line 29 a window and on line 30 the Viewer widget. We set a size by default to the size of the widget. The window will adapt itself to fit it.

Line 32. For the widget to react to a user drag&drop action, we setup the hook for such event with the function DragDrop that we have defined ealier.

Line 33. The script is almost over, we pack the Viewer widget in the Window win.

Line 34. We show the window.

Running the script without arguments will now produce a window that look empty:

However, when we drag & drop a file on the window content, the image will be loaded and displayed, unless it's is not an image. In this case the error message will be displayed as shown bellow :

Saving an image in a different format is handled by the Image object it-self, with the method save. If we go back to our first example :


make "my.image Image '/path/to/the/image/beach.gif'

To export this image as a Targa file we will do the following:


$my.image~save 'beach.tga' "TGA

The file extension doesn't matter here. What matters is the second input of which indicates the format of the image file to create. At any time in a Squirrel script, it is possible to know all the installed image translators by calling the trans.name primitive. If we look for the MIME type corresponding to the image file, we will use trans.mime. Both these primitive return a list.

We will use these features of the Image object later on.

Step-by-step building


A very classic user interface for an image viewer is composed of:

We can imagine that the complete version of EzViewer will look like :

For a better understanding, we are going to go step by step in the process of building this Interface.

Creating the main window and the menu bar

Let's start by creating the window and the menubar as show in the next code:


1
make "win Window "titled 'EzViewer' [100 100]	
2
make "bar MenuBar
3
make "m0 Menu '?'
4
make "m1 Menu "File
5
make "m2 Menu "Display
6
$m2~config "radio "set true 
7
make "m3 Menu "Comment
8
$m3~enable false
9
make "m5 Menu "Favorites
10
$bar~add :m0 :m1 :m2 :m3 :m5
11
Glue :win "top [] :bar
12
$win~show


Line 2. Using the primitive MenuBar we create a menu bar widget and we store it in the variable bar.

Line 3, 4 and 5. We create the 3 first menus that will be present on the menubar using the primitive Menu. The labels of the menu will show on the menu bar.

Line 6. The menu Display will be used by the user to select the way the image will be displayed. Only one choice will be possible at a time, so the menu will display a little check mark for the currently selected item of this menu. Using the method config of the menu m2 we set the radio configuration for that purpose.

Line 7. The menu Comment will allow the user to set or delete the comment of an image. Obviously this is only possible when an image is loaded, so for now, we disable the menu using the method enable of the menu widget on line 8.

Line 10. The method add adds a menu to a menu bar. The order the menu are given to the method indicates the order it will be displayed on the menubar as well.

Line 11 and 12. Last thing to do is to pack the menu bar in the window and then show the window on screen.

The result of this first step is show bellow :

Using an icon instead of a label for a menu

The menu item showing as ? will give access to the user manual (an HTML document) and to an about box. A label like ? is not all that nice ... We can easily change it to the icon of the application:


1
make "icon entry.icon :_file "get "small
2
make "win Window "titled 'EzViewer' [100 100]
3
make "bar MenuBar
4
make "m0 Menu :icon
5
make "m1 Menu "File
6
make "m2 Menu "Display
7
$m2~config "radio "set true 
8
make "m3 Menu "Comment
9
$m3~enable false
10
make "m5 Menu "Favorites
11
$bar~add :m0 :m1 :m2 :m3 :m5
12
Glue :win "top [] :bar
13
$win~show


Changes from the previous listing are located on line 1 and 4. Using the primitive entry.icon we get an Image object that contains the small version of the application icon of the script we running. On line 4, you can see that we have simply replaced the '?' by the variable icon that contains the icon.

The application now look like :

Before going on to the next step, we need, for this script to run, to add some initialization code. We call the use primitive that loads all the Add-Ons we are using, as we did for the very first version of EzViewer. For now, we are using the GUI Add-On and the Storage Add-On (for the entry.icon primitive), so the first line of our script will be :

use 'GUI' 'Storage'

Adding the menus items

Let's add now all the menu items to the menus.

The menu application icon will have these items :

The menu File will have for items :

The menu Display will have an item for each possible style of displaying an image in the Viewer widget that we will use later :

The menu Comment have for items :

The menu Favorites will allow the user to add the currently displayed image to the list of his/her favorites images. The user can always from this menu remove an image from his/her favorites list. When an image is added to the favorites list, its name will be added at the last position of this menu. This will allow the user to reload a favorites images from this menu, just by selecting it in the menu:

Let's see the source code for this step:

1
use 'GUI' 'Storage' 'String Processing' 'List Processing'
2
make "icon entry.icon :_file "get "small
3
make "win Window "titled 'EzViewer' [100 100]
4
make "bar MenuBar
5
make "m0 Menu :icon
6
make "m1 Menu "File
7
make "m2 Menu "Display
8
$m2~config "radio "set true 
9
make "m3 Menu "Comment
10
$m3~enable false
11
make "m5 Menu "Favorites
12
make "m4 Menu "Export
13
for.each "format (trans.mime) {
14
	if (sfind :format 'image/') {
15
		$m4~add ('as ' + (safter :format 'image/'))
16
	}
17
}
18
$m4~enable false
19
$m0~add "Help
20
$m0~add "separator
21
$m0~add 'About .......'
22
$m1~add ["Load 'L']
23
make "s0 $m1~add ['Load from ...' 'F']
24
$m1~i.enable :s0 false
25
$m1~add :m4
26
$m1~add "separator
27
$m1~add ["Quit 'Q'] {
28
	$win~quit	
29
}
30
make "s1 $m2~add "Adapt
31
make "s2 $m2~add "Center
32
make "s3 $m2~add "Scale
33
make "s4 $m2~add "Scroll
34
$m3~add ["Edit 'E']
35
$m3~add ["Remove 'M']
36
make "s5 $m5~add ["Add 'A']
37
make "s6 $m5~add ["Remove 'R']
38
$m5~add "separator
39
$m5~i.enable :s5 false
40
$m5~i.enable :s6 false
41
$bar~add :m0 :m1 :m2 :m3 :m5
42
Glue :win "top [] :bar
43
$win~show


Don't be too scared by the size of it, it may look long but it's not very complicate. We haven't added the menu items hooks for this step.

Line 1. We are going to use few more primitives so we add the Add-Ons that needs to be loaded.

Line 12. When the user wants to save the image with another image type, it will use the menu File and the menu item Export to select the type of the new image to create. Instead of a simple menu item, we need to create a submenu m4 with the label Export.

Line 13. To fill the submenu Export with an item for each image type possible, we use the list returned by the primitive trans.mime. This list contains the MIME type of the images that the installed Translators can handle. We simple iterate through a loop over all the elements of this list in order to fill the submenu.

Line 14. Depending on the Translators installed, some may not be able to handle images like application/x-gimp-pattern. This is the reason for testing on this line if the MIME type is one of an image.

Line 15. Using the method add we had each valid MIME type as a menu item. The first input will be the label of the menu item, for cosmetics reasons we extract from the MIME type the part after the image/.

Line 18. The submenu Export will be accessible to the user only when an image is displayed. So we disable this menu for now.

Line 19 to 21. We create the menu item of the Application icon menu. Note that we separate the two items by a special item.

Line 22. Some of the menu items can be invoked when a given keyboard shortcut is pressed by the user. To do so, we simply give to the menu method add a list instead of a string/word. The first element of the list is the label, and the second is the key that pressed with ALT (or the CTRL key) will invoke the menu.

Line 23. The menu item Load from... is enabled only when a favorite image have been displayed. To enable/disable it, we need to keep it in position within the menu, this why we store in the variable s0 the output of the method add. One line 33, you can see how we specify that the item must be disabled for now using the method i.enable of the menu that contains the menu item.

Line 25. Adding a submenu to a menu is quite easy as you can see on this line. We use the same method add with the menu Export as input.

Line 27 to 29. The only hook that we are going to set for now is the one for the menu item that makes the application quit. The shortcut ALT-Q will have the same effect, but note than this shortcut is imposed by the BeOS Interface Kit and will NOT execute the hook we specify for this item.

Line 30 to 33. We add the items of the Display menu. We keep in variables the position of each item in the menu in order to indicate later which display style is selected by default.

Line 34 and 35. We create the items for the Comment menu.

Line 36 to 40. We create the items for the Favorites menu and we disable them as they should be usable only when an image is displayed.


The next couple of images show how the menu looks :

Adding the display area and the status label

To complete our GUI, we need now to add the Viewer widget and the status label. From the previous version, we replace line 41 (to the end) by the following code:


41
$bar~add :m0 :m1 :m2 :m3 :m5
42
make "view Viewer [200 200]
43
$view~config "expand "set true true
44
make "status Frame "lowered
45
$status~config "expand "set true false	
46
make "msg Banner "Status "left
47
$msg~config "high.color "set :Blue
48
$msg~config "expand "set true false
49
Glue :status "top [] :msg
50
Glue :win "top [] :bar :view
51
Glue :win "bottom []  :status
52
$win~show
53
make "Status 'Ready .......'


Line 42 an 43. We create the Viewer widget with a size of 200x200 pixels. On line 43 we set the widget to always fill the available space when the window is resized.

Line 44 an 45. To display a text message, we create first a Frame widget and we give it a lowered (also call sunken) look. We also specify (line 45) that the widget resizes horizontally (its width) whenever the window is resized.

Line 46. A Banner widget is a widget that displays a string. The main advantage of using it instead of a more simple Text widget is that this widget is linked to a variable. When the content of this variable is changed, the widget will display the new value. The first input of the primitive Banner gives the name of linked variable. The second input gives the text justification within the widget.

Line 47. For a better look, we set the color of the text displayed by the widget msg to the color blue.

Line 48. The widget Banner will be placed in the Frame we created earlier. When the frame expands to fit to a new window size, we would like this widget to expand as well so we setup its expand configuation.

Line 49. We glue the Banner widget in the Frame widget.

Line 50 and 51. The way we glue the widgets that go on the window is important. On line 50, we glue first the widgets that always go on top, the menu bar, then the Viewer widget that expand to fill the available place. On the bottom of the window, we glue the Frame that contains the status label. This insures that the Viewer widget will expand in respect to the widgets around it.

Line 53. We set the first status message in the status label, by setting the value of the linked variable Status.


The user can now resize the window and all the widget will follow as shown bellow:

Adding callbacks to the Application icon menu items

The result of this step will be to add the correct callbacks for the Application icon menu item.

The menu item Help is supposed to start your favorite web browser and display an HTML document, this can be done by using the primitive launch. We implement below the function Help which we will use as the menu item callback:


to Help
	launch :_path + '/Manual/index.html'
end

launch will get the MIME type of the file and will start the Preferred Application of this file. The variable _path which is set by Squirrel, contains the complete path to the running script. For this application, the folder where is the main script of EzViewer will also contains a Manual folder.

The second item of the Help menu, displays an About box. We will display in it some useful information like the version number and the email of the author. As well, for a nicer look we will display the icon of the application (icon of the main script file). The following code implement this function:


make "Version '0.5'

to About
	make.local "icon entry.icon :_file "get "large
	Info :icon "Ok ('EzViewer b' + :Version) '\nA simple image
	 viewer written in Squirrel' :_version '\n\nFeedback : 
	jlv@kirilla.com\n\n(c) Kirilla 2001'
end


The first instruction gets the large icon from the script file and store it in the local variable icon. As this is the only place we need this icon, it is better to load it when needed rather than storing it in the global variable. The primitive Info will create an information window. Note that this primitive returns immediately, so the function terminates even before the information window is shown on screen.

One minor notice: you should be aware that Squirrel always executes the menu callbacks (function or block) in a thread separate from the window thread. You do not need to create and start a thread when you have a complex and long task to perform in a menu's callback.

Loading and exporting an image


Loading an image in EwViewer is done by either using the menu, or by a drag&drop action. For the Load menu item, we are going to use a "block" as the callback. We replace line 22 (see section named as 'Adding the menu items') by the following code :


$m1~add ["Load 'L'] {
	make "file FilePanel "open "last.dir ["file] "single "allow ['image/*'] []
	if (is.string :file) {	
		Load :file
	}		
}
make "s0 $m1~add ['Load from ...' 'F'] {
	make "file FilePanel "open (sbefore :thefile '/') ["file] "single "allow ['image/*'] []
	if (is.string :file) {	
		Load :file
	}		
}

The FilePanel primitive displays an Open file or Save file panel from which the user can select a file. It also filters the entries so that the user sees only the relevant files. In this example, we allow only the image files (filtering by MIME type) to be selected. The word last.dir is the name of the variable that contains the last directory browsed by the user. When the user had selected a file, this variable will be updated to the last directory browsed. This allows us to always start this file selection from the last browsed directory. The first input indicates if the panel is used to select a file for opening or saving. Behaviour of the panel will be different depending on this.

If the user cancels the panel without selecting a file, the FilePanel primitive will return the boolean value false, this is the reason for testing that the variable file contains a string. In this case we call a function Load that has the purpose to load and display the image :

1
to Load :f
2
	Busy true
3
	make "Status 'Loading ......'
4
	catch "error {	
5
		make.local "i Image :f
6
		if ($i~valid?) {
7
			$win~config "title "set ('EzViewer' + ' : ' + :f)
8
			$view~display :i
9
			$m3~enable true
10
			$m4~enable true
11
			make "img :i
12
			make "Status 'Loading ... done'
13
			make "thefile :f
14
		} {
15
			throw "error	
16
		}
17
	} {
18
		make "Status 'Loading ... failed!'
19
		$m3~enable false
20
		$m4~enable false		
21
	}
22
	Busy false
23
end


The following screenshot shows how an image wider than the Viewer widget is displayed (centered on the center of the Viewer widget):

Line 2. The Busy primitive sets the application cursor to an animated cursor that indicates that the application is busy.

Line 3. We set the message to be displayed in the status label using the Status variable.

Line 4. For more security we are going to put the image loading inside a special block defined with the catch primitive. This primitive executes the first block started on line 5 and if an error occurs in this block, the second block starting on line 15 will be exectued.

Line 5. We create the Image object and store it in a local variable i.

Line 6. We check than the Image has been loaded correctly.

Line 7. We add the path of the loaded image to the Window title.

Line 8. We display the image by calling the method display. By default, the Viewer widget centers the image to display and does not adjust its size to fit the image.

Line 9. Now that an image is loaded, the user can edit its comment, so we enable the menu Comment

Line 10. We also enable the Export submenu in the File menu

Line 11. The image we have loaded will be accessible outside this function by using the variable img.

Line 12. We update the status.

Line 13. We store in the global variable thefile the path of the loaded image.

Line 15. If the image hasn't been loaded successfully, we throw an error. This error will be caught by the catch and the error block will be executed.

Line 18. We indicate than the loading failed.

Line 19. We disable the Comment menu as the image hasn't beeing loaded.

Line 20. We disable the Export submenu.

Line 22. The last instruction stops the animated cursor.

If we resize the window, the next screenshot show how the Viewer widget center the image:

We now modify the creation of the submenu Export. Each menu item will call the function Export with different inputs. The first input of this function is the MIME type to export in and the second input is the name of the format (TGA , GIF ......):


make "m4 Menu "Export
make "i 1
for.each "format (trans.mime) {
	if (sfind :format 'image/') {
		$m4~add ('as ' + (safter :format 'image/')) "Export :format lindex (trans.name) :i
	}
	make "i :i + 1
}

The Export function is implemented as follows:

1
to Export :mime :ext
2
	make.local "f FilePanel "save "last.dir ["file] "single "allow [:mime] []
3
	if (is.string :f) {
4
		make "Status 'Exporting ......'
5
		Busy true
6
		$img~save :f :ext
7
		Busy false
8
		make "Status 'Exporting ... done'
9
	}
10
end

The MIME type is used to filter the file displayed by the File Panel. On line 6, we use the method save to save the loaded image (stored in the global variable img) to save it under the selected file and with the correct image format.

We need now to handle a drag & dropped image inside the application. For that we are going to re-use what we have already coded for the second version of EzViewer. The function DragDrop is updated as follows :


to DragDrop :src :type :msg
	if :type = "simple {
		make.local "file $msg~find "refs
		make.local "m mime.get :file 	
		if (sfind :m 'image/') {
			Load :file
		} {
			Info "stop ["oooops] 'uh-oh ...' 'Hmmm .....
			 doesn\'t look like an image to me :-p'		
		}
	}
end

The only major change is the call of the function Load to load the file if it is an image. We then have to set this function has the hook function for the correct event on the Viewer widget :


Hook :view "drop "DragDrop

Using the Viewer display styles


As the previous example show us, this version of EzViewer does not fit an image which is bigger than the default window size. We have created the menu Display to allow the user to select the way a loaded image is to be displayed. We're now going to update our application to add this feature.

The Viewer widget has 4 styles for displaying an image. The style can be given as second input of the method display as one of the following word : "adapt "center "scale "scroll.

We modify the creation of the menu item to had a callback block of instructions:

1
make "s1 $m2~add "Adapt {
2
	make "style "adapt
3
	$view~display :style
4
}
5
make "s2 $m2~add "Center {
6
	make "style "center
7
	$view~display :style	
8
}
9
make "s3 $m2~add "Scale {
10
	make "style "scale
11
	$view~display :style
12
}
13
make "s4 $m2~add "Scroll {
14
	make "style "scroll
15
	$view~display :style	
16
}

For each display style, the instructions are pretty much the same. We set the value of the global variable style and then we redisplay the image (if any) with this new style. The variable style can then be used in the Load function to display a loaded image with the current style selected by the user. As well, the last style used by the user will be remembered between session, so we add the following code to check the correct menu item after the creation of the menu items:


if :style = "adapt {
	$m2~i.mark :s1 true	
} {
	if :style = "center {
		$m2~i.mark :s2 true
	} {
		if :style = "scale {
			$m2~i.mark :s3 true
		} {
			$m2~i.mark :s4 true					
		}			
	}		
}

The method i.mark of a Menu check or uncheck a menu item as show bellow :

The following screenshots show the styles of the Viewer widget :


Adapt style


Scale style


Scroll style

Image's comment


BeOS attributes are very elegant and powerfull and they can be very handy to store all kind of data with a file. They're perfect for storing data related to a file, such as the comment of an image. We will use the special attribute EZVIEWER:COMMENT (stored in the global variable label) to store and retrieve the comment from an image file.

Squirrel handles automaticaly the datatype of the attribute, so we don't have to bother with it: if we store a string, we will get a string out of the attribute later on. For the attributes primitives to be available within a script, we need to use the Storage Add-on.

This Add-On provides a rather simple access to the file attributes. For example, querying for the existance of an attribute on a file and retrieving it only if it exists will be :


if (attr.exists 'myfile.txt' 'mykey') {
    make "mykey attr.get 'myfile.txt' 'mykey'
} {
    make "mykey false
}


With the menu Comment the user can set or remove the comment of an image. For this purpose, we implement the functions that will get and set the comment on an image :

1
to GetComment :file
2
    if (attr.exists :file :label) {
3
        output attr.get :file :label
4
    } {
5
        output ''
6
    }
7
end
8
to SetComment :file :comment
9
    attr.set :file :label :comment
10
end


To remove the attribute from the image, we will call the function DelComment, implemented as follows :


1
to DelComment :file
2
    attr.del :file :label
3
end


We now modify the creation of the menu items of the Comment menu to add the callbacks:


$m3~add ["Edit 'E'] "EditComment
$m3~add ["Remove 'M'] {
	DelComment :thefile
	make "Status ''	
}

The item Edit will execute the function EditComment while the other item will simply execute a block that removes the comment from the file's attributes using the function we have coded earlier DelComment. This block will also delete the status label for the image displayed at any given time.

Let's implement now the EditComment that create and display a window for the user to enter the image's comment :

1
to EditComment
2
	if (is.bool :cwin) {
3
		make "cwin Window "titled 'Image\'s comment' [100 100] "not.resizable
4
		make.local "f Frame	
5
		make "comment Entry 'Comment' "thecomment [0 40]
6
		make.local "b Button 'Ok' "navigable.jump
7
		$b~config "align.h "set "center	
8
		Hook :b "invoked {
9
			$comment~invoke
10
			make "cwin false
11
			SetComment :thefile :thecomment
12
			make "Status :thecomment		
13
		}	
14
		Glue :f "top [2 2] :comment	:b
15
		Glue :cwin "top [] :f
16
		$cwin~center "set $win~center "get
17
		$cwin~show	
18
		Focus :comment
19
	}
20
end


Line 2. To ensure than only one edit comment window is displayed at a time (the user may activate the menu item more than once), we are going to store the window created by the function in the global variable cwin. When the window is not displayed, the value of this variable is the boolean value false. Before creating the window, we check that we don't have one already displayed.

Line 3. This window will not be resizable by the user so we specify the flag not.resizable to the Window primitive.

Line 5. To create an entry field, we use the primitive Entry. Its first argument is the label, the second is the name of the variable linked to the widget. The third input gives the size in characters of the label (0) and the entry field it-self (40). The 0 for the label, indicates in fact than the widget will compute the needed size to fit the label text with the font used by the widget.

Line 6. We create a Button widget.

Line 7. We indicate that the button shall always be displayed centered for a better look of the window.

Line 8. Using the Hook primitive, we set-up the instructions to be executed whenever the button b is pressed.

Line 9. When the user invokes the button, it may not have validated the modification the user made in the Entry field with the ENTER key. So we use the widget's method invoke to make sure the linked variable has been updated.

Line 10. The window will be destroyed now that the user have invoked the button. We simply do that by setting the variable cwin to false. The window object will be destroyed and so removed from screen.

Line 11. We set the comment for the image using the function SetComment. Remember than thefile is the complete pathname of the displayed image.

Line 12. We display in the status label the new comment for the image.

Line 16. The method center will move the window to be centered within the main window.

Line 18. The primitive Focus set the keyboard focus on the entry widget.

When the user select the Edit item, the window will be displayed as shown bellow:

Favorites


Like the image's comments, the list of user's favorites images will be stored as file attributes as well. One of possible place for this, is in the main script file. We going now to write severals functions to retrieve and store this list.

Within the application, the list of favorites image will be stored in the global variable favorites.

Let's see the function that stores the list in the script's attributes :

to PutFavorites
	attr.set :_file 'favorites' :favorites
end

This function simply sets the attributes named favorites with the content of the list. Retrieving the list will be equally simple, but we will have to check that all favorites images are still existing, as there is no point adding a missing image in the Favorites menu. The next function looks a little bit more complicated:

to GetFavorites

	make "favorites clone [] 		

	if (attr.exists :_file 'favorites') {
		make.local "list attr.get :_file 'favorites'
		for.each "e :list {
			if (entry.exists :e) {
				lappend :favorites :e		
			}	
		}		
	}
		
end

It extracts, from the file's attributes, the list stored the last time the application was used and then removes from it all the files that no longer exists.

We will use GetFavorites at the application starts, and PutFavorites when the application quits to load and save the variable favorites wich will always contain the list of favorite images.

Once the list has been restored from the file's attributes, we need to add all the elements of this list in the Favorites menu. This will be done by the function MenuFavorites:

to MenuFavorites :menu 
	for.each "f :favorites {
		$menu~add (safter :f '/') "LoadFavorites :f		
	}
end

For each file in the list, we add an item to the menu given as input. The label of this menu will be the filename. LoadFavorites is the callback function to call when the user invokes the item. The only input of this function will be the complete path of the image:

to LoadFavorites :f
	Load :f
	$m1~i.enable :s0 true
end

The callback function simply calls the Load function that we have implemented earlier and enables the Load from ... menu item of the File menu.

If you remember the menu items than we created in Part 2 of this article, the menu Favorites have an item named Add. This item adds the currently displayed image to the list of Favorites. There is also another item that removes the same image from the list. We are going to create two new functions, one will add the image to the list and the other will remove the image from it:

to AddToFavorites :menu :f
	if not (FindInFavorites :f) {
		$menu~add (safter :f '/') "LoadFavorites :f
		lappend :favorites :f
	}
end

This function will be called by the callback of the item Add. It takes as input the Favorites menu and the path of the image to add. It first checks, using the function FindInFavorites that the file is not already in the list, in this case no need to add it again. Then, it adds the menu item like we did in the MenuFavorites function and add the file to the list favorites.

The function that removes an image from the list, will be implemented as follows:

to RemFromFavorites :menu :f
	make.local "index FindInFavorites :f
	if :index {
		$menu~remove (safter :f '/') 
		lremove :favorites :f				
	}	
end

It also checks than the image is in the list, but this time, it processes it only if the file is present. The method remove of the Menu widget seeks the item to remove from its label, this is why we give as input the name of the file. We then remove with the primitive lremove the file from the Favorites list.

The function FindInFavorites simply searches in the Favorites list for a given file and returns its position in the list, or 0 if the file hasn't been found. It is implemented as follows:

to FindInFavorites :f
	output lfind :favorites :f	
end

The last thing we need to do in order to have this feature working, is to update the creation of the menu items of the Favorites menu. We will set the callbacks to call the function we created above:

make "s5 $m5~add ["Add 'A'] {
	AddToFavorites :m5 :thefile	
}
make "s6 $m5~add ["Remove 'R'] {
	RemFromFavorites :m5 :thefile	
}

The following screenshot shows the Favorites menu being used:

Application preferences


Our application is almost finished. The last thing to do is to save the position & size of the window, the display style and the last directory from which a file has been displayed. We will use (once more) the attributes of the main script file to store this informations.

Within the application, we will use the global variables : position wframe last.dir and style to keep access to the application preferences. Here's the function that stores these variables to attributes on the script:

to SavePrefs
	attr.set :_file 'geometry' $win~frame
	attr.set :_file 'lastdir' :last.dir
	attr.set :_file 'style' :style
end

It first stores the output of the frame method of the main window which outputs the geometry of the window as a list. Then we store the last directory used and the style of the display. The position of the window is included in the window geometry, so we do not need to save it.

To load the settings when the application is started, we will call the following function:

to LoadPrefs
	if (attr.exists :_file 'geometry') {	
		make.local "geom attr.get :_file 'geometry'
		make "position lsub :geom 1 2
		make "wframe list ((lindex :geom 3)-(lindex :geom 1)) ((lindex :geom 4)-(lindex :geom 2))
	} {
		make "position [100 100]	
	}
	if (attr.exists :_file 'lastdir') {
		make "last.dir attr.get :_file 'lastdir'		
	}
	if (attr.exists :_file 'style') {
		make "style attr.get :_file 'style'		
	}		
end

The function retreives first the geometry of the window. We extract from the list the position and size of the window. These two values are stored in the variables position and wframe. Then the function get the last used directory and the display style.

For the window to move and resize according to the preferences, we need to modify the main script, first by using the variable position in the window creation:


make "win Window "titled 'EzViewer' :position

And then by resizing the window before it is shown:

if not (lempty :wframe) {
	$win~resize.to (lindex :wframe 1) (lindex :wframe 2)
}

Source code reorganization


Before releasing EzViewer, is it legimate to see if the application is:

This can be attained by adding comments in the source code, and dividing the application into severals script files.

Adding comments in the code is a rather simple task, yet probably a bit boring, but still an important thing to do to insure than maintaining the application will be easy as possible.

It is not the purpose of this article to cover the best commenting strategies, so we will just focus on how we can simply break the application into severals files.

>From the way we have build EzViewer we can see that there are several logic units within the application:

Each of these units can be separated from the others, to form a script file for each. The main script will contain the building of the GUI. When launching this script, it will load the rest of the scripts. This will be done by the following code:


make "to.load [
	'Preferences.sqi' 
	'Comment.sqi'
	'Functions.sqi'	
	'Favorites.sqi'
]

for.each "f :to.load {
	load :_path + '/Scripts/' + :f
}

The variable to.load is a list of all the script files than the application needs to load. We use the primitive for.each to iterate over all the element of this list and load them. All those files are stored in the Script folder of the EzViewer distribution. The _path variable is set by Squirrel to the path of the running script.

Conclusion


I have make two points in this article. First, BeOS need programming tools for a wider range of computer users. Second, using Squirrel can lead to time saving in software development and to more simplicity. I invite you to download EzViewer and try it out, I'm open to any critics or questions you may have.

Since the beginning of this project (June '99), I have looked forward the time where Squirrel will be powerfull enough to allow me to rewrite YataBe, a FITS image viewer that I had prototyped at that time. From my professional experience, it was clear than a good astronomer tool will definitly benefit from a useful scripting language. Providing the end user with a simple way to expand the application and change it to his/her outfit, is a winner.

With the current release of Squirrel, I have the feeling than this application has never been so close to be rewrite. Almost 2 years after the beginning of the project, it realization should demonstrate (as well as this article I hope) that Squirrel is definitly ready for mainstream.

I would like to thanks (in alphabetical order):

for there much appreciated help and support through the writing of this article. Of course, I won't forget Susan, who by her constant support had make all of this possible.