Rick's b.log - 2024/07/16 |
|
It is the 21st of November 2024 You are 18.217.10.200, pleased to meet you! |
|
mailto:
blog -at- heyrick -dot- eu
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.
You will want to create two windows:
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. ☺
More fractions of a moment later...
You might be tempted to build it... oh, go on then. But note that it won't do a lot right now.
In the "wimp" module, locate the
That is to say, make the function look like this:
Here's a picture to help.
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.
Just below that, in the function prototypes, insert a prototype for a handler for the DataLoad message, like this:
You can delete the reference to "viewer_draw", this isn't needed in our program.
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:
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.
What I shall do is present the code, then comment on what it is actually doing.
Phew! That was a lot, wasn't it?
Okay, let's take it step by step now.
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...
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.
Now perform actions on the file according to type.
If you're wondering why I used switch here instead of just an
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.
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.
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.
The next step is to load the file.
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...
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.
This is then copied into the title string in the form
Finally, we open the viewer window and force a redraw (to ensure it is updated for what may be a new file).
And a 'default' case for everything else. This is for you to play with later.
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?
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.
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...
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.
Right, then, over to you. Have fun.
If you also need DeskLib, you can grab a copy of mine from:
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.
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!
An example of the templates to create.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.
The created application shell.
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').
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.
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;
}
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;
static BOOL dataload_handler(event_pollblock *event, void *reference);
Step 5: Modifying the Wimp startup
There are two things to change in the wimp_initialise function.
int wimpmsgs[] = { message_PREQUIT, message_MENUWARNING,
message_MODECHANGE, message_DATALOAD, 0 };
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.
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;
}
int filetype = event->data.message.data.dataload.filetype;
char filename[240] = "";
char title[64] = "";
char *posn = NULL;
file_handle fp = NULL;
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.
// Copy input file name.
strncpy(filename, event->data.message.data.dataload.filename, 240);
switch (filetype)
{
case 0xC85 : // JPEG file
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.
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];
// 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);
...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);
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".
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);
// 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;
}
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.
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);
}
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.
Improvements
These are left as exercises for you.
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.
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.
For RISC OS machines.
For the ORIC-1. ☺
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/PrintingDavid 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.
© 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. |