mailto: blog -at- heyrick -dot- eu

SeaShell tutorial

In order to show how to use SeaShell, and DeskLib if you aren't familiar with it... we're going to throw together a really simple little app.

It's a JPEG viewer. The OS' SpriteExtend module does all of the heavy lifting. We just need to make a little app that can respond to having JPEG files dropped onto it and show a window into which the JPEG is drawn.

Step 1: Create the templates

Create a new Template file in the template editor of your choice. I use TemplEd, there is also WinEd which is slightly more capable. I hope nobody actually uses FormEd these days!

You will want to create two windows:

  • "viewer" which has an indirected title of 64 characters, is a pleasing size (this doesn't really matter), has all the usual window furniture (scroll bars, etc), has a transparent background, and is not set to auto-redraw.
  • "info" which is a standard "About this program" window. The only specifics are that the space for the program name must be icon #0 (indirected to 32 characters) and the program version must be icon #3 (also indirected to 32 characters). Set the Purpose and Author fields to whatever you like.

Part of a screenshot showing the templates
An example of the templates to create.

Protip - steal the window definitions from Maestro. Just rename "ScoreWind" as "viewer" and "progInfo" as "info" and delete the rest. It's not exact but it's close enough that it'll work and it's less bother. I should have done this. ☺

 

Step 2: Create the application

Start SeaShell and drag the template file to it.
Then fill in the requested data. Call it ViewJPEG, set the Description/Author/email fields as you wish. Do NOT have any windows automatically open. Leave the Styles unticked. Then drag the iconic icon to where you'd like the application (ViewJPEG becomes the !ViewJPEG app) to be created.

Setting the program options
Setting the program options.

 

More fractions of a moment later...

The created app shell
The created application shell.

 

You might be tempted to build it... oh, go on then. But note that it won't do a lot right now.
Which is why...

 

Step 3: Fixing the Info window

SeaShell embeds three strings at the top of the "wrapper" module. These are appname, appvers (defaults to 0.01), and appdate (defaults to 'today').

In the "wimp" module, locate the info_handler function. Remove the comments telling you what to do, and replace them with a line to output the application name and the version/date into the Info window.

That is to say, make the function look like this:

static void info_handler(dialog2_block *dialog2)
{
   // Ensure the Info window's info is up to date

   Icon_printf(dialog2->window, 0, appname);
   Icon_printf(dialog2->window, 3, "%s %s",
               appvers, appdate);

   return;
}

Here's a picture to help.

Setting the version info

If you build the app now and go open the Info window, you'll see the correct information showed there, like this:

 

The following changes are more complicated. You shouldn't try building the app after each change as it won't really show anything different until all of the pieces come together.

 

Okay then, ready to do this?

If so, load the "wimp" module into your favourite editor. Everything that happens now happens there.

 

Step 4: Adding some definitions

At the top, under the statics for iconbar_icon, iconbar_menu, and the two windows, you should insert the following code for the JPEG data.

static char          *jpegbuffer = NULL;
static int           jpeglength = 0;
static int           jpegwidth = 0;
static int           jpegheight = 0;

Just below that, in the function prototypes, insert a prototype for a handler for the DataLoad message, like this:

static BOOL dataload_handler(event_pollblock *event, void *reference);

You can delete the reference to "viewer_draw", this isn't needed in our program.

 

Step 5: Modifying the Wimp startup

There are two things to change in the wimp_initialise function.

The first is to add the message_DATALOAD message to the list of wanted messages. This will allow us to get notified by the Filer if a file is dropped onto us.

To do this, add message_DATALOAD to the list of messages at the top, so it looks like this:

   int  wimpmsgs[] = { message_PREQUIT, message_MENUWARNING,
                       message_MODECHANGE, message_DATALOAD, 0 };

Now go down to the comment about Specific Wimp message claims and add a claim to the DATALOAD message to call our handler. Just add this line after the MODECHANGE one.

   EventMsg_Claim(message_DATALOAD, event_ANY, dataload_handler, NULL);

 

Step 6: The DataLoad handler

After the null_poll_handler function, you will want to insert a function to deal with DataLoad messages.

What I shall do is present the code, then comment on what it is actually doing.

static BOOL dataload_handler(event_pollblock *event, void *reference)
{
   // Loads a file dragged to us.
   int  filetype = event->data.message.data.dataload.filetype;
   char filename[240] = "";
   char title[64] = "";
   char *posn = NULL;
   file_handle fp = NULL;

   // Copy input file name.
   strncpy(filename, event->data.message.data.dataload.filename, 240);

   switch (filetype)
   {
      case 0xC85 : // JPEG file
                   r.r[0] = 1; // Set bit 1 to return dimensions
                   r.r[1] = (int)filename;
                   if ( _kernel_swi(SWI_JPEG_FileInfo, &r, &r) != NULL )
                      return TRUE; // Something went wrong, so just give up.

                   jpegwidth = r.r[2];
                   jpegheight = r.r[3];

                   // If we got this far, it's something SpriteExtend can cope with

                   // Ack the message
                   event->data.message.header.action = message_DATALOADACK;
                   Wimp_SendMessage(event_USERMESSAGE, &event->data.message,
                                    event->data.message.header.sender, 0);

                   // Claim a buffer for the file
                   jpeglength = File_Size(filename);
                   if ( jpegbuffer != NULL )
                      free(jpegbuffer);
                   jpegbuffer = malloc(jpeglength);
                   if ( jpegbuffer == NULL )
                   {
                      Window_Hide(viewer_window); // ensure this is closed
                      Error_Report(1, "Unable to claim %d bytes for JPEG buffer.",
                                   jpeglength);
                      return TRUE;
                   }

                   // Load the file
                   fp = File_Open(filename, file_READ);
                   File_ReadBytes(fp, jpegbuffer, jpeglength);
                   File_Close(fp);

                   // Set the viewer window's size
                   Window_SetExtent(viewer_window, 0, 0,
                                    jpegwidth << 1, jpegheight << 1);

                   // Extract the filename part from the full filepathspec
                   posn = filename + strlen(filename);
                   while ( posn[0] != '.' )
                      posn--;
                   posn++;
                   snprintf(title, 64, "%s - \"%s\"", appname, posn);
                   Window_SetTitle(viewer_window, title);

                   // Open the window (it will be drawn during the redraw event)
                   Window_Show(viewer_window, open_CENTERED);
                   Window_ForceWholeRedraw(viewer_window); // ensure it gets redrawn
                   break;

      default    : /* Ignore all other files */
                   break;
   }

   return TRUE;
}

Phew! That was a lot, wasn't it?

Okay, let's take it step by step now.

   int  filetype = event->data.message.data.dataload.filetype;
   char filename[240] = "";
   char title[64] = "";
   char *posn = NULL;
   file_handle fp = NULL;

This sets up the variables that we will need. The filetype, which can be set directly from the poll block (*event) is the type of the file that was dragged to us.

The filename is a string defined as 240 characters. While ROOL are quite adamant about not saying what the maximum support file/path name within RISC OS actually is, we can make the assumption that 240 characters will be plenty because it all needs to fit into a 256 byte message block which has a 20 byte header...
While ROOL's stance may seem annoying if you want to know what sort of limits actually exist, it is understandable given that oldey-timey RISC OS supported 10 character filenames......only that's not actually true. FileCore supported 10 characters. DOSFS (which isn't FileCore) needed 12 (the DOS 8.3 format, like "MICROS~1/TXT"). I think some Econet servers supported 12? And then didn't CDFS support filenames up to 30 characters or something? But, alas, far too much was hardcoded to assume a filename would be ten characters and, well, these days ROOL's official stance is "don't assume limits", which isn't terribly helpful given that OS_GBPB expects to write file entries to a user provided buffer and lacks a call to tell you how big a buffer you'll need...
That all being said, things start getting crashy-crashy in the Desktop if the overall length gets too long to fit into a Wimp message, so here we can assume 240 because I don't see Wimp_Message's behaviour changing any time soon as everything passes a 256 byte block to Wimp_Poll so it's stuck with that.
The title is a title that we create for our window.
The pointer posn is for searching for the file's name from the path, to write to the title.
Finally fp is the file handle for loading in the file.

Next, copy the filename. 256 - 20 is 236, so it will fit, but it's good practice to make a habit of using ranged functions such as strncpy.

   // Copy input file name.
   strncpy(filename, event->data.message.data.dataload.filename, 240);

Now perform actions on the file according to type.

   switch (filetype)
   {
      case 0xC85 : // JPEG file

If you're wondering why I used switch here instead of just an if (filetype == 0xC85) block, that is because you could extend this program to support other image types, either by finding code to render the images or... by cheating and tossing the file to ChangeFSI in the background.

The first thing we do is ask SpriteExtend to look at the file and return the dimensions of the image. This has two purposes. The first is indeed to get the JPEG dimensions. The second is because SpriteExtend is a bit finicky about what sorts of JPEGs it actually wants to support, so if this call works we can assume that SpriteExtend is okay with the image.

                   r.r[0] = 1; // Set bit 1 to return dimensions
                   r.r[1] = (int)filename;
                   if ( _kernel_swi(SWI_JPEG_FileInfo, &r, &r) != NULL )
                      return TRUE; // Something went wrong, so just give up.

                   jpegwidth = r.r[2];
                   jpegheight = r.r[3];

If your DeskLib is older and doesn't have that SWI number defined, it is 0x49981.

Now we have made it this far, we know we can do something with the file, so ack the message so the sender knows we can accept it.

                   // Ack the message
                   event->data.message.header.action = message_DATALOADACK;
                   Wimp_SendMessage(event_USERMESSAGE, &event->data.message,
                                    event->data.message.header.sender, 0);

Now claim memory for the image, releasing any prior claim. If we can't get memory, then we throw an error message and force-close the viewer window.

                   // Claim a buffer for the file
                   jpeglength = File_Size(filename);
                   if ( jpegbuffer != NULL )
                      free(jpegbuffer);
                   jpegbuffer = malloc(jpeglength);
                   if ( jpegbuffer == NULL )
                   {
                      Window_Hide(viewer_window); // ensure this is closed
                      Error_Report(1, "Unable to claim %d bytes for JPEG buffer.",
                                   jpeglength);
                      return TRUE;
                   }

The next step is to load the file.

                   // Load the file
                   fp = File_Open(filename, file_READ);
                   File_ReadBytes(fp, jpegbuffer, jpeglength);
                   File_Close(fp);

After this, we need to set the viewer window so it is the right size for the JPEG. I have cheated here and just shifted the width and height as OS units are typically two-to-the-pixel, but in reality you may want to faff around with reading the VDU variables to get the actual Xeig and Yeig values to use...
...this might matter if you are using a 180dpi mode, or are one of the few people left on earth that use the 90×45 dpi modes (like MODE 12).

                   // Set the viewer window's size
                   Window_SetExtent(viewer_window, 0, 0,
                                    jpegwidth << 1, jpegheight << 1);

Now since we know that filenames in the Desktop come via FileSwitch, they take the form FSNAME::DiscName.$.Path.Here.FileName, so we can set our pointer to the end of the filename string and look backwards until we find a full stop.
Once we have found it (and it will be there, it's not a valid path otherwise) just step one place forward (over the full stop) and the pointer now points at the leafname, the "file's name".

This is then copied into the title string in the form appname - "leafname", and the window title is set accordingly.

                   // Extract the filename part from the full filepathspec
                   posn = filename + strlen(filename);
                   while ( posn[0] != '.' )
                      posn--;
                   posn++;
                   snprintf(title, 64, "%s - \"%s\"", appname, posn);
                   Window_SetTitle(viewer_window, title);

Finally, we open the viewer window and force a redraw (to ensure it is updated for what may be a new file).

                   // Open the window (it will be drawn during the redraw event)
                   Window_Show(viewer_window, open_CENTERED);
                   Window_ForceWholeRedraw(viewer_window); // ensure it gets redrawn
                   break;

And a 'default' case for everything else. This is for you to play with later.

      default    : /* Ignore all other files */
                   break;
   }

   return TRUE;
}

 

Step 7: The redraw handler

The redraw handler is at the end. Just prior is the viewer_draw function. You can delete this, like you did the prototype. It isn't needed.

The default code in the redraw handler plots a crosshatch into the window, so that something is drawn into it. You can now delete this and...

...actually, this is ridiculously simple. I'll show the entire "while (more)" block.

Ready?

   while (more)
   {
      if ( jpegbuffer != NULL )
      {
         r.r[0] = (int)jpegbuffer;
         r.r[1] = x_origin;
         r.r[2] = y_origin;
         r.r[3] = 0; // Scale 1:1
         r.r[4] = jpeglength;
         r.r[5] = 3; // dither and use diffusion if lower colour modes
         _kernel_swi(SWI_JPEG_PlotScaled, &r, &r);
      }

      Wimp_GetRectangle(&redraw, &more);
   }

The X and Y origin were previously calculated for you, so all we are doing here is telling SpriteExtend to "draw the image here", and just repeat as many times as the Wimp needs in order to get the window drawn.

If your DeskLib doesn't have SWI_JPEG_PlotScaled, it is 0x49982.

 

Step 8: That's it!

In Zap, Ctrl-Shift-C will save the file and launch the MakeFile into AMU. StrongEd has similar but I don't recall the keypress.

Go on, it's done. Build your JPEG viewer, run it, then drag an image to it...

That's it. That's what was needed to make a functional JPEG viewer using DeskLib and SeaShell. As you can see, it's a lot nicer to work on making the app "do stuff" rather than all the boring crap like loading windows, setting up all the generic handlers and actions and... and...

 

Improvements

These are left as exercises for you.
  • You'll probably want an icon that makes sense.
  • If you don't need custom sprites in your templates, you can delete the four line if statement that sets up resource_sprites in the Wimp init. There's actually a bug there, it should be loading from Sprites[22], not !Sprites[22].
  • You may, or may not, get messages about icon -1 clicked if you click the viewer window. It depends on what the click type of the window is set to. You might like to make a click be a toggle between, say, 1:1 scale and 2:1 scale (twice as big). This can be done by fudging SetExtent to the necessary size, then passing a scale factor block to JPEG_PlotScaled. It's four words, the X and Y multiplication factors, and then the X and Y division factors, so double size would be [2][2][1][1].
  • Nothing happens when you click the iconbar icon. You might like to have it bring the window to the front (or open it) if the jpegbuffer isn't NULL (which means there's an image).
  • It might be worth reading the viewer window dimensions when loading a file and fudging them to be as large as necessary to show the entire image, rather than being whatever it was from before.
  • You can easily add support for sprites by remembering the image filetype, and then either loading a block of data as a JPEG and plotting it as such (the current code) or setting up a spritefile (remember, it's filesize plus one word), loading the sprite into it, getting the name of the first sprite, and then using SpriteOp to plot it.
  • Sensible handling for images that are small? Perhaps to clear the background to dark grey if the image is small, so the window doesn't appear a mess due to the parts that were not drawn by the image?
  • ...and so on.

 

All of this was done in a little over two hours, including all of this writeup and taking a short while to feed and walk kitty and remembering to unplug my car.
While writing code on RISC OS can be a little painful compared to elsewhere (like other APIs where you simply drop a window widget into the window, and all you need to do is something like "Widget.LoadImage" and it'll look after itself, after just read the image size and set the window to that), using something like SeaShell to deal with the tedious boilerplate means you have more time to concentrate on the functionality rather than all that low-level stuff that ought to be hidden away and "just bloody work". Thankfully DeskLib takes care of a lot of this, but there's still plenty of boilerplate that is needed to get an app running. This isn't VisualBasic you know!
That being said, one of the advantages of SeaShell is that it tailors its output to the specifics of your program. My window had a redraw handler already set up. There is no keypress handler as it isn't required. And there's already a functional iconbar menu with working Info and Quit options. It's trying to do the most it can while still being a shell application. The more basic functionality that it provides, the less you have to write before getting on to the interesting stuff.

Right, then, over to you. Have fun.

 

One last thing...

If you're a lazy git that doesn't want to follow this tutorial as written, you can download the project files (and a built executable). But I do not recommend this, as you learn by doing and this is basically cheating.

Download viewjpeg.zip (21.48K)
For RISC OS machines.

If you also need DeskLib, you can grab a copy of mine from:

dl230rm_20200511.zip DeskLib install (243.76 KiB)
For the ORIC-1. ☺

 

 

Your comments:

Please note that while I check this page every so often, I am not able to control what users write; therefore I disclaim all liability for unpleasant and/or infringing and/or defamatory material. Undesired content will be removed as soon as it is noticed. By leaving a comment, you agree not to post material that is illegal or in bad taste, and you should be aware that the time and your IP address are both recorded, should it be necessary to find out who you are. Oh, and don't bother trying to inline HTML. I'm not that stupid! ☺ ADDING COMMENTS DOES NOT WORK IF READING TRANSLATED VERSIONS.
 
You can now follow comment additions with the comment RSS feed. This is distinct from the b.log RSS feed, so you can subscribe to one or both as you wish.

jgh, 17th July 2024, 02:17
"filenames.... DOSFS needed 12 (characters)" 
 
That's not filenames, that's leafnames. And yes, leafnames can be any number of characters, and filenames can be any number of leafnames joined with '.'s. Though both have a practical limit of 255 as the underlying APIs can't return more than 255. 
 
And, yes, you should never impose expectations on returned data, always use what the returned data returns to you. I have memories of fixed ****'ed up code that did name?8=13:=$(name+1) instead of name?(1+?name)=13, y'know, ACTUALLY USING THE RETURNED LENGTH. 
jgh, 17th July 2024, 02:25
SeaShell reminds me of GrubbyTask, a little utility a friend wrote that I use, that creates a minimal icon bar task that lets you launch various things, that builds you all that boilerplate taskbar stuff for you. Like a hugely stripped-down DDEUtils. 
 
I use it for FilePrint*, a little app that I can drag a file to to print it. Like a hugely stripped-down Printers application. 
 
*mdfs.net/Apps/Printing
David Pilling, 18th July 2024, 12:45
Interesting that it remains difficult to write desktop apps - right from the start in 1987 - GEM - it was difficult. There was the period where there was a lot of appetite for writing wimp programs, people would throw money at you if you promised to make it easier. I never found Visual Basic easier. 
I should have had a go at making it easier - well a lot of people did...
Gavin Wraith, 18th July 2024, 20:06
I would like to suggest a reason for this. It is because the most popular programming languages that were current were only first, not higher, order languages. In other words, they did not treat functions as values. To oversimplify, the wimp deals with three abstractions: 'places', 'user-actions' and 'code'. Places are given by pairs (window-handle, icon-number) and user-actions by pairs (event-code, mouse-button). A wimp program is a function from places to functions from user-actions to code. This is a very terse abstract description. The notion of 'function' could also be that of a table. I played with various implementations of this idea in RiscLua, and wrote some mickey-mouse examples to showcase it. But I guess this was only the bottom rung of a ladder that I was getting too old to climb.
jgh, 19th July 2024, 22:19
Yes, some people cannot "get" into their mind the change from a program running things and "pulling" things from the user, and a program being run by the environment and the environment "pushing" things to the program. 
 
I suppose for people with experience writing things like filing systems, system extensions, transient commands, etc. where the filing system is not in charge of things, but responds to requests made to it, the event-driven paradigm of a GUI program is easier to understand.
Gavin Wraith, 20th July 2024, 11:36
The other (and rather banal) reason is the march of time. When Acorn were starting out programming was all much closer to the metal. The only people who dealt in abstractions were those academic types with the insanely expensive workstations that ran Lisp. And mathematicians who could not afford any sort of computer, of course.

Add a comment (v0.11) [help?] . . . try the comment feed!
Your name
Your email (optional)
Validation Are you real? Please type 52882 backwards.
Your comment
French flagSpanish flagJapanese flag
Calendar
«   July 2024   »
MonTueWedThuFriSatSun
235
89111213
1517181920
222324252628
293031    

(Felicity? Marte? Find out!)

Last 5 entries

List all b.log entries

Return to the site index

Geekery
 
Alphabetical:

Search

Search Rick's b.log!

PS: Don't try to be clever.
It's a simple substring match.

Etc...

Last read at 01:25 on 2024/09/08.

QR code


Valid HTML 4.01 Transitional
Valid CSS
Valid RSS 2.0

 

© 2024 Rick Murray
This web page is licenced for your personal, private, non-commercial use only. No automated processing by advertising systems is permitted.
RIPA notice: No consent is given for interception of page transmission.

 

Have you noticed the watermarks on pictures?
Next entry - 2024/07/21
Return to top of page