A while ago we received a bug report that included scanned-in polaroids of an engineer's BIOS screen and serial card. Pretty twisted—but we had fun using Magnify to examine the pictures. The only problem was that the image that Magnify showed was pixelated. The solution: Magnify filter add-ons.
This article shows you how to add add-on functionality to Magnify. It should also work as a tutorial for adding similar functionality to your own app. Add-on recognition is simple to incorporate, and it's a great way to extend, or have others extend, the functionality of an application.
To follow this article, open the sample source for Magnify and copy and paste. Note that all of the image-manipulation work is done in the add-ons themselves; Magnify just calls the add-on's functions to process the image.
We start by adding a couple of global variables to the app. The variables
point to the filter's entry point function (gFilterFunc
)
and image id (gAddOnImageID
). The function takes a
BBitmap
object; this is the image that the filter
will work on:
void (*gFilterFunc
)(BBitmap
*); image_idgAddOnImageID
;
We initialize the variables in the TWindow
constructor:
TWindow
::TWindow
(int32pixelCount
) :BWindow
(BRect
(0,0,0,0), "Magnify",B_TITLED_WINDOW
,B_OUTLINE_RESIZE
) {gFilterFunc
= 0;gAddOnImageID
= 0; ... }
We also create a global function that examines Magnify's add-on directory (a subdirectory of the default add-on directory). We create a menu item for each file we find (each file is assumed to be a Magnify filter):
static voidAddFilters
(BMenu
*menu
) {BPath
path
;find_directory
(B_USER_ADDONS_DIRECTORY
, &path
);path
.Append
("Magnify");BDirectory
dir
(path
.Path
()); // only add the rest of the menu if filters exist if (dir
.CountEntries
() return;menu
->AddSeparatorItem
();BMessage
*msg
= newBMessage
('filt');msg
->AddString
("filter", "disable");BMenuItem
*menuItem
= newBMenuItem
("Disable Current Filter",msg
);menu
->AddItem
(menuItem
); int32index
=1; entry_refref
; while (true
) { status_terr
=dir
.fffffffffGetNextRef(&ref); if (err
==B_ENTRY_NOT_FOUND
) break; else {msg
= newBMessage
('filt');msg
->AddString
("filter",ref
.name
);menuItem
= newBMenuItem
(ref
.name
,msg
);menu
->AddItem
(menuItem
);index
++; } } }
AddFilters()
is called alongside the app's other menu-building functions,
in TInfoView
's AddMenu()
function:
voidTInfoView
::AddMenu
() {fMenu
= newTMenu
(dynamic_cast(Window
()), "");BuildInfoMenu
(fMenu
);AddFilters
(fMenu
); ... }
When the user selects a filter from the menu, TWindow::MessageReceived()
catches the in-coming "load filter" message (which contains the name of
the filter) and passes it to TWindow::LoadFilter()
:
voidTWindow
::MessageReceived
(BMessage
*m
) { ... case 'filt':LoadFilter
(m
); break; ... }
In TWindow::LoadFilter()
, we reconstruct the pathname to the filter, and
load it by passing the pathname to load_add_on()
(first unloading the
previous filter). We then call get_image_symbol()
to find the add-on's
filter()
function—this is our interface to the add-on's functionality:
voidTWindow
::LoadFilter
(BMessage
*m
) { char*fname
;m
->FindString
("filter", (const char**)&fname
); if (fname
) { if (strcmp
(fname
, "disable") == 0) {gFilterFunc
= 0;gAddOnImageID
= 0; return; } // unload the previous add-on, free its memory, etc. if (gFilterFunc
)unload_add_on
(gAddOnImageID
);BPath
path
;find_directory
(B_USER_ADDONS_DIRECTORY
, &path
,true
);path
.Append
("Magnify");path
.Append
(fname
); // load the add-ongAddOnImageID
= load_add_on(path
.Path
()); if (gAddOnImageID
< 0) {printf
("ERROR: can't load addon\n"); return; } // get the filter function symbol intresult
=get_image_symbol
(gAddOnImageID
, "filter",B_SYMBOL_TYPE_TEXT
,(void**)&gFilterFunc
); if (result
< 0) {printf
("ERROR: can't get image symbol\n"); return; } } }
The filter code is now loaded, and gFilterFunc
points
to its filter()
function. To call the function, all we have to do is pass it the bitmap
that's displayed on screen and render the result. We do this from the
TOSMagnify::CreateImage()
function, just before
the DrawBitmap()
call:
boolTOSMagnify
::CreateImage
(BPoint
mouseLoc
, boolforce
) { ... if (gFilterFunc
) (*gFilterFunc
)(fBitmap
);DrawBitmap
(fBitmap
,srcRect
,destRect
); ... }
What would add-on support be without some sample add-ons. Here is some sample source for a couple filters (thanks to Victor Tsou for coming up with this idea and creating both of these filters).
This first filter (continuously) rotates the Magnify image.
#include <math.h> #include <stdio.h> #include <Bitmap.h> extern "C" _EXPORT voidfilter
(BBitmap
*image
);BRect
bounds
; uint32 *bits
=NULL
; int *xcosT, *xsinT
; intframe
= 30; #definemax
(a
,b
) (((a
)>(b
))?(a
):(b
)) #definePI
3.14159265 #defineSCALE
1 voidfilter
(BBitmap
*image
) {frame
= (system_time
() / 200000) % 360; if (bits
&& (bounds
!=image
->Bounds
())) { deletebits
;bits
=NULL
; deletexcosT
;xcosT
=NULL
; deletexsinT
;xsinT
=NULL
; } if (!bits
) { int32m
;bounds
=image
->Bounds
();bits
= new uint32[(image
->BytesPerRow
() / 4) * (bounds
.Height
() + 1)];m
=max
((int32)bounds
.Width
(), (int32)bounds
.Height
());xcosT
= new int[m
];xsinT
= new int[m
]; } // prepare for 27.5 fixed point floatsinT
= 32*sin
(frame
*PI
/180*SCALE
),cosT
= 32*cos
(frame
*PI
/180*SCALE
); intcenter_x
=image
->BytesPerRow
()/8,center_y
= (int32)bounds
.Height
()/2; intshiftx
= (int)(-center_x
*cosT
-center_y
*sinT
+center_x
*32),shifty
= (int)(center_x
*sinT
-center_y
*cosT
+center_y
*32); intnewx
,newy
; int32 *dest_int32
= (int32 *)bits
, *bitmap_bits_int32
= (int32*)image
->Bits
(); intwidth
=image
->BytesPerRow
() / 4,height
= (int)image
->Bounds
().Height
() + 1; for (intz
=((width
>height
)?width
:height
)-1;z
>-1;z
--) {xcosT
[z
] = (int)(z
*cosT
);xsinT
[z
] = (int)(z
*sinT
); } for (inty
=0;y
<height
;y
++) { intysinT
= (int)(y
*sinT
),ycosT
= (int)(y
*cosT
); intbasex
=ysinT
+shiftx
+ (3*width
<< 5),basey
=ycosT
+shifty
+ (3*height
<< 5); for (intx
=0;x
<width
;x
++) {newx
= ((basex
+xcosT
[x
]) >> 5) %width
;newy
= ((basey
-xsinT
[x
]) >> 5) %height
; *(dest_int32
++) =bitmap_bits_int32
[newx
+newy
*width
]; } }memcpy
(image
->Bits
(),bits
,image
->BytesPerRow
() * (int)(bounds
.Height
()));frame
++; }
The second filter makes the image wave. Just build and run it, the visual is better than a description.
#include <math.h> #include <stdio.h> #include <Bitmap.h> extern "C" _EXPORT void filter(BBitmap
*image
);BRect
bounds; uchar *tbits; int32 bpr; #definePI
3.14159265 voidfilter
(BBitmap
*image
) { int32w
,h
,x
,y
; uchar *bits
; int32bpp
; intframe
= (system_time
() / 20000) % 360; if (tbits
&& ((bounds
!=image
->Bounds
()) || (bpr
!=image
->BytesPerRow
()))) { deletetbits
;tbits
=NULL
; } if (!tbits
) {bounds
=image
->Bounds
();tbits
= new uchar[image
->BytesPerRow
()];bpr
=image
->BytesPerRow
(); }w
= (int32)bounds
.Width
() + 1;h
= (int32)bounds
.Height
() + 1;bits
= (uchar *)image
->Bits
(); switch (image
->ColorSpace
()) { caseB_RGB32
: caseB_RGB32_BIG
:bpp
= 4; break; caseB_RGB16
: caseB_RGB16_BIG
: caseB_RGB15
: caseB_RGB15_BIG
:bpp
= 2; break; caseB_CMAP8
:bpp
= 1; break; default :printf
("Unknown color space (%x)\n",image
->ColorSpace
()); return; } for (y
=0;y
<h
;y
++) { int32delta
= (int32) (30 *sin
((10 *frame
+y
) *PI
/180) + 15 *sin
(( 7 *frame
+ 3 *y
) *PI
/180)); uchar *s
;s
=bits
+y
*image
->BytesPerRow
(); if (delta
< 0) {delta
= -delta
;memcpy
(tbits
,s
,bpp
*delta
);memcpy
(s
,s
+bpp
*delta
,bpp
* (w
-delta
));memcpy
(s
+bpp
* (w
-delta
),tbits
,bpp
*delta
); } else if (delta
> 0) {memcpy
(tbits
,s
,bpp
* (w
-delta
));memcpy
(s
,s
+bpp
* (w
-delta
),bpp
*delta
);memcpy
(s
+bpp
*delta
,tbits
,bpp
* (w
-delta
)); } } }
To build these filters, create a BeIDE "SharedLibrary" project (one
project for each filter), and move the results to
/boot/home/config/add-ons/Magnify
.
Name the first filter "Rotate" and the
second "Wave". Next time you launch Magnify, you'll find the new menu
items at the bottom of the pop-up list. Select a filter and watch the
image.
What's next? How about combining filters and adding a game to Magnify.
Contrary to its name, BeOS message scripting doesn't refer to a scripting language such a Perl or Awk. BeOS scripting is a format for passing messages between applications. Your programs already support scripting as many classes in the Interface Kit have it built in. The really interesting aspect of scripting is that applications can inter-operate without necessarily having to be specially designed to work together. The sample app, Thesaurus demonstrates this principle. You can download it from here:
<ftp://ftp.be.com/pub/samples/application_kit/Thesaurus.zip>
Thesaurus lets you scan through text that's displayed by some other application, and replace words with their synonyms. Ok, it's not totally useful (unless you're writing a college paper maybe!), but it is mildly entertaining, and the code could easily be adapted to work as a spelling checker or similar application. The synonyms are taken from a file that's included in the optional/goodies folder on the R4 CD.
The basic operation of this application is relatively simple. BTextView
has a property named "Text" which gives you access to the contents of the
text view. This application reads some text, scans for a word that it has
synonyms for, presents the list of choices to the user, and then sets the
text depending on what the user chooses. Unfortunately, the text view
scripting messages used by Thesaurus don't yet belong to a suite, so if
you try this with a program such as Eddie or
BeIDE, you may find that it
doesn't work.
One problem I ran into was how to find out which application Thesaurus
should talk to. An easy way to do it was to allow the user to drag an
icon onto the app. The message that is dragged is just bogus. When the
view in question receives it, it won't know how to handle it and will
reply with B_MESSAGE_NOT_UNDERSTOOD
. The returned message, however, will
have a messenger to the view in question which we can use to talk to the
app. This worked fine for BeMail,
but unfortunately StyledEdit doesn't
accept dropped messages. To solve this problem, I've added optional
command line apps that can be used to manually select a target. In the
case of StyledEdit, you can use the optional command line arguments as
follows:
Thesaurus --app StyledEdit --window 0 --view text
The meat and potatoes of Thesaurus is in the
RemoteTextScanner
class. It handles communication
with the targeted text view. The actual work for getting the text is done
in FetchNextTextChunk()
, which fetches 1k of text
from the targeted text view. BTextView
has a
property called "Text" which represents text that it contains.
You can use an index specifier to choose which text you get back. The
following code retrieves a chunk of text that starts at
fTextBufferOffset
and is
TEXT_BUFFER_SIZE
long:
BMessage
textRequestMessage
(B_GET_PROPERTY
);textRequestMessage
.AddSpecifier
("Text",fTextBufferOffset
,TEXT_BUFFER_SIZE
);fTextViewMessenger
.SendMessage
(&textRequestMessage, &reply
); if (reply
.FindString
("result", &text
) ==B_OK
)strncpy
(fTextBuffer
,text
,TEXT_BUFFER_SIZE
);
That's it. Replacing the targeted word is almost as easy. In this case,
we change the verb to B_SET_PROPERTY
. It is then a two step process of
removing the old text and inserting the new:
// Erase the word that is going to be replacedBMessage
reply
;BMessage
textDelMessage
(B_SET_PROPERTY
);textDelMessage
.AddSpecifier
("Text",fCurrentWordOffset
,fCurrentWord
.Length
());fTextViewMessenger
.SendMessage
(&textDelMessage
, &reply
); // Now insert the new word beginning at the same locationBMessage
textSetMessage
(B_SET_PROPERTY
);textSetMessage
.AddString
("data",newWord
);textSetMessage
.AddSpecifier
("Text",fCurrentWordOffset
,strlen
(newWord
));fTextViewMessenger
.SendMessage
(&textSetMessage
, &reply
);
The BTextView
also has a "selection" property, which, as you might guess,
allows us to change what text is selected. My plan was to set the
selection to the currently selected word, so the user could see it in its
context. This is where I ran into a little snag. It turns out that when
the window containing a text view doesn't have the focus, the selection
isn't highlighted. Since the thesaurus window has the focus when you're
changing words, this won't work.
Luckily, the text view also has a property called "text_run_array". This property contains raw information about the font and color of text in the view, so it was a simple matter of setting the color of the word to red to indicate which word is currently being scanned. Note that this only works when multiple colors/fonts are enabled for the view, however, in most cases where you would want to use this app, they are.
There's quite a bit of room for improvement in this application: You
could modify it to work within a selection by getting the selection
property from the text view before starting, or improve the UTF8 support
(BTextView
does utilize UTF8, so fixing this should be simple).
I hope this gets your creative juices flowing in thinking of ways to make applications more scriptable. The scripting mechanism is still evolving, so if you have ideas for new properties or suites, you can submit requests using the bug database.
Sometimes when you're writing an application that reads data files, it makes a difference what type of data the file contains. The BeOS uses MIME types to describe the type of data contained in files. For example, ASCII text files are type text/plain, and HTML files are text/html.
Sometimes applications try to read these files but the type information isn't correct. The most common case of this occurs when a file has been archived using the "tar" utility, then unarchived. This utility doesn't preserve attributes such as the file's type, so the file is untyped.
If an application is trying to read the file and interpret its contents, this can complicate matters. For example, a web server application (such as, say, the PoorMan web server that comes with the BeOS) needs to be able to tell the web browser receiving a file what type of file it is.
Here's some code that looks up the MIME type of a file whose name is given by filename:
BFile
file
; chartype
[256]; status_tstatus
;status
=file
.SetTo
(filename
,B_READ_ONLY
); if (status
==B_OK
) {BNodeInfo
ninfo
(&file
); if (ninfo
.GetType
(type
)printf
("Oh no! Can't get the type!\n"); } else {printf
("Type is %s\n",type
); } }
This code just uses the BNodeInfo::GetType()
function to get the MIME
type of the file, and fails if the function returns an error (which
typically indicates that the file doesn't have type information
established).
You could write code to look at the file's extension and/or contents and figure out what type of data it contains, but this isn't an optimal solution because your application shouldn't have to be aware of every possible type of data the file might contain.
The BeOS provides a function, update_mime_info()
, that lets you ask the
BeOS to attempt to identify the type of data in a file and update the
file's type information.
Here's an altered version of the example code that uses
update_mime_info()
to try to have BeOS identify the file's type:
BFile
file
; chartype
[256]; status_tstatus
;type
[0] = '\0';status
=file
.SetTo
(filename
,B_READ_ONLY
); if (status
==B_OK
) {BNodeInfo
ninfo
(&file
); if (ninfo
.GetType
(type
) <B_OK
) {update_mime_info
(filename
, 0, 1, 0); if (ninfo
.GetType
(type
) <B_OK
) {printf
("Can't get the type!\n"); } } if (strlen
(type
)) {printf
("Type is %s\n",type
); } }
This version, if GetType()
fails, calls
update_mime_info()
to ask the
BeOS to attempt to identify the file. Once that's been done, GetType()
is
called again to see if the type has been fixed. If not, an error message
is displayed.
Let's look at update_mime_info()
in more depth. Here's the prototype:
intupdate_mime_info
(const char *path
, intrecursive
, intsynchronous
, intforce
);
The path
argument indicates the pathname of the file to update.
The recursive
argument indicates that if pathname indicates a directory,
whether or not the entire directory tree at that point should be
traversed, updating every file in the tree. If this value is 1, the tree
is traversed recursively; if it's 0, only the indicated file is updated.
To ask update_mime_info()
to operate synchronously (so it will only
return when it's finished updating), specify 1 for the synchronous
parameter. Specify 0 if you want update_mime_info()
to perform in the
background; in this case, it returns immediately.
If the force
argument is 1, files are updated even if they already have
type information specified. If it's 0, files with types assigned already
are skipped.
update_mime_info()
returns a B_OK
if all is well, otherwise it returns a
negative value indicating the error that occurred.
If you haven't guessed already, this one function does at least 50% of
the work of the mimeset utility. Most of the other 50% of the work is
done by create_app_meta_mime()
, which updates
application meta-MIME
information (it's called if the -apps or -all flag is specified to
mimeset).
This may be helpful if you run into situations in which your application is trying to read files and is being confused by nonexistent type information.
On April 9 and 10, we'll hold our next Be Developer Conference in Palo Alto. Last year, the event spotlighted the first release of the BeOS for Intel-based PCs. This year we're on to our third Intel-based release (or fourth or fifth, depending on how you count 3.1, 3.2, 4.0 and the upcoming 4.1) and, indeed, we'll be happy to chat about the progress we've made in our UI, the kernel, the file system, etc. But at the April conference we're going to focus on the Media Kit, one of Release 4's most notable additions to the BeOS.
Given our Media OS banner, and our plans for a media-rich future for the Be commonwealth, this topic is clearly rich enough for several Developer Conferences. Using the Media Kit, and using it well, is essential to apps that want to be fast and flexible.
So, does this mean Be is only interested in Media Kit-based applications? I'll just summarize earlier statements made in this space: Media applications are the ones that are most likely to help distinguish the BeOS from its venerable elders. But drawn by media, BeOS users need to be delighted enough with our "everyday" apps—e-mail, Web browsing, word processing, spreadsheet—that they'll find no reason to leave.
Prior to the April BeDC we'll promote the BeOS Media Kit at events such as the NAMM Music Market in Los Angeles, January 28-31, the Frankfurt Musikmesse, March 3-7, and the CeBIT show in Hannover, March 18-24. Tim Self and his team expect a number of announcements for these events; these announcements will provide good technical (and business) cases, which we'll use to encourage more developers to embrace our technology.
In June we're off to PC Expo. Last year was the first time Be took part in a PC-centric trade show. This year we'll return with a proof of the concept that we demonstrated in 1998. This represents a lot of work -- but this is fun work, and it's paying off. The Release 4 sales results so far are a gentle but firm tug that indicates traction: Distributors are paying their bills and ordering more. This motivates all of us for the next milestones and we all look forward to a fun and productive BeDC.