In HyperCard the difference between the index card and the entry cards is expressed as two different backgrounds. In REALbasic, there is no card/background metaphor, so we need two different windows. Let's call these "Index" and "Entry". To be more HyperCard-like, we can make these behave like a single window by having them appear only one at a time in the same position on the screen. So Index has a method
if not self.visible then self.top = entry.top self.left = entry.left self.show entry.hide end
Entry has a method
doshow as well, which is parallel. REALbasic doesn't easily support HyperCard-like visual effects during the transition between cards, so we won't have any.
HyperCard is natively a text database, where storage and retrieval of data is handled automatically and transparently; thus the entries are just different cards of the second background, where showing a card means showing its data automatically. To navigate from entry 573 to entry 574, for example, we simply "go next card."
In REALbasic none of that is true! There are no cards -- to show different entries we must replace the data displayed in the Entry window with other data. Thus, we must implement storage of the data in such a way that for any entry we can retrieve the data on demand. And when we retrieve new data, we must manually repopulate the Entry window.
To solve this problem, let's take advantage of the fact that REALbasic provides a native "database" file format, along with methods to access the data. This isn't as fast or compact as HyperCard's built-in storage, and of course it's nowhere near as automatic, but it will prove adequate to our purposes. So, unlike HyperCard (where the code and data together make up a stack), in REALbasic the data will live in a separate file and our application will just be a way of viewing it.
Constructing the REALbasic application
The first step is to build the database file, which we'll call
rogetdb. I start by exporting the entry text from the HyperCard stack as a series of styled text files, because the entries have styling I want to preserve. The files are named "roget1", "roget2", and so forth. Because the cards are exported in order, the numeric part of these names can be used as each entry's index value.
In the REALbasic IDE, I choose "New REAL Database" to create the database file and double-click the database entry in the Project Window to edit its schema. This consists of a single table called "data". REALbasic's database file format has no provision for storing styled text, so I need to hold the text information and the style information in two different fields, which I call
styl. I also add a numeric field
num to hold the index value. I write and run a routine to open each styled text file in turn, capturing its text and style information, along with the index value from the file's name, and adding a record to the database containing this information.
Next, I export the HyperCard stack's
part headings -- along with the information from the top line of the entry -- as a tab-delimited and return-delimited text file. In REALbasic, I add
numword fields to the database file's schema, and run code to import the data by parsing the text file. I'm not showing you any of that code, because it isn't part of the final project. Once the database file is built, the code that built it can be thrown away! Besides, all this isn't really so different from how you'd initially construct a HyperCard stack.
Now we are ready to create our actual REALbasic application. The first thing it must do is link up with its data. We assume that
rogetdb is in the same folder as our application. Since this linkage belongs to the application as a whole, we create an application subclass and give it a database property called
d. In the application subclass's
open event, which is the first code executed as the application starts up, we put this code:
self.d = openrealdatabase(getfolderitem("").child("rogetdb"))
Now let's consider how to navigate among entries. Only the Entry window needs to do this, so we give it a database cursor property called
c and a method
dodisplay. The idea of this architecture is that any routine can set
c to point to the desired record, and call
dodisplay to show it.
c will be performed using a SQL
select command to obtain a virtual table of records, and possibly the
movenext command to cycle forward through the virtual table, until the
c property's current record is the right one. Here is the
txt.text = "" txt.settextandstyle c.field("txt").stringvalue, c.field("styl").stringvalue txt.sellength = 0 txt.selstart = 0 class.text = c.field("class").stringvalue section.text = c.field("section").stringvalue part.text = c.field("part").stringvalue numword.text = c.field("numword").stringvalue idx = c.field("num").integervalue doshow
numword are the StaticText controls displaying the headings at the top of the window;
txt is the EditField displaying the text of the entry.
idx is an integer property of the Entry window. Its purpose is to make it easy to move to the next or previous record. For example, here is the code for the method
gonext which is called by the
action event handler of the Next button:
c = app.d.sqlselect( "select * from data where num=" + str(idx+1)) if c.eof then beep return end dodisplay
That's our equivalent of HyperCard's "go next." It's a good illustration of how
dodisplay are intended to be used. The code for
goprev is completely parallel.
When the user clicks a heading, the display should cycle forward to the first entry that has a different heading in that category. In HyperCard, the way I did this for, say, the
class heading was to give the
class field this
get me lock screen repeat while it is bg fld "class" set cursor to busy go next cd of this bg end repeat
The REALbasic approach is almost exactly the same. All three headings work the same way, so we move the common code out into a method called
nextdifferent, which takes two string parameters -- the current value displayed by the particular static text,
s, and the name of the database field to look in,
fld. Here is the
c = app.d.sqlselect( "select * from data where num > " + str(idx)) while (not c.eof) and c.field(fld).stringvalue = s c.movenext wend dodisplay
To illustrate, here is the
mousedown event handler:
nextdifferent me.text, "class"
Now let's talk about what happens when the user clicks in the text of an entry. Recall that if the user clicks in a number we want to navigate to that card; otherwise we want to find the word clicked in. In HyperCard, here's the code I was using in the
put the clicktext into what if what is a number then find what in bg fld "numword" else put "find word" && quote & the clicktext & quote && "in bg fld " & quote & "txt" & quote returnkey returnkey end if
This relies on two things we get for free in HyperCard: the
clicktext property, and the ability to "find." But we don't get either of those in REALbasic! Thus we must implement them ourselves. So the first thing that the
mouseup event handler must do is to work out the "click text." To do this, I start with the point of the selection and expand in both directions until I hit a character that isn't a word character:
dim sel as integer dim first,last as integer dim s as string dim acc as string dim r as regex dim m as regexmatch acc = "abcdefghijklmnopqrstuvwxyz1234567890" sel = me.selstart s = me.text first = sel+1 last = first do until instr(acc, mid(s,first,1)) = 0 first = first - 1 loop do until instr(acc, mid(s,last,1)) = 0 last = last + 1 loop s = mid(s, first+1, last-first-1)
s contains the click text, and we need to search for it. If
s is a number, the REALbasic approach is very like HyperCard's:
if str(val(s)) = s then c = app.d.sqlselect( "select * from data where numword like '" + s + ".%'") dodisplay return true
In HyperCard, if
s is text, recall that we simply use "find word" to cycle through every entry looking for a word consisting of the text we specify. In REALbasic, though, we must implement this ourselves.
To handle the fact that the search is for a word, we'll take advantage of REALbasic's new regular-expression support. To handle the fact that the search is from card to card, we rely on a utility method,
canfind, which accepts a regular expression and a SQL command specifying a set of entries to cycle through, and calls
dodisplay if the search succeeds. We look first in the same entry we're already in; if we don't find the text, we look in the entries that follow it; if we still don't find the text, we look in the entries that precede it. Finally, if we've found the text, we select it.
else r = new regex r.searchpattern = "\b" + s + "\b" // whole word r.options.casesensitive = false m = r.search(me.text, last) if m = nil then // try subsequent m = canfind(r, "select * from data where num>" + str(idx)) if m = nil then // try previous m = canfind(r, "select * from data where num<" + str(idx)) if m = nil then beep // not found return false end end end // found it, mark it me.selstart = m.subexpressionstart(0) me.sellength = last-first-1 end
Here is the
canfind code (
s are the incoming regex and string parameters):
dim m as regexmatch c = app.d.sqlselect(s) while not c.eof m = r.search(c.field("txt").stringvalue,0) if m <> nil then dodisplay return m end c.movenext wend return nil