Issue 4-35, September 1, 1999

Business & Marketing: The New Be Website, Part 1: Version 1.0

By Michael Alderete

If you've been a member of the Be community for long, you know that we recently redesigned the Be corporate web site. Many people, especially our developers, gave us feedback on what they liked and disliked, and lots of suggestions. To give you some insights into what we did and why, and what we'll do in the future, I thought I'd write about the process we went through, and what happened afterwards.

I took the job of Webmaster this year at the end of March. My second day on the job I was told "We need a new web site. You have 4 weeks." After the paramedics restarted my heart, we got to work.

After some consultation with outside design firms, it became clear that 4 weeks weren't enough to do the job well and at a reasonable cost. We extended our deadline, asked our branding and design firm to handle this project as well, and got to the hard part of the task: creating a new organizational scheme for the site.

The number one complaint about the old design was that it was too hard to figure out what Be did. The Be web site was focused on the existing Be community, and served it well, but tended to turn off people who were visiting for the first time. The site didn't convert casual browsers into customers, because they would quit browsing before they learned what our product was!

Since one of the groups of people we wanted to please with the new site was first time visitors, including potential customers and investors, we knew we had to make significant changes. (This was one of only many considerations we had on our list.)

To reorganize our site, we worked on two different approaches with our design agency, Fitch. One, which we called "The Matrix," was very sophisticated but was also so complicated we couldn't explain it to other people at Be. We took the simpler of the two approaches.

The visual design took less time. Fitch presented us with three different visual looks. We chose one, and then looked at multiple variations on that theme. We picked one of those, made a couple of minor changes, and Fitch's designers turned it over to their web production team.

Fitch's web production group was going to build out a set of base templates: one skeleton page for each section, and all the associated graphics. Then Be's web team (all two of us) would take those, and build out the site to the 20 pages we were planning to complete for launch.

The first iteration of the templates we got used HTML frames for the top navigation area. Somehow we had forgotten to say "don't use frames." (Though we were very clear that the new design could not depend on anything that wasn't supported by NetPositive. And it doesn't, though the JavaScript rollovers are kinda cool if you visit using another browser.)

For those of you who don't already know my attitude towards HTML frames, let me be plain: Frames are Evil. They are the work of the Prince of Darkness. I *hate* frames. I can go into detail as to why, but I'll just link to Jakob Nielsen's two excellent articles on the issue:

So we kicked the base templates back, and asked Fitch to redo them without frames. This put us a bit over time and over budget, but getting rid of the frames was worth it.

Other flaws were not so easy to fix. At the last moment, we realized that the new site design would unveil the new Be logo three weeks ahead of our planned "debut" at PCExpo. So we quickly hacked out a replacement graphic using the old logo. It looked terrible, but there was no other way to keep the new logo secret.

The awful logo graphic was one of about a dozen visual flaws we could see in the new design. Some others were that the gradient background and the small text in the blue side navigation bar combined to make it very hard to read; the grayed-out "sub-navigation" links looked disabled and unavailable; JPEG artifacts in the Be logo (even the new one) were especially visible in NetPositive; and somewhat inappropriate icons were used in the blue side navigation bar. The Zookeeper icon for the Jobs section was especially unfortunate; rumors that Be chains people to their desks, or flogs them when they get behind schedule, are completely untrue. We haven't done that in almost two years.

We and Fitch knew about these flaws. But with the aggressive development schedule and fairly fixed launch date, we just didn't have the luxury of going back to fix things. At all four stages of the project (architecture, visual design, HTML design, production) we came to a point where we said "it's flawed but the deadline is really important; we'll fix it in version 1.1" (a phrase I'm sure every software developer has heard at one time or another!). We moved on to production.

With the templates and graphics in hand, the Web Team got to work building out the site. We implemented the actual site templates in a mix of HTML and the outstanding PHP (, a server-side processing language (someone please port this to BeOS!). These templates are "smart," in that they know what page they're displaying and adjust elements accordingly. All the work of keeping the navigational links, highlighted graphics, and JavaScript code correct is done by those templates, making it *vastly* easier to add new pages to sections, etc.

With production basically finished (we were just waiting for a few pieces of content to trickle in), we examined our handiwork. It wasn't perfect by a long shot, but it didn't suck completely either. At some point, you just have to STFP ("ship the product"—the "f" is silent). So we did.

Uh, well, we wanted to. But at the last moment, we realized that the new server hardware, a much beefier—and physically larger—system than its predecessor, would not fit in the cage at our co-location facility. We'd have to wait three days until they could rewire a nearby rack to receive our box. "Three days" turned out to be a week...

In the end we launched the new site on June 2nd, a week and a day late. Not bad, all things considered—our schedule had not allowed us to add "padding" for delays at all; we'd barely had time to do QA.

The various delays had an upside, though. They and the smart templates allowed the Web Team to expand the number of pages on the site when it launched. Instead of the original target of 20 pages on the new site, we ended up with 42 (we're well over 100 now).

Next week, I'll return and talk about the feedback we've received, additional issues we've uncovered—and what we're going to do about it, very soon.

Be Engineering Insights: Programming at the Limit...

By Benoît Schillings

Before I dive into the subject of this week's newsletter article, I'd like to claim the title for working in the "hardest programming environment for programming the BeOS framework..."

One of my regular programming environments is my garden. I connect my faithful dual 300 Mhz PC running BeOS to my telescope, and under the curious gaze of Molly or Zippy (two wonderful dogs! See, I happily program under the stars.

Now this is a pleasant environment. But with the comfort of my home/DSL/refrigerator at hand, I can't really claim it as the hardest environment for programming BeOS. So last week I went one step farther -- I programmed the same machine (still connected to the telescope) near the summit of White Mountain in eastern California.

The altitude was about 11,000 feet, the wind speed about 30 mph, and the temperature was low enough that I had to add some rum to my Diet Coke to keep it from freezing. A Honda generator provided the power—the fuel-efficient BeOS burns about 0.8 gallon of supreme unleaded a day.

Amazingly, even under something like wilderness survival conditions, further attenuated by my highly diminished mental abilities, the Be programming framework is still simple enough that I could program with ease. That's the magic of BeOS!

Now, lets talk about something more "down to earth"...

A while back I wanted to add a little animated gizmo to 3dsound -- something I thought was very simple but which turned out to be a bit more difficult to implement than I expected. I'm talking about the little black triangle at the top left of the content window.

If you click on that triangle (or hit the tab key), the panel containing the vu-meters, etc., will show/hide. I wanted this triangle to rotate smoothly.

At first, I did a trivial implementation of just drawing a triangle at the different rotations, but I wasn't very happy with the result. The edges of the triangle were pretty huggly (that's "ugly" to you) at most angles!

The best solution would be anti-aliasing, but I wanted to try something different: using the framework to render the triangle, then blurring the result before blitting it to the screen.

In this case, blurring works like a simple low-pass filter. It removes the offending high frequency (jaggies). In general, when you do fast animation, adding blurring tends to make the result look much better.

The result looks really nice (I think it's as good as real anti-aliasing) and can be applied to many kinds of rendering.

Here is the code:


class   TCtrlView : public BView {
    BBitmap         *b;
    BView           *bv;
    float           rot;

                TCtrlView(BRect frame, char *name);
virtual void    Draw(BRect ur);
        BPoint  transform(BPoint in);
        void    SetRotation(float r);
        void    blur();


    TCtrlView::TCtrlView(BRect frame, char *name)
    : BView(frame, name, B_FOLLOW_NONE, B_WILL_DRAW)

    b = new BBitmap(BRect(0,0,31,31),

    b->AddChild(bv = new BView(BRect(0, 0, 31,
    rot = 3.1415926;



    delete b;


// 2d transform routine

BPoint  TCtrlView::transform(BPoint p)
    float   sina = sin(rot);
    float   cosa = cos(rot);
    float   x0,y0;

    p.x -= 9;
        //offset from the rotation center
    p.y -= 17;

    x0 = p.x * cosa - p.y * sina;
    y0 = p.x * sina + p.y * cosa;

    x0 += 9;
        //offset back
    y0 += 17;

    p.x = x0;
    p.y = y0;

    return p;

// very simple blurring routine, also pretty fast!

void    TCtrlView::blur()
    uchar   *c;
    long    x,y;
    uchar   tmp[32][32];
    long    acc;

    c = (uchar *)b->Bits();

    for (y = 1; y < 29;y++)
    for (x = 2; x < 20;x++) {
        acc = c[(x+y*32)];
        acc = acc + acc + acc + acc;

        acc += c[(x+y*32)+1];
        acc += c[(x+y*32)-1];
        acc += c[(x+y*32)+32];
        acc += c[(x+y*32)-32];
        acc /= 8;
        tmp[y][x] = acc;
    for (y = 1; y < 29;y++)
    for (x = 2; x < 20;x++) {
        c[x+y*32] = tmp[y][x];

// Set the rotation of the arrow and draw the arrow to the
// screen.

void    TCtrlView::SetRotation(float r)
    rot = r;



    DrawBitmap(b, BRect(1, 1, 1 + 14, 1 +

void    TCtrlView::Draw(BRect ur)
    BRect       r;
    long        i;
    rgb_color   c;

    DrawBitmap(b, BRect(1, 1, 15, 31),BRect(0,3,14,33));


And a final bonus...

In the 3dsound timeline, the selection is represented by a darker area. Here's the routine I use to darken an 8-bit bitmap. The idea is to build whatever lookup tables you want which will perform the color change you need, in this case darkening by 25%.

You could easily modify the "init dark table" for any other purpose you want, like darkening everything but bright red, or remapping an image to gray scale, etc. This is really simple but gives you a lot of flexibility in the graphic effects of your program.


char    dark_table_inited = 0;
uchar   dark_table[256];


void    init_dark_table()
    ushort      v;
    uchar       ov;
    rgb_color   c;
    BScreen     s;

    for (v = 0; v < 256; v++) {
        c = s.ColorForIndex(v); = (int)(*0.75); = (int)(*0.75); = (int)(*0.75);

        ov = s.IndexForColor(c);
        dark_table[v] = ov;
    dark_table_inited = 1;


void    TrackViewer::darken_rect(BRect r)
    long    rowbyte = TRACK_B_H;
    char    *base;
    long    y1,y2;
    long    x1,x2;
    long    dx,dy;

    if (dark_table_inited == 0) {

    y1 = (int);
    y2 = (int)r.bottom;
    if (r.right < r.left)

    if (y1 >= TRACK_HEIGHT)

    if (y2 < 0)

    if (y1 < 0) {
        y1 = 0;

    if (y2 > (TRACK_HEIGHT - 1)) {
        y2 = TRACK_HEIGHT - 1;

    dy = (y2-y1);

    x1 = (int)(r.left);
    x2 = (int)(r.right);

    if (x1 > (TRACK_WIDTH))
    if (x2 < 0)

    if (x1 < 0)
        x1 = 0;
    if (x2 > (TRACK_WIDTH))
        x2 = TRACK_WIDTH;

    if ( >= 0) {
        while(y1 <= y2) {
            dx = x2 - x1;
            base = (char *)the_off->Bits() + y1*rowbyte+x1;

            while(dx>=0) {
                *base = dark_table[*base];



So, my final question. Would anybody like to challenge my claim of finding the "most hostile environment for programming BeOS?"

Developers' Workshop: Simple Single-Threaded Message Handling

By Owen Smith

One of the most important issues that comes up in porting apps from other OSes is adjusting to BeOS's pervasively threaded world. In today's article, I'll show you an easy way to make apps that you're porting happy, as well as demonstrate a sometimes-overlooked class that you can make good use of in your BeOS-native programs as well.

The Problem

If you're familiar with Windows, Macintosh, and UNIX apps, you know that they usually run in one thread. This main thread is responsible for all messages involving the application, its windows, their views, and any other message targets there might be in the application.

For example, 99.99% of a Windows application's time in the main thread is spent inside the main message loop, which looks something like this:

while (::PeekMessage(&msg, 0, 0, 0, PM_REMOVE)) {
    if (/* is a quit msg */) {
    } else {

Most applications that you port assume this one-thread model of handling data. Because in this model only one thread is handling a message at a time, there's no need for message handling routines to access their data in a thread-safe manner (i.e., using synchronization and locking mechanisms).

Now this is actually quite similar to the BeOS way of doing things -- after all, how many ways can you write a message handling loop?—though the actual processing loop is run automatically for you by BWindow, BApplication, and other BLooper-derived denizens. The big difference in the BeOS, however, is that each BLooper runs in its own thread. So in the BeOS, there are several threads running message loops: one for your application, one for each window, and one for each other BLooper-derived object you might have created for your own nefarious purposes. If you scatter the message- handling routines of your ported app among these threads, disaster is almost certain to result, as suddenly the message handling functions will be stepping on each other's toes left and right.

One way to fix this is to implement synchronization and locking mechanisms throughout the app, making sure that each message- handling routine carefully locks the data it will be handling, and otherwise deals with the nuances of being thrust into a tangled web of threads. Managing this, of course, will no doubt earn you the astonishment and admiration of your fellow BeOS porters, as well as sincere thanks from the original authors for transforming their weekend hack into a bullet-proof, thread-safe, and otherwise divinely inspired piece of code. But if that application you're trying to port is over, say, 100 lines in size, this solution quickly becomes intractable—especially when you take one of the Great Mysteries of Porting into account: that is, the code you're trying to port is never quite as clean or understandable as you'd like -- or need.

The second way is to serialize all message handling in your app through one thread, to present apps that you're porting with the threading model that they expect. Curiously enough, this seems to be the method of choice around here.

The Solution

To do the latter, we need to take the messages we receive and put them into a single queue for the message-handling thread to read and dispatch. Enter the appropriately named target of today's discussion, the BMessageQueue. The BMessageQueue implements a simple, thread-safe container of BMessages (the Be Book has the lowdown if you're curious). Let's look at some of the ways we might implement our solution with BMessageQueue.

At first blush, it seems that all we need to do is add the messages we receive to the queue:

BMessageQueue theQueue;

MyWindow::MessageReceived(BMessage* msg) {

If you read my newsletter articles from a few weeks back "Developers' Workshop: The Magic of Messages Part 1: The Sending" and "Developers' Workshop: The Magic of Messages Part 2: The Receiving", you'll hopefully realize that something is wrong with this code. The problem is that msg gets deleted when we return from MessageReceived(), so that by the time our message-handling thread reads the item from the queue, the message pointer will be invalid. The correct way to do this is to detach the message from the BLooper first so the BLooper doesn't delete them behind our backs when we return from MessageReceived():

MyWindow::MessageReceived(BMessage* msg) {

When we do this, remember the Solemn Responsibilities that you shoulder when you detach the message:

  • Thou shalt handle the message soon. (The message sender might be waiting for you to finish with this message, so it's good etiquette to ensure that the message sender doesn't wait longer than necessary for you to complete your task.)

  • Thou shalt delete the message when thou art done with the message. (Otherwise, the message sender might wait in limbo for eternity for a reply to come.)

Now we need to implement the message-processing thread to handle the messages. It will look something like this:

status_t RunMyMessageThread(void* /* obj */) {
    while (true) {
        // Peek until we see a message and remove it
        while (theQueue.IsEmpty()) snooze(1000);
        BMessage* msg = theQueue.NextMessage();

        if (/* is a quit message */) {
            delete msg; // almost forgot, didn't you?
        } else {
            // translate and dispatch the message
            delete msg;
    return B_OK;

Alternatively, to avoid actively polling the queue in our peek message, we can use a semaphore to signal the message thread when there are messages in the queue. In this case, our thread will only wake up when there are messages to handle:

// semaphore is created with an initial count of 0
// the count indicates the number of messages in the queue
sem_id queueHasMessages;
BMessageQueue theQueue;

MyWindow::MessageReceived(BMessage* msg) {
    release_sem(queueHasMessages); // signals thread to
                                   // wake up

status_t RunMyMessageThread(void* /* obj */) {
    while (true) {
        // Peek until we see a message and remove it
        BMessage* msg = theQueue.NextMessage();
        if (/* is a quit message */) {
            delete msg;
        } else {
            // translate and dispatch the message
            delete msg;
    return B_OK;

For giggles, you can extend this approach to have multiple threads reading from the message queue—for example, if you want to farm out a single list of tasks among a number of worker threads.

I hope these simple snippets will give you even more ideas about how you can distribute message handling responsibilities in your application in a flexible and simple way.

Bit By Bit: BFilePanels and Saving Files

By Stephen Beaulieu

In this installment, Rephrase becomes a usable basic plain text editor with the introduction of file open and save capabilities.

You'll find this version of Rephrase at:


Rephrase 0.1d4
New Features
Create new phrases.
Open existing phrases.
Save phrases.

This version of Rephrase introduces too many programming concepts to cover in one article. Therefore, next week's article will not introduce any new sample code.

Programming Concepts

BFilePanels provide windows that let a user navigate a file system to open one or more files or save a file at a specific location. A BFilePanel can either open or save files, but this flavor must be set when it is created.

All file panels present the user with a navigable view of the file system. The panel's default button sends a message to a specified target that contains identifiers for the file or files to open or save. By default, the target is the BApplication object, but a different object can be set through the constructor or SetMessage(). The messages sent vary depending on the flavor of the panel.

Open panels send a message containing all the entry refs selected by the user. Save panels send a message with the entry_ref of the directory selected by the user and the name of the file to create.

Open panel messages have a default what code of B_REFS_RECEIVED. Save panels have a default code of B_SAVE_REQUESTED. An application can specify a different message for the panel to send through the constructor or SetMessage(). The file panel information is added to this new message.

Both save and open file panels also send B_CANCEL messages whenever the panel is closed, has its cancel button pressed, or is hidden. This means that when a user selects the open or save button, two messages are sent: first, the save or open message, and then the B_CANCEL message to inform the target that the panel is no longer visible.

Implementation details:

  1. Don't build a file panel until it is requested. This saves resources and time. [pDisplay.cpp:361-367 & Rephrase.cpp:125-129]

  2. Do not delete commonly used BFilePanels, as they can take time to create. Instead, hide them and show them again when needed. In addition, the file panel will remember its last browsed directory.

  3. There is only one open file panel for Rephrase. Opening new display windows is handled in the app. [Rephrase.h:24]

  4. Each phrase display window has its own save panel, as each phrase might need to be saved to a different directory. Also, synchronizing access to one save panel from many documents would be very complicated. With independent save panels, each panel can target a different phrase display window. [pDisplay.h:29]

  5. With the introduction of custom message types for opening and saving phrases, it's now necessary to override MessageReceived() in the Rephrase and pDisplay classes.

    There are three main guidelines when overriding MessageReceived():

    • Use a switch on the what field of the BMessage to specify the flow of code.

    • Either call functions or wrap each case in brackets to avoid warnings and errors about "jumping past initializers."

    • Always send unrecognized messages to your superclass in the default case of the switch statement. [pDisplay.cpp:164-202 & Rephrase.cpp:111-151]

  6. The New menu item sends a NEW_PHRASE message to the app, which then creates and shows an empty display window. [Rephrase.cpp:116-121]

  7. The Open menu item sends an OPEN_PHRASE message to the app. In response the app will show the open panel, creating it if necessary. The file panel sends a message that is handled in RefsReceived(). [Rephrase.cpp:122-136]

  8. To save a phrase to a file, the file's location must be specified. This happens in one of two ways: either the phrase display window is opened with a given entry_ref or the entry_ref is specified in a save panel.

    The save panel is shown if an entry ref has never been set for the file, or if the SaveAs item is selected to save the contents to a new location.

  9. pDisplay::SaveTo() handles all the work. Its sole argument is a BEntry*. If the argument is NULL, the save panel is shown so the user can specify a location for the file.

    Otherwise, a BFile object is created and told to create the actual file if it does not already exist. Then the contents of the phrase are written to the file, and the size of the file is set accordingly. A BNodeInfo object is created so that new files can get their mime type set appropriately.

    Finally the window title is set to the name of the file and the entry ref is cached in the window for future use. [pDisplay.cpp:352-433]

  10. The Save menu item sends a SAVE_PHRASE message to the phrase window. In response, pDisplay::Save() checks to see if the cached entry_ref has been set. If so, it creates a BEntry object and passes it to SaveTo(). If not, it passes NULL so that the save panel can be shown. [pDisplay.cpp:334-350]

  11. The SaveAs menu item sends a SAVE_PHRASE_AS message to the window. In response, pDisplay::SaveTo(NULL) is called, bringing up the save panel to specify the new file location. [pDisplay.cpp:174-178]

  12. pDisplay no longer calls Show() in the constructor. As BFilePanels need to have Show() called on them, it made sense to have consistency with all window-related classes.

  13. pDisplay::TargetMenuItems() specifies appropriate targets for the menu items. The New, Open, About, and Quit items target the application, while the items in the Edit menu target the text view. [pDisplay.cpp:277-303]

  14. BTextView::TextRect() returns a special rectangle. The sides and top of the rectangle are set from a rectangle passed in the constructor or SetTextRect(). The bottom value is set from the height of the text in the text view. Accordingly, the text rect could be much taller or shorter than the rect specified in SetTextRect().

  15. The BString utility class is used as a replacement for char*s throughout much of Rephrase. The class provides type-safe and easy sprintf() functionality through the operator<< and operator+=. [pDisplay.cpp:394-398]

There is considerably more going on in this week's sample code. This extra code deals with properly quitting the application, and will be covered in detail next week.

Chips Questions

By Jean-Louis Gassée

Now that IBM has announced the availability of a PPC motherboard, why doesn't Be jump on the bandwagon and commit to this hardware? My apologies for not doing full justice to the number, variations, or eloquence of the messages sent to or directly to me. You get the idea, though. Something is happening in the PPC world and Be ought to support it. One correspondent expressed it as a brutal Wall Street reduction: "I would even be willing to bet that this would cause the most dramatic rise in Be stock since the IPO."

The stock price is not a topic for me to comment on, but the sentiment regarding the PowerPC needs to be addressed (immediately, which is why I've postponed the next installment of the IPO story, "Going Public: Part II"). The processor itself is a fine one, as the benchmarks prove. We've had versions of BeOS on the PowerPC since 1994, including our current 4.5 release (whose package clearly states "For x86 and PowerPC"). So, what we're dealing with is not really a processor dilemma. Is it then a question not of chips but of chipsets? Setting aside a possible oversimplification for the moment, this turns out to be a better lead to the real issues.

Look at the evolution of PC motherboards. More and more functions get integrated, as opposed to implemented on plug-in cards. In the process, the "chipset," the chip or chips interposed between the faster and faster CPU and slower devices such as the PCI bus, memory, frame buffers and the like, become richer and richer mediators. The complexity of the better chipsets now rivals or exceeds that of the CPU itself.

The operating system, in turn, needs to support these mediating devices; otherwise, the hardware, or the OS, or both aren't going anywhere. Take Silicon Graphics' recent decision to de-emphasize their low-end workstations based on Intel processors and Windows NT. These workstations required two proprietary objects, a physical one and a logical one. On the physical side, Silicon Graphics decided to use ASICs to express their knowledge of the application space. Together, these Application Specific Integrated Circuits constituted a "chipset" providing high-performance I/O for graphics, video, and audio applications in SGI's heritage. The idea was to achieve a level of performance higher than that offered by the standard PC clone organ bank. This, in turn, required a logical object, NT software, drivers and, perhaps, modifications to NT itself. Meanwhile, the relentless clone industry continued to drive the price of standard organs down and the performance up. The clone drive reached a point where the SGI solution became less likely to achieve its original goals.

If that example isn't enough, recall recent announcements regarding high-end chipsets for servers. Two camps were ready to make war over a standard, one led by Intel and, if memory serves, the other by Compaq. But the cost of the conflict and the benefits of having only one standard led to peace. We can now expect organ bank support for eight-way multiprocessing, coming soon from your favorite motherboard supplier.

To return to PowerPC hardware, we need to know more about chipsets that support the PowerPC. Who builds them, how competitive are they, which I/O devices are supported, how is the technical documentation accessed, who fixes bugs in the product and the documentation? As far as the IBM PPC hardware is concerned, other questions arise. Where can I buy it and where can I get it fixed, for instance? As answers emerge, it will be easy for us to make a decision.

That said, I still haven't answered the Apple question. If Apple is selling respectable quantities of PowerPC hardware, how come we don't support the latest G3 and G4 machines? As I said above, it boils down to a chipset issue. Intel doesn't have an operating system to defend. In their best interest, they profess to be "OS-agnostic," the more options, the better. In Apple's case, it's different. They own and operate an OS and, like Microsoft, see no reason to help a competitor. Linux provides access to classical Unix applications and, therefore, is little competition for Apple's multimedia heritage. The same can't be said of BeOS, and I can see the logic in Apple's decision not to help us with access to chipset technical data for a G3/G4 BeOS port.

Some have suggested that we look into the Linux sources for such data. Perhaps, but I see little reason to open ourselves to possible accusations of reverse-engineering. We're welcome on x-86 hardware, we're not welcome on Apple G3/G4. We respect the logic and that settles it for us.

Creative Commons License
Legal Notice
This work is licensed under a Creative Commons Attribution-Non commercial-No Derivative Works 3.0 License.