! ! Copyright (C) 2003-2004 Simon Baldwin (simon_baldwin@yahoo.com) ! ! This program is free software; you can redistribute it and/or ! modify it under the terms of the GNU General Public License ! as published by the Free Software Foundation; either version 2 ! of the License, or (at your option) any later version. ! ! This program is distributed in the hope that it will be useful, ! but WITHOUT ANY WARRANTY; without even the implied warranty of ! MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ! GNU General Public License for more details. ! ! You should have received a copy of the GNU General Public License ! along with this program; if not, write to the Free Software ! Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. ! ! ! glkebook.inf -- PalmOS pdb DOC format eBook reader. ! ! For a large selection of eBooks, many free of charge, visit MemoWare at ! www.memoware.com. ! ! Compile with "inform -~S '$$MAX_STATIC_DATA=40000' glkebook.inf". ! Include "infglk.h"; Constant Story "GlkeBook"; Constant Headline "^An eBook reader for PalmOS DOC format pdb files^"; Release 8; ! Default window dimensions. Constant DEFAULT_WIDTH 72; Constant DEFAULT_HEIGHT 40; ! The DOC format is not well specified, nor well adhered to. All the files ! I've seen have 4kb as their record length. They then tend to do one of ! two things for compression: either compress so the record, when decoded, ! is exactly 4kb, or compress so when decoded it's around 4kb, +/- a bit. ! It seems, then, that 4kb read buffer and 8kb decode buffer should be okay. Constant DECODE_BUFFER_SIZE 8192; Constant PDB_BUFFER_SIZE 4096; !----------------------------------------------------------------------------- ! Errors and error handlers. !----------------------------------------------------------------------------- ! Error number if all goes horribly wrong. Global errno = 0; ! Error number constants. Constant E_SUCCESS 0; Constant E_PDB_BAD_REF 1; Constant E_PDB_CANT_OPEN 2; Constant E_PDB_NOT_DOC 3; Constant E_PDB_NO_DATA 4; Constant E_PDB_TOO_BIG 5; Constant E_PDB_NO_REC0 6; Constant E_PDB_REC0_READ 7; Constant E_PDB_BAD_VER 8; Constant E_PDB_NOT_OPEN 9; Constant E_PDB_BAD_REC 10; Constant E_PDB_NO_END 11; Constant E_PDB_REC_READ 12; Constant E_DECODE_BAD_REC 20; Constant E_DECODE_BUF 21; Constant E_LOCATION_NO_EXP 30; Constant E_CRC_FAILED 40; Constant E_BOOKMARK_BAD_REF 50; Constant E_BOOKMARK_CANT_OPEN 51; Constant E_BOOKMARK_CANT_REOPEN 52; Constant E_BOOKMARK_HDR 53; Constant E_BOOKMARK_BAD_REC 54; Constant E_BOOKMARK_WRITE 55; Constant E_BOOK_NOT_OPEN 60; Constant E_BOOK_BAD_ACTION 61; Constant E_DISPLAY_BAD_ACTION 70; ! ! strerror() ! ! Return a printable string describing the error code. ! [ strerror err_num; switch (err_num) { E_SUCCESS: return "Success"; E_PDB_BAD_REF: return "No PDB file was selected"; E_PDB_CANT_OPEN:return "Unable to open the PDB file"; E_PDB_NOT_DOC: return "The PDB file does not contain a DOC header"; E_PDB_NO_DATA: return "The PDB file contains no data records"; E_PDB_TOO_BIG: return "The PDB file contains too many data records"; E_PDB_NO_REC0: return "Unable to locate PDB record zero"; E_PDB_REC0_READ:return "Unable to read PDB record zero"; E_PDB_BAD_VER: return "Unknown compression type in PDB record zero"; E_PDB_NOT_OPEN: return "No PDB file is currently open"; E_PDB_BAD_REC: return "Unable to locate PDB record"; E_PDB_NO_END: return "Unable to locate PDB record end"; E_PDB_REC_READ: return "Unable to read PDB record"; E_DECODE_BAD_REC: return "Cannot decode PDB record zero"; E_DECODE_BUF: return "Decoded PDB record exceeds available buffer"; E_LOCATION_NO_EXP: return "No decoded PDB record expansion for location"; E_CRC_FAILED: return "CRC table or logic error; self-test failed"; E_BOOKMARK_BAD_REF: return "Invalid bookmarks file reference"; E_BOOKMARK_CANT_OPEN: return "Unable to open the bookmarks file"; E_BOOKMARK_CANT_REOPEN: return "Unable to reopen the bookmarks file for write"; E_BOOKMARK_HDR: return "Bookmarks file does not contain a valid header"; E_BOOKMARK_BAD_REC: return "Invalid record found in bookmarks file"; E_BOOKMARK_WRITE: return "Failed to write or rewrite bookmarks file"; E_BOOK_NOT_OPEN:return "Displayable book not opened"; E_BOOK_BAD_ACTION: return "Bad displayable book action code"; E_DISPLAY_BAD_ACTION: return "Bad user interface action code"; default: return "[Undefined error]"; } ]; ! ! fatal() ! ! Handle fatal errors -- try to open a window at the bottom of the screen ! to report the message, print errno text, then quit. ! [ fatal win errwin; ! Search for a text buffer window. If we can find one, we'll ! split out an error window at the bottom of it. win = glk_window_iterate (GLK_NULL, GLK_NULL); while (win ~= GLK_NULL) { if (glk_window_get_type (win) == wintype_TextBuffer) break; win = glk_window_iterate (win, GLK_NULL); } ! If we found a window, split it; on error, use the main window. if (win ~= GLK_NULL) { errwin = glk_window_open (win, winmethod_Below|winmethod_Fixed, 1, wintype_TextBuffer, 0); if (errwin == GLK_NULL) errwin = win; } else errwin = win; ! Set this window, and if no split, print a separator. glk_set_window (errwin); if (errwin == win) print "^^"; ! Print the error message in alert style. glk_set_style (style_Alert); print (string) Story, ": Fatal error: ", (string) strerror (errno); glk_set_style (style_Normal); ! Tidy up and exit. if (errwin == win) print "^^"; quit; ]; !----------------------------------------------------------------------------- ! Pdb handling. !----------------------------------------------------------------------------- ! General pdb header lengths, strings, and limits. Constant PDB_HEADER_SIZE 78; Constant PDB_TITLE_SIZE 31; Constant PDB_RECORD_HEADER_SIZE 8; Array DOC_CREATOR -> 'R''E''A''d'; Array DOC_TYPE -> 'T''E''X''t'; Constant PDB_RECORD_0_SIZE 16; Constant PDB_RECORD_COUNT_MIN 2; Constant PDB_RECORD_COUNT_MAX 32767; ! Pdb file handling data. Global pdb_stream = GLK_NULL; Array pdb_header -> PDB_HEADER_SIZE; Array pdb_record_0 -> PDB_RECORD_0_SIZE; Global pdb_record_count = 0; Global pdb_index_count = 0; Global pdb_file_length = 0; Global pdb_is_compressed = false; Array pdb_title -> PDB_TITLE_SIZE; Global pdb_title_length = 0; Array pdb_buffer -> PDB_BUFFER_SIZE; Global pdb_length = 0; Global pdb_current = 0; ! ! pdb_close_ebook() ! ! Close any eBook pdb file currently open, and clear records. ! [ pdb_close_ebook; ! If open, close and note as closed. if (pdb_stream ~= GLK_NULL) { glk_stream_close (pdb_stream, GLK_NULL); pdb_stream = GLK_NULL; } ! Reset record and index count, compression, length and current. pdb_record_count = 0; pdb_index_count = 0; pdb_file_length = 0; pdb_is_compressed = false; pdb_title_length = 0; pdb_length = 0; pdb_current = 0; ]; ! ! pdb_get_int32_stream() ! ! Helper for reading pdb records and offsets. Reads a pdb double word from ! the pdb file stream. ! Array pdb_union --> 1; [ pdb_get_int32_stream stream; glk_get_buffer_stream (stream, pdb_union, WORDSIZE); return pdb_union-->0; ]; ! ! pdb_load_record_0() ! ! Read in pdb record zero from the given stream. Return true on success, ! false with errno on error. Use local record 0 copy. ! Array pdb_local_record_0 -> PDB_RECORD_0_SIZE; [ pdb_load_record_0 stream len offset; ! Get the offset of record 0. glk_stream_set_position (stream, PDB_HEADER_SIZE, seekmode_Start); if (glk_stream_get_position (stream) ~= PDB_HEADER_SIZE) { errno = E_PDB_NO_REC0; return false; } offset = pdb_get_int32_stream (stream); if (glk_stream_get_position (stream) ~= PDB_HEADER_SIZE + WORDSIZE) { errno = E_PDB_NO_REC0; return false; } ! Seek to record 0 and read it. glk_stream_set_position (stream, offset, seekmode_Start); if (glk_stream_get_position (stream) ~= offset) { errno = E_PDB_REC0_READ; return false; } len = glk_get_buffer_stream (stream, pdb_local_record_0, PDB_RECORD_0_SIZE); if (len ~= PDB_RECORD_0_SIZE) { errno = E_PDB_REC0_READ; return false; } return true; ]; ! ! pdb_open_ebook() ! ! Prompt for a file, and open as an eBook on pdb_stream. Set record counts, ! compression, title and title length, and file length. Return true on ! success, false with errno (and undisturbed pdb data) on error. ! Array pdb_local_header -> PDB_HEADER_SIZE; [ pdb_open_ebook fileref stream len compression count_hdr count_r0 i; ! Get a user-selected fileref. fileref = glk_fileref_create_by_prompt (fileusage_BinaryMode|fileusage_Data, filemode_Read, 0); if (fileref == GLK_NULL) { errno = E_PDB_BAD_REF; return false; } ! Try to open a stream on the fileref. stream = glk_stream_open_file (fileref, filemode_Read, 0); if (stream == GLK_NULL) { glk_fileref_destroy (fileref); errno = E_PDB_CANT_OPEN; return false; } glk_fileref_destroy (fileref); ! Read the pdb header into a local buffer. len = glk_get_buffer_stream (stream, pdb_local_header, PDB_HEADER_SIZE); if (len ~= PDB_HEADER_SIZE) { glk_stream_close (stream, GLK_NULL); errno = E_PDB_NOT_DOC; return false; } ! Check for expected type and creator. if (pdb_local_header->60 ~= DOC_TYPE->0 || pdb_local_header->61 ~= DOC_TYPE->1 || pdb_local_header->62 ~= DOC_TYPE->2 || pdb_local_header->63 ~= DOC_TYPE->3) { glk_stream_close (stream, GLK_NULL); errno = E_PDB_NOT_DOC; return false; } if (pdb_local_header->64 ~= DOC_CREATOR->0 || pdb_local_header->65 ~= DOC_CREATOR->1 || pdb_local_header->66 ~= DOC_CREATOR->2 || pdb_local_header->67 ~= DOC_CREATOR->3) { glk_stream_close (stream, GLK_NULL); errno = E_PDB_NOT_DOC; return false; } ! Extract and check the record count from the main header. There ! must be at least one data record, and fewer than 32,767. count_hdr = pdb_local_header->76 * 256 + pdb_local_header->77; if (count_hdr < PDB_RECORD_COUNT_MIN) { glk_stream_close (stream, GLK_NULL); errno = E_PDB_NO_DATA; return false; } else if (count_hdr > PDB_RECORD_COUNT_MAX) { glk_stream_close (stream, GLK_NULL); errno = E_PDB_TOO_BIG; return false; } ! Load record zero into a local buffer, to get compression and ! data records count. if (~~pdb_load_record_0 (stream)) { glk_stream_close (stream, GLK_NULL); return false; } ! Determine if the pdb records are compressed, and check values. compression = pdb_local_record_0->0 * 256 + pdb_local_record_0->1; if (compression ~= 1 or 2 or 1026) { glk_stream_close (stream, GLK_NULL); errno = E_PDB_BAD_VER; return false; } ! This looks like a valid pdb eBook. Close any existing open ! pdb stream, and make the newly opened one current. pdb_close_ebook (); pdb_stream = stream; ! Note the compression. pdb_is_compressed = (compression == 2 or 1026); ! Extract the record count from record zero, and use it, or the ! header's, whichever is lower, as the record count. count_r0 = pdb_local_record_0->8 * 256 + pdb_local_record_0->9; if (count_r0 > 0 && count_r0 < count_hdr) pdb_record_count = count_r0; else pdb_record_count = count_hdr - 1; ! Note index size, for finding the last record length. pdb_index_count = count_hdr - 1; ! Salt away the book's title, if any found. for (i = 0: pdb_local_header->i ~= 0 && i < PDB_TITLE_SIZE: i++) pdb_title->i = pdb_local_header->i; pdb_title_length = i; ! Measure the length of the file. glk_stream_set_position (pdb_stream, 0, seekmode_End); pdb_file_length = glk_stream_get_position (pdb_stream); ! Copy out the local header and record 0 for use by bookmarks. for (i = 0: i < PDB_HEADER_SIZE: i++) pdb_header->i = pdb_local_header->i; for (i = 0: i < PDB_RECORD_0_SIZE: i++) pdb_record_0->i = pdb_local_record_0->i; return true; ]; ! ! pdb_load_record() ! ! Load the specified record into the buffer, and record its length. Return ! true on success, false with errno on error. ! [ pdb_load_record recnum seek offset next_offset; ! Fail if no open pdb stream. if (pdb_stream == GLK_NULL) { errno = E_PDB_NOT_OPEN; return false; } ! Range check record number; 1 to N inclusive. if (recnum < 1 || recnum > pdb_record_count) { errno = E_PDB_BAD_REC; return false; } ! If already loaded, return immediately. if (recnum == pdb_current) return true; ! Find the record offset from its index entry. seek = PDB_HEADER_SIZE + PDB_RECORD_HEADER_SIZE * recnum; glk_stream_set_position (pdb_stream, seek, seekmode_Start); if (glk_stream_get_position (pdb_stream) ~= seek) { errno = E_PDB_BAD_REC; return false; } offset = pdb_get_int32_stream (pdb_stream); if (glk_stream_get_position (pdb_stream) ~= seek + WORDSIZE) { errno = E_PDB_BAD_REC; return false; } ! Find the end of the record, either the next record's offset, or ! end of file if this is the last record in the index. if (recnum < pdb_index_count) { seek = seek + PDB_RECORD_HEADER_SIZE; glk_stream_set_position (pdb_stream, seek, seekmode_Start); if (glk_stream_get_position (pdb_stream) ~= seek) { errno = E_PDB_NO_END; return false; } next_offset = pdb_get_int32_stream (pdb_stream); if (glk_stream_get_position (pdb_stream) ~= seek + WORDSIZE) { errno = E_PDB_NO_END; return false; } } else next_offset = pdb_file_length; ! Seek to, and read the requested record. glk_stream_set_position (pdb_stream, offset, seekmode_Start); if (glk_stream_get_position (pdb_stream) ~= offset) { errno = E_PDB_REC_READ; return false; } pdb_length = glk_get_buffer_stream (pdb_stream, pdb_buffer, next_offset - offset); if (pdb_length ~= next_offset - offset) { pdb_current = 0; errno = E_PDB_REC_READ; return false; } ! Note current record number and return. pdb_current = recnum; return true; ]; !----------------------------------------------------------------------------- ! Record decoding. !----------------------------------------------------------------------------- ! Cache two decoded pdb records. This helps with efficiency; paging hops ! across the boundary of two pages fairly frequently, and by keeping two in ! memory, we can avoid repeated pdb reads and decoding. Array decode_buffer_a -> DECODE_BUFFER_SIZE; Array decode_buffer_b -> DECODE_BUFFER_SIZE; Global decode_length_a = 0; Global decode_current_a = 0; Global decode_length_b = 0; Global decode_current_b = 0; ! Caller-visible decode buffer and length, and an approximated measure of ! record expansions for location percentage calculations. Global decode_buffer = 0; Global decode_length = 0; Global decode_expansion = 0; ! Magic constants for sequence decoding. Constant DECODE_MASK_11 $7ff; Constant DECODE_MASK_3 7; Constant DECODE_SHIFT_3 8; ! ! decode_record() ! ! Load and decode the requested record into the decode buffer. For efficiency ! we cache two decoded buffers, and try to return data from the cache where ! possible. Return true on success, fail with errno on error. ! [ decode_record recnum i j c wdist wlen n; ! Refuse to decode record number 0. if (recnum == 0) { errno = E_DECODE_BAD_REC; return false; } ! If already loaded and decoded, return immediately. if (recnum == decode_current_a) { decode_buffer = decode_buffer_a; decode_length = decode_length_a; return true; } else if (recnum == decode_current_b) { decode_buffer = decode_buffer_b; decode_length = decode_length_b; return true; } ! Load the requested pdb record, and fail on error. if (~~pdb_load_record (recnum)) return false; ! Keep the most recently used cache decode, and drop the other. if (decode_buffer == decode_buffer_a) decode_buffer = decode_buffer_b; else decode_buffer = decode_buffer_a; ! If the pdb is not compressed, copy data only. if (~~pdb_is_compressed) { ! Copy data directly. for (i = 0: i < pdb_length: i++) decode_buffer->i = pdb_buffer->i; decode_length = pdb_length; ! Record as an approximate measure of expansion. if (decode_expansion == 0) decode_expansion = decode_length; ! Update cache and return; if (decode_buffer == decode_buffer_a) { decode_length_a = decode_length; decode_current_a = recnum; } else { decode_length_b = decode_length; decode_current_b = recnum; } return true; } ! Decode each character from the pdb record. for (i = 0, j = 0: i < pdb_length: ) { c = pdb_buffer->i++; ! Type A, next c chars are literal. if (c >= 1 && c <= 8) { while (c-- && j < DECODE_BUFFER_SIZE) decode_buffer->j++ = pdb_buffer->i++; } ! Self-representing, no command type. else if (c <= $7f || c == 0) decode_buffer->j++ = c; ! Type C, space followed by xor'ed char. else if (c >= $c0) { decode_buffer->j++ = ' '; if (j < DECODE_BUFFER_SIZE) decode_buffer->j++ = (c | $80) & ~(c & $80); } ! Type B, sliding window. else { ! Move c to high bits and read low. Calculate window ! distance as 11 bits, and window length as the low 3 ! bits + 3. c = c * 256 + pdb_buffer->i++; wdist = (c / DECODE_SHIFT_3) & DECODE_MASK_11; wlen = (c & DECODE_MASK_3) + 3; ! Copy until window exhausted. for (n = wlen: n-- && j < DECODE_BUFFER_SIZE: j++) decode_buffer->j = decode_buffer->(j - wdist); } ! Check for decode buffer overrun. if (j == DECODE_BUFFER_SIZE) { if (decode_buffer == decode_buffer_a) decode_current_a = 0; else decode_current_b = 0; errno = E_DECODE_BUF; return false; } } ! Save the decoded buffer length. decode_length = j; ! Record as an approximate measure of expansion. if (decode_expansion == 0) decode_expansion = decode_length; ! Update cache and return; if (decode_buffer == decode_buffer_a) { decode_length_a = decode_length; decode_current_a = recnum; } else { decode_length_b = decode_length; decode_current_b = recnum; } return true; ]; ! ! decode_reset() ! ! Flush cached decoded data back to initial values. ! [ decode_reset; decode_length_a = 0; decode_current_a = 0; decode_length_b = 0; decode_current_b = 0; decode_buffer = 0; decode_length = 0; decode_expansion = 0; ]; !----------------------------------------------------------------------------- ! Book locations. !----------------------------------------------------------------------------- ! Location magic numbers. Constant LOCATION_SHIFT 65536; Constant LOCATION_MASK LOCATION_SHIFT - 1; ! ! location() ! location_recnum() ! location_offset() ! ! Helper functions to combine a record and offset into a single location ! integer, and extract record or offset from a location. ! [ location recnum offset; return recnum * LOCATION_SHIFT + offset; ]; [ location_recnum loc; return loc / LOCATION_SHIFT; ]; [ location_offset loc; return loc & LOCATION_MASK; ]; ! ! location_percent() ! percent_location() ! ! Return the approximate percentage of a location through the book, and the ! approximate location for a given percentage. ! [ location_percent loc recnum offset length posn; ! Fail if no decode expansion set. if (decode_expansion == 0) { errno = E_LOCATION_NO_EXP; return -1; } ! Approximate a position from the location. recnum = location_recnum (loc); offset = location_offset (loc); posn = (recnum - 1) * decode_expansion + offset; ! Extrapolate length, return a percentage for the position. length = decode_expansion * pdb_record_count; return (posn * 100) / length; ]; [ percent_location percent length posn recnum offset; ! Fail if no decode expansion set. if (decode_expansion == 0) { errno = E_LOCATION_NO_EXP; return -1; } ! Extrapolate length, approximate a location from percentage. length = decode_expansion * pdb_record_count; posn = (length * percent) / 100; recnum = posn / decode_expansion + 1; offset = posn % decode_expansion; ! If location exceeds the document, clamp it. if (recnum > pdb_record_count) recnum = pdb_record_count; if (~~decode_record (recnum)) return -1; if (offset > decode_length - 1) offset = decode_length - 1; ! Return the constructed location. return location (recnum, offset); ]; !----------------------------------------------------------------------------- ! CRC functions. !----------------------------------------------------------------------------- ! CRC magic number and other constants. Constant CRC_CHAR_BIT 8; Constant CRC_UCHAR_MAX 255; Constant CRC_MAGIC $edb88320; Constant CRC_ALLONES $ffffffff; ! CRC self-test data -- standard simple test. Array CRC_TEST -> '1''2''3''4''5''6''7''8''9'; Constant CRC_CHECK $cbf43926; ! ! crc_rshift() ! crc_xor() ! ! Bit-level right shift and xor for CRC functions. ! [ crc_rshift x n retval; @ushiftr x n retval; return retval; ]; [ crc_xor a b; return (a | b) & ~(a & b); ]; ! ! crc_update() ! ! Update a running CRC with buffer data. ! Array crc_table --> CRC_UCHAR_MAX + 1; Global crc_table_built = false; [ crc_update crc buf len c n k; ! If not yet done, build the CRC lookup table. if (~~crc_table_built) { ! Create a table entry for each byte value. for (n = 0: n < CRC_UCHAR_MAX + 1: n++) { c = n; for (k = 0: k < CRC_CHAR_BIT: k++) { if (c & 1) c = crc_xor (CRC_MAGIC, crc_rshift (c, 1)); else c = crc_rshift (c, 1); } crc_table-->n = c; } ! Flag the table as built. crc_table_built = true; ! CRC self-test (after flag set -- recursion!). c = crc_xor (CRC_ALLONES, crc_update (CRC_ALLONES, CRC_TEST, 9)); if (c ~= CRC_CHECK) { errno = E_CRC_FAILED; fatal (); } } ! Update the CRC with each character in the buffer. c = crc; for (n = 0: n < len: n++) { c = crc_xor (crc_rshift (c, CRC_CHAR_BIT), crc_table-->(crc_xor (buf->n, c) & CRC_UCHAR_MAX)); } ! Return the updated CRC. return c; ]; ! ! crc_start() ! crc_end() ! ! Start and wrap up CRC calculations. ! [ crc_start; return CRC_ALLONES; ]; [ crc_end c; return crc_xor (c, CRC_ALLONES); ]; !----------------------------------------------------------------------------- ! Persistent book locations. !----------------------------------------------------------------------------- ! Bookmark file constants and data. The bookmark file id/type is "Gbmk", ! held below as $47626d6b. Record sizes _must_ be a multiple of WORDSIZE. Constant BOOKMARK_RECORD_SIZE 16; Constant BOOKMARK_HEADER_SIZE 16; Array BOOKMARK_FILE -> $e0 'G''L''K''E''B''O''O''K' '.''B''M''K' 0; Constant BOOKMARK_FILE_ID $47626d6b; Constant BOOKMARK_FILE_VERSION $00000100; ! Bookmark record array; contains the signature, current position, and any ! set bookmark for the book. The last word is used as a CRC of the first ! three fields, for safety. Also, the bookmark file header, containing the ! id/type, a version number just in case, and eight commentary bytes. Array bookmark_record --> BOOKMARK_RECORD_SIZE / WORDSIZE; Array bookmark_header --> BOOKMARK_HEADER_SIZE / WORDSIZE; ! ! bookmark_rec_set() ! bookmark_rec_current() ! bookmark_rec_marker() ! bookmark_rec_check_crc() ! ! Helpers for setting and dissecting bookmark records. ! [ bookmark_rec_set sig current bookmark crc; bookmark_record-->0 = sig; bookmark_record-->1 = current; bookmark_record-->2 = bookmark; crc = crc_update (crc_start (), bookmark_record, BOOKMARK_RECORD_SIZE - WORDSIZE); bookmark_record-->3 = crc_end (crc); ]; [ bookmark_rec_current; return bookmark_record-->1; ]; [ bookmark_rec_marker; return bookmark_record-->2; ]; [ bookmark_rec_check_crc crc; crc = crc_update (crc_start (), bookmark_record, BOOKMARK_RECORD_SIZE - WORDSIZE); return (crc_end (crc) == bookmark_record-->3); ]; ! ! bookmark_signature() ! ! Return the "signature" of the current book. We try to make it unique; ! the CRC of the PDB header, record 0, and the first data record. None of ! the pdb's header fields alone can really be relied on to accomplish this. ! [ bookmark_signature crc; ! Calculate the CRC of the pdb header and record 0. crc = crc_update (crc_start (), pdb_header, PDB_HEADER_SIZE); crc = crc_update (crc, pdb_record_0, PDB_RECORD_0_SIZE); ! Add the first data record (load should never fail). if (pdb_load_record (1)) crc = crc_update (crc, pdb_buffer, pdb_length); ! Return the CRC. return crc_end (crc); ]; ! ! bookmark_create() ! ! Create a new bookmarks file. Returns the writable stream, or GLK_NULL ! on error, with errno set. ! [ bookmark_create fileref stream posn; ! Create a bookmarks fileref. fileref = glk_fileref_create_by_name (fileusage_Data|fileusage_BinaryMode, BOOKMARK_FILE, 0); if (fileref == GLK_NULL) { errno = E_BOOKMARK_BAD_REF; return GLK_NULL; } ! Create a new writable stream, fail on error. stream = glk_stream_open_file (fileref, filemode_Write, 0); if (stream == GLK_NULL) { glk_fileref_destroy (fileref); errno = E_BOOKMARK_CANT_OPEN; return GLK_NULL; } glk_fileref_destroy (fileref); ! Write the initial file header -- id and version to the first ! two words; Glkebook's release and serial number to the second ! two (these are commentary only; unused on reading the file). bookmark_header-->0 = BOOKMARK_FILE_ID; bookmark_header-->1 = BOOKMARK_FILE_VERSION; bookmark_header-->2 = 0-->13; ! 52->0 word transfer bookmark_header-->3 = 0-->14; ! 56->0 word transfer posn = glk_stream_get_position (stream); glk_put_buffer_stream (stream, bookmark_header, BOOKMARK_HEADER_SIZE); if (glk_stream_get_position (stream) ~= posn + BOOKMARK_HEADER_SIZE) { glk_stream_close (stream, GLK_NULL); errno = E_BOOKMARK_WRITE; return GLK_NULL; } ! Return the new writable stream. return stream; ]; ! ! bookmark_open() ! ! Open a stream for bookmarks, and seek to the bookmark for the given ! signature. For write, return a writable stream with with position set, ! or GLK_NULL on error. For read, return a readable stream with bookmark ! data, matched or unmatched, in bookmark_record, and GLK_NULL on error. ! Sets errno on error returns. ! [ bookmark_open write sig fileref stream len matched; ! Create a bookmarks fileref. fileref = glk_fileref_create_by_name (fileusage_Data|fileusage_BinaryMode, BOOKMARK_FILE, 0); if (fileref == GLK_NULL) { errno = E_BOOKMARK_BAD_REF; return GLK_NULL; } ! Open read or readwrite stream. if (write) { ! Open readwrite stream. If not openable, create a new ! file. glk_does_file_exist() is broken in many Glulx ! implementations, and worse, filemode_ReadWrite differs ! across Glks -- in Xglk, this fails if the file doesn't ! exist, but in Winglk, it succeeds and creates a new ! empty file. So... we have to do this the _hard_ way. ! ! First, try a read open, as a substitute for a call to ! glk_does_file_exist(). stream = glk_stream_open_file (fileref, filemode_Read, 0); if (stream == GLK_NULL) { glk_fileref_destroy (fileref); ! File doesn't exist, so create and return a new ! bookmarks file. stream = bookmark_create (); return stream; } ! If the file does exist, close it and reopen readwrite. ! If the reopen fails, perhaps the file is marked readonly. glk_stream_close (stream, GLK_NULL); stream = glk_stream_open_file (fileref, filemode_ReadWrite, 0); if (stream == GLK_NULL) { glk_fileref_destroy (fileref); errno = E_BOOKMARK_CANT_REOPEN; return GLK_NULL; } } else { ! Open read stream. If not openable, error. stream = glk_stream_open_file (fileref, filemode_Read, 0); if (stream == GLK_NULL) { glk_fileref_destroy (fileref); errno = E_BOOKMARK_CANT_OPEN; return GLK_NULL; } } ! Finished with fileref. glk_fileref_destroy (fileref); ! Verify that the file's header is valid. len = glk_get_buffer_stream (stream, bookmark_header, BOOKMARK_HEADER_SIZE); if (len ~= BOOKMARK_HEADER_SIZE || bookmark_header-->0 ~= BOOKMARK_FILE_ID || bookmark_header-->1 ~= BOOKMARK_FILE_VERSION) { glk_stream_close (stream, GLK_NULL); errno = E_BOOKMARK_HDR; return GLK_NULL; } ! Read bookmarks from the file, looking for a match. matched = false; while (~~matched) { ! Read next bookmark record, break on end of file. len = glk_get_buffer_stream (stream, bookmark_record, BOOKMARK_RECORD_SIZE); if (len ~= BOOKMARK_RECORD_SIZE) { glk_stream_set_position (stream, -(len), seekmode_Current); break; } ! Check the record's CRC. If we run into _any_ invalid ! records, the file might be corrupted, so fail. if (~~bookmark_rec_check_crc ()) { glk_stream_close (stream, GLK_NULL); errno = E_BOOKMARK_BAD_REC; return GLK_NULL; } ! Compare this record to the book signature. On match, back ! up to record start and note match. if (bookmark_record-->0 == sig) { glk_stream_set_position (stream, -(BOOKMARK_RECORD_SIZE), seekmode_Current); matched = true; } } ! Return the opened stream. return stream; ]; ! ! bookmark_load() ! ! Locate and read the bookmark record for the current book. Returns true ! on success, false with errno on error. ! [ bookmark_load stream sig; ! Generate the book's signature. Use current record if it matches ! and has a valid CRC. sig = bookmark_signature (); if (bookmark_record-->0 == sig && bookmark_rec_check_crc ()) return true; ! Search for the bookmark, and return false if not found. For the ! "soft" error of bookmarks openable, but the requested one just ! absent, set errno 0. stream = bookmark_open (false, sig); if (stream == GLK_NULL) return false; if (bookmark_record-->0 ~= sig) { glk_stream_close (stream, GLK_NULL); errno = E_SUCCESS; return false; } ! Close the bookmark file, and return success. glk_stream_close (stream, GLK_NULL); return true; ]; ! ! bookmark_verify() ! ! In the unlikely case of a clash of book signatures, a saved location ! might be beyond the book end. This function indicates such a problem. ! [ bookmark_verify loc recnum offset; ! Split up the location. recnum = location_recnum (loc); offset = location_offset (loc); ! If location exceeds the document, return error. if (recnum > pdb_record_count) return false; if (~~decode_record (recnum)) return false; if (offset > decode_length - 1) return false; ! Safe to use this location. return true; ]; ! ! bookmark_get_current() ! bookmark_get_marker() ! ! Return the current location and the bookmark location for the current book. ! Return 0, with errno, on error (errno is zero if no matching record was ! found for the current book). ! [ bookmark_get_current loc; ! Load the bookmark record. if (~~bookmark_load ()) return 0; ! Verify that it looks valid for the book, return 0 if not. loc = bookmark_rec_current (); if (bookmark_verify (loc)) return loc; else return 0; ]; [ bookmark_get_marker loc; ! Load the bookmark record. if (~~bookmark_load ()) return 0; ! Verify that it looks valid for the book, return 0 if not. loc = bookmark_rec_marker (); if (bookmark_verify (loc)) return loc; else return 0; ]; ! ! bookmark_save() ! ! Save the current location and bookmark for this book. Return true if saved ! successfully, false otherwise. ! [ bookmark_save current marker stream sig posn; ! Generate the book's signature. sig = bookmark_signature (); ! If there's a valid, matching record already buffered, and it ! contains the data we've just been given, then it came out of the ! bookmarks file or is already written; no need to write out again. if (bookmark_record-->0 == sig && bookmark_rec_check_crc () && bookmark_rec_current () == current && bookmark_rec_marker () == marker) return true; ! Search for the bookmark, seeks to file end if not found. stream = bookmark_open (true, sig); if (stream == GLK_NULL) return false; ! Set the bookmark record and write it back. bookmark_rec_set (sig, current, marker); posn = glk_stream_get_position (stream); glk_put_buffer_stream (stream, bookmark_record, BOOKMARK_RECORD_SIZE); if (glk_stream_get_position (stream) ~= posn + BOOKMARK_RECORD_SIZE) { glk_stream_close (stream, GLK_NULL); errno = E_BOOKMARK_WRITE; return GLK_NULL; } ! Close the bookmark file, return success. glk_stream_close (stream, GLK_NULL); return true; ]; !----------------------------------------------------------------------------- ! Paging. !----------------------------------------------------------------------------- ! ! page_adjust() ! ! Adjust a given location to settle on a line or word break, and return ! the adjusted location. Give up if more than width scanned. ! Constant NEWLINE 10; Constant RETURN 13; [ page_adjust loc forward width incr recnum offset done limit checked newloc; ! Set the increment depending on direction. if (forward) incr = 1; else incr = -1; ! Start at the location handed in. recnum = location_recnum (loc); offset = location_offset (loc); ! Search forward or backward for a word break. checked = 0; done = false; while (~~done) { if (~~decode_record (recnum)) return -1; ! Set offset limit depending on direction. if (forward) limit = decode_length - 1; else limit = 0; ! Scan decoded characters for newlines or space. while (~~done) { ! Check against newline, return, or space. if (decode_buffer->offset == NEWLINE or RETURN or ' ') done = true; ! Drop out if more than a line scanned. else { checked++; if (checked > width) done = true; } ! If not at limit, next offset, otherwise break. if (~~done) { if (offset ~= limit) offset = offset + incr; else break; } } ! If we ran out of decode buffer, try any next/prior one. if (~~done) { ! If no more, break the search loop. if (forward) { if (recnum == pdb_record_count) break; } else { if (recnum == 1) break; } ! Move to the next/prior record. recnum = recnum + incr; if (forward) offset = 0; else { if (~~decode_record (recnum)) return -1; offset = decode_length - 1; } } } ! Generate the new location, or the old one if we ran out of patience. if (checked > width) newloc = loc; else newloc = location (recnum, offset); ! Return the new page location. return newloc; ]; ! ! page_next_prior() ! ! Given a location, return a new location for either the next, or the prior, ! page. The function needs the window width and height to do this. Return ! the location on success, -1 with errno on error. Note: returns the very ! end of the document on forward scan from the last page start. ! [ page_next_prior loc forward width height incr recnum offset lcount ccount limit newloc; ! Set the increment depending on direction. if (forward) incr = 1; else incr = -1; ! Start at the location handed in. recnum = location_recnum (loc); offset = location_offset (loc); ! Loop until enough lines found. lcount = 0; ccount = 0; while (lcount < height) { if (~~decode_record (recnum)) return -1; ! Set offset limit depending on direction. if (forward) limit = decode_length - 1; else limit = 0; ! Update counts for decoded characters. while (lcount < height) { ! Update counts for this character. if (decode_buffer->offset == NEWLINE or RETURN) { lcount++; ccount = 0; } else { ccount++; if (ccount > width) { lcount++; ccount = 0; } } ! If not at limit, next offset, otherwise break. if (lcount < height) { if (offset ~= limit) offset = offset + incr; else break; } } ! If we ran out of decode buffer, try the next/prior one, if ! any (and if none, we're done). if (lcount < height) { ! If no more, break the search loop. if (forward) { if (recnum == pdb_record_count) break; } else { if (recnum == 1) break; } ! Move to the next/prior record. recnum = recnum + incr; if (forward) offset = 0; else { if (~~decode_record (recnum)) return -1; offset = decode_length - 1; } } } ! Generate the page location, and adjust unless we ran out of data. ! Adjust in opposing direction to avoid making the page longer. if (lcount < height) newloc = location (recnum, offset); else newloc = page_adjust (location (recnum, offset), ~~forward, width); ! Return the new page location. return newloc; ]; ! Because prior page is not an exact reversal of next, cache one prior ! location, so going back one page is an exact retrace of the last page ! forward. Others may not be. We have to record the width and height, ! to ensure they match before using the cached location. Global page_prior_width = 0; Global page_prior_height = 0; Global page_prior_location = 0; ! ! page_next() ! page_prior() ! ! Given a location, return a new location for either the next, or the prior, ! page. ! [ page_next loc width height next beyond; ! Get the next page location. next = page_next_prior (loc, true, width, height); if (next == -1) return -1; ! Get the page after the next one. beyond = page_next_prior (next, true, width, height); if (beyond == -1) return -1; ! If there is no page after next, this is the very end of the book; ! return loc, as the end of the book is an empty page. if (beyond == next) return loc; ! Update the prior page location cache. page_prior_width = width; page_prior_height = height; page_prior_location = loc; ! Return the next page. return next; ]; [ page_prior loc width height prior; ! If a page prior cache location is available, see if it matches ! the width and height passed in. if (page_prior_location ~= 0) { ! On match, use cached prior, otherwise search as normal. if (width == page_prior_width && height == page_prior_height) prior = page_prior_location; else prior = page_next_prior (loc, false, width, height); ! Drop the cached location, whether matched or not. page_prior_location = 0; } else { ! No cache, so search as normal. prior = page_next_prior (loc, false, width, height); } ! Return the prior page location. return prior; ]; ! ! page_reset() ! ! Flush cached prior page location back to initial values. ! [ page_reset; page_prior_location = 0; page_prior_width = 0; page_prior_height = 0; ]; ! ! first_page() ! last_page() ! ! Return locations for the first and the last displayable pages. ! [ first_page; ! Flush cache and return the book start location. page_reset (); return location (1, 0); ]; [ last_page width height loc; ! Find the very end of the book. if (~~decode_record (pdb_record_count)) return -1; loc = location (pdb_record_count, decode_length - 1); ! Flush cache and return a location one page back. page_reset (); return page_next_prior (loc, false, width, height); ]; ! ! page_print() ! ! Print a page forwards from the given location, given the width and height ! of the screen. Returns true on success, false with errno on error. ! [ page_print loc width height endloc endrec endoff recnum offset limit marker curs c; ! Get the end location for printing, and split up. endloc = page_next_prior (loc, true, width, height); if (endloc == -1) return false; endrec = location_recnum (endloc); endoff = location_offset (endloc); ! Start at the location handed in. recnum = location_recnum (loc); offset = location_offset (loc); ! Loop until the end location is reached. while (recnum <= endrec) { if (~~decode_record (recnum)) return false; ! Set the limit on decode buffer write. if (recnum == endrec) limit = endoff; else limit = decode_length - 1; ! Scan for invalid Glk characters, and handle specially. marker = offset; for (curs = offset: curs <= limit: curs++) { ! Check for Glk printability, and loop if printable. ! Special check for newline which we know will print ! (Xglk denies it will, though), and 7-bit ASCII. c = decode_buffer->curs; if (c == NEWLINE || (c >= 32 && c <= 126) || glk_gestalt (gestalt_CharOutput, c)) continue; ! Flush data up to this point. if (curs > marker) glk_put_buffer (decode_buffer + marker, curs - marker); ! Handle unprintable character, advance marker. if (c == RETURN) glk_put_char (NEWLINE); else if (c < ' ' || c == 128) glk_put_char (' '); else if (c == 131) glk_put_char ('f'); else if (c == 133) { glk_put_char ('.'); glk_put_char ('.'); glk_put_char ('.'); } else if (c == 136) glk_put_char ('^'); else if (c == 139) glk_put_char ('<'); else if (c == 140) { glk_put_char ('O'); glk_put_char ('E'); } else if (c == 145 or 146) glk_put_char (39); else if (c == 147 or 148) glk_put_char ('"'); else if (c == 150 or 151) glk_put_char ('-'); else if (c == 152) glk_put_char ('~'); else if (c == 153) { glk_put_char ('['); glk_put_char ('T'); glk_put_char ('M'); glk_put_char (']'); } else if (c == 155) glk_put_char ('>'); else if (c == 156) { glk_put_char ('o'); glk_put_char ('e'); } else glk_put_char ('?'); marker = curs + 1; } ! Flush remaining unprinted data. if (curs > marker) glk_put_buffer (decode_buffer + marker, curs - marker); ! Move on to the next record. If we just handled the ! last record, then recnum becomes endrec+1 and we exit ! the loop as a result. recnum++; offset = 0; } return true; ]; !----------------------------------------------------------------------------- ! GUI toolbar. !----------------------------------------------------------------------------- ! Toolbar background and shading, and timeouts for button press simulation. Constant TOOLBAR_BACKGROUND $dcdcdc; Constant TOOLBAR_LIGHT_SHADE $ffffff; Constant TOOLBAR_DARK_SHADE $9a9a9a; Constant TOOLBAR_TIMEOUT 50; Constant TOOLBAR_TIMEOUT_COUNT 4; ! Icons and images. The redraw pseudo-button is special; an indication that ! the toolbar changed size (collapsed, or restored), and that paging functions ! should redraw their page to account for the new main window dimensions. Constant PIC_NONE 0; Constant PIC_VERT_SEPARATOR 1; Constant PIC_HORIZ_SEPARATOR 2; Constant PIC_IDLE_SEPARATOR 3; Constant BUTTON_FIRST_PAGE 4; Constant BUTTON_LAST_PAGE 5; Constant BUTTON_NEXT_PAGE 6; Constant BUTTON_PRIOR_PAGE 7; Constant BUTTON_NEXT_SHORT 8; Constant BUTTON_PRIOR_SHORT 9; Constant BUTTON_SET_BOOKMARK 10; Constant BUTTON_GOTO_BOOKMARK 11; Constant BUTTON_TOGGLE_AUTO 12; Constant BUTTON_UNDO 13; Constant BUTTON_HELP 14; Constant BUTTON_LICENSE 15; Constant BUTTON_NOTES 16; Constant BUTTON_NEW_BOOK 17; Constant BUTTON_QUIT 18; Constant BUTTON_PSEUDO_REDRAW -1; ! Active toolbar icons layout, and count of icon coord entries. Constant TOOLBAR_COORD_ENTRIES 105; ! 5 * number of icons Array TOOLBAR_LAYOUT -> PIC_VERT_SEPARATOR BUTTON_PRIOR_PAGE BUTTON_NEXT_PAGE BUTTON_PRIOR_SHORT BUTTON_NEXT_SHORT PIC_IDLE_SEPARATOR BUTTON_FIRST_PAGE BUTTON_LAST_PAGE PIC_IDLE_SEPARATOR BUTTON_SET_BOOKMARK BUTTON_GOTO_BOOKMARK PIC_IDLE_SEPARATOR BUTTON_UNDO BUTTON_TOGGLE_AUTO PIC_IDLE_SEPARATOR BUTTON_HELP BUTTON_LICENSE BUTTON_NOTES PIC_IDLE_SEPARATOR BUTTON_NEW_BOOK BUTTON_QUIT PIC_NONE; ! Toolbar window, note of pressed button, displayed icon slots (five bytes per ! entry -- button, xlo, ylo, xhi, yhi), collapsed, hidden, timeout counter. Global toolbar_win = GLK_NULL; Global toolbar_pressed = PIC_NONE; Array toolbar_coords --> TOOLBAR_COORD_ENTRIES; Global toolbar_is_collapsed = false; Global toolbar_is_hidden = false; Global toolbar_timeouts = 0; ! ! toolbar_outline3d ! ! Display a modest 3-d effect around a region. ! [ toolbar_outline3d lowered x y w h tls brs; ! Decide shading based on whether the region is lowered or raised. if (lowered) { tls = TOOLBAR_DARK_SHADE; brs = TOOLBAR_LIGHT_SHADE; } else { tls = TOOLBAR_LIGHT_SHADE; brs = TOOLBAR_DARK_SHADE; } ! Fill areas above, left, below, and right with shading. glk_window_fill_rect (toolbar_win, tls, x, y, w, 1); glk_window_fill_rect (toolbar_win, tls, x, y, 1, h); glk_window_fill_rect (toolbar_win, brs, x + 2, y + h - 1, w - 2, 1); glk_window_fill_rect (toolbar_win, brs, x + w - 1, y + 2, 1, w - 2); ]; ! ! toolbar_icon() ! ! Helper for painting a toolbar. Display an icon, and note its coordinates ! for mouse press checks. ! [ toolbar_icon pic x y width height index i; ! Render, with a cheap button press effect if pressed. if (pic == toolbar_pressed) { glk_image_draw (toolbar_win, pic, x + 1, y + 1); toolbar_outline3d (true, x, y, width, height); } else glk_image_draw (toolbar_win, pic, x, y); ! Note button image and coordinates. i = index; toolbar_coords-->i++ = pic; toolbar_coords-->i++ = x; toolbar_coords-->i++ = y; toolbar_coords-->i++ = x + width; toolbar_coords-->i++ = y + height; ! Return next index to use. return i; ]; ! ! toolbar_paint() ! ! Paint in the toolbar. ! Array toolbar_glkargs --> 2; [ toolbar_paint x y width height i button pic; ! Ignore the call if we have no toolbar. if (toolbar_win == GLK_NULL) return; ! Set toolbar window background and clear it. glk_window_set_background_color (toolbar_win, TOOLBAR_BACKGROUND); glk_window_clear (toolbar_win); ! Clear all current icon images and coordinates. for (i = 0: i < TOOLBAR_COORD_ENTRIES: i++) toolbar_coords-->i = 0; ! If totally hidden, nothing more to do. if (toolbar_is_hidden) return; ! Start icon image and coordinate entries at 0. i = 0; ! If collapsed, paint only the horizontal control separator. if (toolbar_is_collapsed) { glk_image_get_info (PIC_HORIZ_SEPARATOR, toolbar_glkargs, toolbar_glkargs + WORDSIZE); width = toolbar_glkargs-->0; height = toolbar_glkargs-->1; x = 2; y = 0; i = toolbar_icon (PIC_HORIZ_SEPARATOR, x, y, width, height, i); return; } ! Paint a small 3-d effect. glk_window_get_size (toolbar_win, toolbar_glkargs, toolbar_glkargs + WORDSIZE); width = toolbar_glkargs-->0; height = toolbar_glkargs-->1; toolbar_outline3d (false, 0, 0, width, height); ! Display each button icon in the layout. x = 0; y = 2; for (button = 0: TOOLBAR_LAYOUT->button ~= PIC_NONE: button++) { ! Get the image, and its dimensions. pic = TOOLBAR_LAYOUT->button; glk_image_get_info (pic, toolbar_glkargs, toolbar_glkargs + WORDSIZE); width = toolbar_glkargs-->0; height = toolbar_glkargs-->1; ! Display the image, spaced on separators, and advance x. if (pic == PIC_VERT_SEPARATOR or PIC_IDLE_SEPARATOR) { x = x + 2; i = toolbar_icon (pic, x, y, width, height, i); x = x + width + 2; } else { i = toolbar_icon (pic, x, y, width, height, i); x = x + width; } } ]; ! ! toolbar_create() ! ! Create the toolbar window. ! [ toolbar_create win h; ! Check Glk library capabilities; ignore if not possible. if (~~(glk_gestalt (gestalt_Graphics, 0) && glk_gestalt (gestalt_DrawImage, wintype_Graphics) && glk_gestalt (gestalt_MouseInput, wintype_Graphics) && glk_gestalt (gestalt_Timer, 0))) return; ! Discern initial toolbar height from the vertical separator. glk_image_get_info (PIC_VERT_SEPARATOR, GLK_NULL, toolbar_glkargs); h = toolbar_glkargs-->0; ! Split main window to create toolbar at the top. toolbar_win = glk_window_open (win, winmethod_Above|winmethod_Fixed, h + 4, wintype_Graphics, 0); if (toolbar_win == 0) return; ! Initialize toolbar variables. toolbar_pressed = PIC_NONE; toolbar_is_collapsed = false; toolbar_is_hidden = false; toolbar_timeouts = 0; ! Paint the toolbar window. toolbar_paint (); ]; ! ! toolbar_set_collapsed() ! ! Set toolbar collapsed mode, and resize the window if necessary. ! [ toolbar_set_collapsed collapsed height _parent; ! Ignore the call if we have no toolbar. if (toolbar_win == GLK_NULL) return; ! Find required height from the separator. Add four for borders ! to the uncollapsed toolbar. toolbar_is_collapsed = collapsed; if (toolbar_is_collapsed) { glk_image_get_info (PIC_HORIZ_SEPARATOR, GLK_NULL, toolbar_glkargs); height = toolbar_glkargs-->0; ! get height } else { glk_image_get_info (PIC_VERT_SEPARATOR, GLK_NULL, toolbar_glkargs); height = toolbar_glkargs-->0 + 4; ! get height, add border } ! Resize to this height. _parent = glk_window_get_parent (toolbar_win); glk_window_set_arrangement (_parent, winmethod_Above|winmethod_Fixed, height, 0); ! Paint the new appearance. toolbar_paint (); ]; ! ! toolbar_set_hidden() ! ! Try to completely hide the toolbar temporarily. Glk doesn't react well to ! zero-height windows, so we use one pixel height; not completely hidden. ! [ toolbar_set_hidden hidden _parent; ! Ignore the call if we have no toolbar. if (toolbar_win == GLK_NULL) return; ! Set new hidden flag, and resize to one pixel in height if hidden. toolbar_is_hidden = hidden; if (toolbar_is_hidden) { _parent = glk_window_get_parent (toolbar_win); glk_window_set_arrangement (_parent, winmethod_Above|winmethod_Fixed, 1, 0); } else { ! Restore correct toolbar status -- collapsed, or full. toolbar_set_collapsed (toolbar_is_collapsed); } ! Paint the new highly minimal appearance. toolbar_paint (); ]; ! ! toolbar_handle_click() ! ! Check x,y coordinates against toolbar buttons, and set toolbar_pressed to ! the button containing the point. The function handles the horizontal and ! vertical control separators directly, and returns the button clicked, or ! PIC_NONE if none. ! [ toolbar_handle_click x y i button; ! Ignore the call if we have no toolbar. if (toolbar_win == GLK_NULL) return; ! Check coordinates array for a match, stopping either at the array ! end, or on the first empty entry. button = PIC_NONE; for (i = 0: i < TOOLBAR_COORD_ENTRIES && toolbar_coords-->i ~= 0: i = i + 5) { ! Check if this event is inside the saved coordinates. if (x >= toolbar_coords-->(i + 1) && y >= toolbar_coords-->(i + 2) && x <= toolbar_coords-->(i + 3) && y <= toolbar_coords-->(i + 4)) { button = toolbar_coords-->i; break; } } ! Handle separators, control or idle, separately. For control ! separators, return the redraw pseudo-button. switch (button) { PIC_VERT_SEPARATOR: ! Collapse toolbar and request redraw. toolbar_set_collapsed (true); return BUTTON_PSEUDO_REDRAW; PIC_HORIZ_SEPARATOR: ! Restore toolbar and request redraw. toolbar_set_collapsed (false); return BUTTON_PSEUDO_REDRAW; PIC_IDLE_SEPARATOR: return PIC_NONE; } ! See if a button matched the coordinates. if (button ~= PIC_NONE) { ! Note any matched button, and repaint the toolbar. toolbar_pressed = button; toolbar_paint (); ! (Re)set timers to a short timeout, to simulate press. glk_request_timer_events (TOOLBAR_TIMEOUT); toolbar_timeouts = TOOLBAR_TIMEOUT_COUNT; } ! Return the pressed button, or PIC_NONE. return button; ]; ! ! toolbar_is_timing() ! toolbar_handle_timeout() ! ! Return true if timing a button press, and handle timeouts, returning the ! pressed button on the last one, and PIC_NONE otherwise. Timeouts are ! composed of multiple smaller ones to minimize Glk timer jitter. ! [ toolbar_is_timing; return toolbar_timeouts > 0; ]; [ toolbar_handle_timeout button; ! Nothing to do if not timing a button press. if (toolbar_timeouts == 0) return PIC_NONE; ! Decrement timeouts, and return PIC_NONE if not yet zero. toolbar_timeouts--; if (toolbar_timeouts > 0) return PIC_NONE; ! Stop timer and clear pressed button. glk_request_timer_events (0); button = toolbar_pressed; toolbar_pressed = PIC_NONE; ! Repaint, and return the pressed button code. toolbar_paint (); return button; ]; !----------------------------------------------------------------------------- ! Glk event management. !----------------------------------------------------------------------------- ! Book display actions. These are codes that events will translate into, ! representing user commands and gestures, timeouts, and so on. The first ! set of these are directly usable on displayable books; the second set ! are "pseudo" actions -- requests that require additional handling. Constant ACTION_FIRST_PAGE 1; Constant ACTION_LAST_PAGE 2; Constant ACTION_NEXT_PAGE 3; Constant ACTION_PRIOR_PAGE 4; Constant ACTION_NEXT_SHORT 5; Constant ACTION_PRIOR_SHORT 6; Constant ACTION_NUDGE 7; Constant ACTION_REDRAW 8; Constant ACTION_GOTO_PERCENT_10 9; Constant ACTION_GOTO_PERCENT_20 10; Constant ACTION_GOTO_PERCENT_30 11; Constant ACTION_GOTO_PERCENT_40 12; Constant ACTION_GOTO_PERCENT_50 13; Constant ACTION_GOTO_PERCENT_60 14; Constant ACTION_GOTO_PERCENT_70 15; Constant ACTION_GOTO_PERCENT_80 16; Constant ACTION_GOTO_PERCENT_90 17; Constant ACTION_SET_BOOKMARK 18; Constant ACTION_GOTO_BOOKMARK 19; Constant ACTION_UNDO 20; Constant ACTION_NONE 0; Constant ACTION_AUTO_SHORT 21; Constant ACTION_HELP 22; Constant ACTION_LICENSE 23; Constant ACTION_NOTES 24; Constant ACTION_TOGGLE_AUTO 25; Constant ACTION_GUI_NEW_BOOK 26; Constant ACTION_GUI_QUIT 27; Constant ACTION_CHAR_QUIT 28; Constant ACTION_CHAR_UNKNOWN 29; ! Event buffer, initially evtype_None so that a full set of event requests ! is generated on first calls. Array event_buffer --> evtype_None GLK_NULL 0 0; ! ! event_wait() ! ! Wait for a Glk event. The event buffer contains either a previously ! consumed event, or evtype_None. If none, the function issues requests ! for as many events as are possible; otherwise, it reinstates a request ! for the consumed event. It then waits for an event, and returns the ! event buffer. ! [ event_wait ev_type ev_win win wintype; ! Get the event buffer's type and window. ev_type = event_buffer-->0; ev_win = event_buffer-->1; ! See if this is an initial call, or a subsequent one. if (ev_type == evtype_None) { ! On initial call, issue a full set of event requests. win = glk_window_iterate (GLK_NULL, GLK_NULL); while (win ~= GLK_NULL) { ! Issue requests appropriate to this window type. wintype = glk_window_get_type (win); switch (wintype) { wintype_TextBuffer, wintype_TextGrid: ! Request characters, and mouse presses if ! the window will do them. There's a problem ! with char events in grid windows in GlkTerm; ! it won't page modal screens if we issue ! them, so to work round this, we don't... if (wintype == wintype_TextBuffer) glk_request_char_event (win); if (glk_gestalt (gestalt_MouseInput, wintype)) glk_request_mouse_event (win); wintype_Graphics: ! Request mouse events from graphics windows. glk_request_mouse_event (win); } win = glk_window_iterate (win, GLK_NULL); } } else { ! Replace the buffered event with a new request. switch (ev_type) { evtype_CharInput: glk_request_char_event (ev_win); evtype_MouseInput: glk_request_mouse_event (ev_win); } } ! Wait for an event. glk_select (event_buffer); ! If redraw or arrange, redraw the toolbar automatically. ev_type = event_buffer-->0; if (ev_type == evtype_Arrange or evtype_Redraw) toolbar_paint (); return event_buffer; ]; ! ! event_reset() ! ! Clear all event requests created above, and clear the buffer to initial ! values. ! [ event_reset win wintype; ! Clear event requests from each window. win = glk_window_iterate (GLK_NULL, GLK_NULL); while (win ~= GLK_NULL) { ! Issue requests appropriate to this window type. wintype = glk_window_get_type (win); switch (wintype) { wintype_TextBuffer, wintype_TextGrid: ! Cancel character and maybe mouse requests. glk_cancel_char_event (win); if (glk_gestalt (gestalt_MouseInput, wintype)) glk_cancel_mouse_event (win); wintype_Graphics: ! Cancel mouse events on graphics windows. glk_cancel_mouse_event (win); } win = glk_window_iterate (win, GLK_NULL); } ! Clear event buffer to initial values. event_buffer-->0 = evtype_None; event_buffer-->1 = GLK_NULL; event_buffer-->2 = 0; event_buffer-->3 = 0; ]; ! ! event_translate() ! ! Translate Glk events into action codes. Returns the action code for the ! event, ACTION_NONE if no action. ! [ event_translate event ev_type ev_win ev_char wintype x y button; ! Handle specific event types. ev_type = event-->0; switch (ev_type) { evtype_CharInput: ! Extract the event character, and set lowercase. ev_char = event-->2; if (ev_char >= 'A' && ev_char <= 'Z') ev_char = glk_char_to_lower (ev_char); ! Convert event character into an action code. switch (ev_char) { 'q', keycode_Escape: return ACTION_CHAR_QUIT; 'a', keycode_Tab, keycode_Func12: return ACTION_TOGGLE_AUTO; 'j', '+', keycode_Func9: return ACTION_NEXT_SHORT; 'k', '-', keycode_Func8: return ACTION_PRIOR_SHORT; 'n', keycode_PageDown, keycode_Down, keycode_Return, keycode_Func7: return ACTION_NEXT_PAGE; 'p', keycode_PageUp, keycode_Up, keycode_Func6: return ACTION_PRIOR_PAGE; 'h', keycode_Home, keycode_Func10:return ACTION_FIRST_PAGE; 'e', keycode_End, keycode_Func11: return ACTION_LAST_PAGE; '1': return ACTION_GOTO_PERCENT_10; '2': return ACTION_GOTO_PERCENT_20; '3': return ACTION_GOTO_PERCENT_30; '4': return ACTION_GOTO_PERCENT_40; '5': return ACTION_GOTO_PERCENT_50; '6': return ACTION_GOTO_PERCENT_60; '7': return ACTION_GOTO_PERCENT_70; '8': return ACTION_GOTO_PERCENT_80; '9': return ACTION_GOTO_PERCENT_90; ' ': return ACTION_NUDGE; keycode_Left, 'b', keycode_Func4: return ACTION_SET_BOOKMARK; keycode_Right, 'g', keycode_Func5:return ACTION_GOTO_BOOKMARK; keycode_Delete, 'u': return ACTION_UNDO; keycode_Func1, '?', '/': return ACTION_HELP; keycode_Func2, 'l': return ACTION_LICENSE; keycode_Func3, 'i': return ACTION_NOTES; default: return ACTION_CHAR_UNKNOWN; } evtype_MouseInput: ! Get the window type from the event. ev_win = event-->1; wintype = glk_window_get_type (ev_win); ! Differentiate graphics and other mouse events. switch (wintype) { wintype_Graphics: ! Get the x,y coordinates and pass to the toolbar. x = event-->2; y = event-->3; button = toolbar_handle_click (x, y); ! Convert toolbar button into an action code; book- ! affecting buttons are handled here to present a more ! responsive toolbar. switch (button) { PIC_NONE: return ACTION_NONE; BUTTON_FIRST_PAGE: return ACTION_FIRST_PAGE; BUTTON_LAST_PAGE: return ACTION_LAST_PAGE; BUTTON_NEXT_PAGE: return ACTION_NEXT_PAGE; BUTTON_PRIOR_PAGE: return ACTION_PRIOR_PAGE; BUTTON_NEXT_SHORT: return ACTION_NEXT_SHORT; BUTTON_PRIOR_SHORT: return ACTION_PRIOR_SHORT; BUTTON_SET_BOOKMARK: return ACTION_SET_BOOKMARK; BUTTON_GOTO_BOOKMARK: return ACTION_GOTO_BOOKMARK; BUTTON_UNDO: return ACTION_UNDO; BUTTON_PSEUDO_REDRAW: return ACTION_REDRAW; default: return ACTION_NONE; } wintype_TextBuffer, wintype_TextGrid: ! Nudge in the current direction. return ACTION_NUDGE; default: ! Unknown window type. return ACTION_NONE; } evtype_Timer: ! See if the toolbar is busy. if (toolbar_is_timing ()) { ! Handle the toolbar timeout. Because it interacts ! with the timers, we have to toggle timeout after the ! button effect, rather than before, as normal. Others ! are also handled here to improve toolbar visuals. button = toolbar_handle_timeout (); ! Convert button into an action code. switch (button) { PIC_NONE: return ACTION_NONE; BUTTON_TOGGLE_AUTO: return ACTION_TOGGLE_AUTO; BUTTON_HELP: return ACTION_HELP; BUTTON_LICENSE: return ACTION_LICENSE; BUTTON_NOTES: return ACTION_NOTES; BUTTON_NEW_BOOK: return ACTION_GUI_NEW_BOOK; BUTTON_QUIT: return ACTION_GUI_QUIT; default: return ACTION_NONE; } } ! Not a toolbar timeout, so it must be an auto advance. return ACTION_AUTO_SHORT; evtype_Arrange, evtype_Redraw: ! Return redraw action. return ACTION_REDRAW; } ! Unknown event. return ACTION_NONE; ]; !----------------------------------------------------------------------------- ! Displayable book. !----------------------------------------------------------------------------- ! Current book locations and reading direction. Global book_location = 0; Global book_bookmark = 0; Global book_undo = 0; Global book_forward = true; ! ! book_start() ! book_end() ! ! Start and end a displayable book. ! [ book_start; ! Decode expansion is recorded on first record decode; make sure ! it's a representative record, not (say) the last, truncated one. decode_reset (); decode_record (1); ! Initialize location and bookmark from persistent attributes. book_location = bookmark_get_current (); if (book_location == 0) book_location = first_page (); book_bookmark = bookmark_get_marker (); ! Initialize undo and direction. book_undo = 0; book_forward = true; ]; [ book_end; ! Ignore the call if no displayable book open. if (book_location == 0) return; ! Save the current location and any set bookmark. bookmark_save (book_location, book_bookmark); ! Flush the decoder cache, and reset the pager. decode_reset (); page_reset (); ! Reset locations and flags to default values. book_location = 0; book_bookmark = 0; book_undo = 0; book_forward = true; ]; ! ! book_change() ! ! Switch out one open eBook for a different one. Returns true on success, ! false with errno on error. Behaves as open and start if no current ! displayable book is open. ! [ book_change; ! Save the current location and any set bookmark. if (book_location ~= 0) bookmark_save (book_location, book_bookmark); ! Try to open a new eBook (this closes the current one). if (pdb_open_ebook ()) { ! New book opened; reset decoding and paging. decode_reset (); page_reset (); ! Start new displayable book. book_start (); return true; } ! Could not open a different eBook. return false; ]; ! ! book_apply_action() ! ! Apply an action to the current displayable book. Return true on success, ! false with errno on error. ! Array book_winsize --> 2; [ book_apply_action win action_code new_location width height percent; ! Fail the call if no displayable book open. if (book_location == 0) { errno = E_BOOK_NOT_OPEN; return false; } ! Get the main window size. If not gettable (and it's not in ! Xglk -- sigh), default it. glk_window_get_size (win, book_winsize, book_winsize + WORDSIZE); if (book_winsize-->0 == 0 || book_winsize-->1 == 0) { width = DEFAULT_WIDTH; height = DEFAULT_HEIGHT; } else { width = book_winsize-->0; height = book_winsize-->1; } ! Reduce height by three. This is a fudge factor to compensate for ! unpredictability in how Glk wraps lines and words, plus what we ! steal for messages. height = height - 3; ! Apply the given action. new_location = book_location; switch (action_code) { ACTION_REDRAW: ! Reset page prior location cache. page_reset (); ! Redraw the book page to the new width and height. glk_window_clear (win); if (~~page_print (book_location, width, height)) return false; ACTION_NEXT_SHORT, ACTION_PRIOR_SHORT, ACTION_NEXT_PAGE, ACTION_PRIOR_PAGE, ACTION_FIRST_PAGE, ACTION_LAST_PAGE: ! Paginate up or down the book, or start/end. switch (action_code) { ACTION_NEXT_SHORT: new_location = page_next (book_location, width, 2); book_forward = true; ACTION_PRIOR_SHORT: new_location = page_prior (book_location, width, 2); book_forward = false; ACTION_NEXT_PAGE: new_location = page_next (book_location, width, height); book_forward = true; ACTION_PRIOR_PAGE: new_location = page_prior (book_location, width, height); book_forward = false; ACTION_FIRST_PAGE: new_location = first_page (); book_forward = true; ACTION_LAST_PAGE: new_location = last_page (width, height); book_forward = false; } ACTION_GOTO_PERCENT_10 to ACTION_GOTO_PERCENT_90: ! Move 10 to 90 percent into the book. switch (action_code) { ACTION_GOTO_PERCENT_10: percent = 10; ACTION_GOTO_PERCENT_20: percent = 20; ACTION_GOTO_PERCENT_30: percent = 30; ACTION_GOTO_PERCENT_40: percent = 40; ACTION_GOTO_PERCENT_50: percent = 50; ACTION_GOTO_PERCENT_60: percent = 60; ACTION_GOTO_PERCENT_70: percent = 70; ACTION_GOTO_PERCENT_80: percent = 80; ACTION_GOTO_PERCENT_90: percent = 90; } new_location = percent_location (percent); if (new_location == -1) return false; new_location = page_adjust (new_location, true, width); ! Invalidate cached prior page. page_reset (); ACTION_NUDGE: ! Advance in the current direction. if (book_forward) new_location = page_next (book_location, width, height); else new_location = page_prior (book_location, width, height); ACTION_SET_BOOKMARK: ! Set bookmark to this location, except if already set to ! this location, in which case, clear it. if (book_bookmark ~= 0 && book_bookmark == book_location) book_bookmark = 0; else book_bookmark = book_location; ACTION_GOTO_BOOKMARK: ! Go to bookmark location, if valid, and invalidate cached ! prior page. if (book_bookmark ~= 0) { new_location = book_bookmark; page_reset (); } ACTION_UNDO: ! Go to undo location, if valid, and invalidate cached ! prior page. if (book_undo ~= 0) { new_location = book_undo; page_reset (); } default: ! Invalid book action. errno = E_BOOK_BAD_ACTION; return false; } ! If anything above failed on location, return false. if (new_location == -1) return false; ! See if the location changed. if (new_location ~= book_location) { ! Save location as undo, unless this was an undo, in which ! case drop it. if (action_code == ACTION_UNDO) book_undo = 0; else book_undo = book_location; ! Update current location to the new one. book_location = new_location; ! Repaint the page. glk_window_clear (win); if (~~page_print (book_location, width, height)) return false; } ! Action applied successfully. return true; ]; ! ! book_show_progress() ! ! Display book progress in the title window. Shows percentages, direction, ! and optionally an auto flag, in the upper window, if present. Returns ! true on success, false with errno if no displayable book opened. ! [ book_show_progress titlewin auto stream width percent; ! Ignore if no upper window. if (titlewin == GLK_NULL) return true; ! Fail the call if no displayable book open. if (book_location == 0) { errno = E_BOOK_NOT_OPEN; return false; } ! Clear window and set output stream. glk_window_clear (titlewin); stream = glk_stream_get_current (); glk_set_window (titlewin); ! Print the book title, or the program name if none. glk_window_move_cursor (titlewin, 1, 0); if (pdb_title_length > 0) glk_put_buffer (pdb_title, pdb_title_length); else print (string) Story; ! Find the window's width, default if not gettable. glk_window_get_size (titlewin, book_winsize, GLK_NULL); if (book_winsize-->0 == 0) width = DEFAULT_WIDTH; else width = book_winsize-->0; ! Display the bookmark percent, if given. glk_window_move_cursor (titlewin, width - 16, 0); print "| "; if (book_bookmark ~= 0) { percent = location_percent (book_bookmark); if (percent >= 100) percent = 99; else if (percent < 10) print " "; print percent; print "%"; } else print " - "; ! Display the percent at window right. glk_window_move_cursor (titlewin, width - 10, 0); print "| "; percent = location_percent (book_location); if (percent >= 100) percent = 99; else if (percent < 10) print " "; print percent; print "% | "; ! Add direction, or '>' auto advance flag. if (auto) print (char) '>'; else if (book_forward) print (char) 'v'; else print (char) '^'; ! Restore original output stream. glk_stream_set_current (stream); return true; ]; !----------------------------------------------------------------------------- ! Help and other general text. !----------------------------------------------------------------------------- ! ! emphasized() ! preformatted() ! ! Shorthand helper functions for printing strings in explicit Glk styles. ! [ emphasized str; glk_set_style (style_Emphasized); print (string) str; glk_set_style (style_Normal); ]; [ preformatted str; glk_set_style (style_Preformatted); print (string) str; glk_set_style (style_Normal); ]; ! ! keyable() ! clickable() ! ! Shorthand helpers for testing Glk character and mouse input capabilities. ! [ keyable char; return glk_gestalt (gestalt_CharInput, char); ]; [ clickable win wintype; wintype = glk_window_get_type (win); return glk_gestalt (gestalt_MouseInput, wintype); ]; ! ! print_help() ! ! Display a short help screen, using a lot of code to do it. ! [ print_help win titlewin toolbarwin; print "^", (emphasized) Story, " navigates its way around the book that you are reading according to commands that you give it using "; if (toolbarwin ~= GLK_NULL) print "either the GUI toolbar or "; print "the keyboard. It understands these keyboard commands:^^"; print (preformatted) " Next page - ", "N"; if (keyable (keycode_PageDown)) print ", PageDown"; if (keyable (keycode_Down)) print ", DownArrow"; if (keyable (keycode_Return)) print ", Return"; if (keyable (keycode_Func7)) print ", FKey-7"; print "^"; print (preformatted) " Previous page - ", "P"; if (keyable (keycode_PageUp)) print ", PageUp"; if (keyable (keycode_Up)) print ", UpArrow"; if (keyable (keycode_Func6)) print ", FKey-6"; print "^"; print (preformatted) " Next/Previous page - "; print "Space bar"; if (clickable (win)) print ", Mouse click on book"; else if (titlewin ~= GLK_NULL) { if (clickable (titlewin)) print ", Mouse click on book title"; } print "^"; print (preformatted) " First page - ", "H"; if (keyable (keycode_Home)) print ", Home"; if (keyable (keycode_Func10)) print ", FKey-10"; print "^"; print (preformatted) " Last page - ", "E"; if (keyable (keycode_End)) print ", End"; if (keyable (keycode_Func11)) print ", FKey-11"; print "^"; print (preformatted) " Go to book percent - "; print "1 to 9, representing 10% to 90%"; print "^"; print (preformatted) " Set bookmark - ", "B"; if (keyable (keycode_Left)) print ", LeftArrow"; if (keyable (keycode_Func4)) print ", FKey-4"; print "^"; print (preformatted) " Go to bookmark - ", "G"; if (keyable (keycode_Right)) print ", RightArrow"; if (keyable (keycode_Func5)) print ", FKey-5"; print "^"; print (preformatted) " Next one/two lines - ", "+, J"; if (keyable (keycode_Func9)) print ", FKey-9"; print "^"; print (preformatted) " Previous one/two lines - ", "-, K"; if (keyable (keycode_Func8)) print ", FKey-8"; print "^"; print (preformatted) " Undo last action - ", "U"; if (keyable (keycode_Delete)) print ", Delete"; print "^"; if (glk_gestalt (gestalt_Timer, 0)) { print (preformatted) " Automatic advance - ", "A"; if (keyable (keycode_Tab)) print ", Tab"; if (keyable (keycode_Func12)) print ", FKey-12"; print "^"; } print (preformatted) " Close book - ", "Q"; if (keyable (keycode_Escape)) print ", Escape"; print "^"; print (preformatted) " Display help screen - ", "?"; if (keyable (keycode_Func1)) print ", FKey-1"; print "^"; print (preformatted) " Display license screen - ", "L"; if (keyable (keycode_Func2)) print ", FKey-2"; print "^"; print (preformatted) " Display notes screen - ", "I"; if (keyable (keycode_Func3)) print ", FKey-3"; print "^^"; print "Next/previous page, the Space bar, will move to either the next or the previous page, depending on which direction you are currently reading.^^"; if (glk_gestalt (gestalt_Timer, 0)) { print "Automatic advance moves on a line or two every few seconds. Use its command to toggle it on and off. You can also cancel it by pressing the Space bar. It stops by itself at the end of the book, or if you use a ", (emphasized) Story, " command.^^"; } if (toolbarwin ~= GLK_NULL) { print "As well as tools for most of the above commands, the GUI toolbar offers a tool to close the current eBook and begin reading a different one. You can hide the toolbar by clicking on the arrowed separator icon at its extreme left edge.^^"; } if (titlewin ~= GLK_NULL) { print "The title window shows the name of the book, and how far through it you have read, as a percentage. You can set a bookmark, and once set, it too appears in the title window. An up/down arrow at the far right of the title window shows the current reading direction."; if (glk_gestalt (gestalt_Timer, 0)) { print " A '>' replaces the up/down arrow if automatic advance is on."; } print "^^"; } print "When you close an eBook, ", (emphasized) Story, " saves your current reading position for that eBook, and any bookmark, in a file called ", (preformatted) "GLKEBOOK.BMK", ". When loading in an eBook, ", (emphasized) Story, " restores the current position and any bookmark for the eBook from this file.^^"; print "You can find a large selection of ready-made PalmOS eBook titles, many free of charge, at ", (emphasized) "www.memoware.com", ".^^"; print "eBook pdb files for the Palm hold books in one of several formats -- DOC, TomeRaider, Plucker, iSilo, and so on, and all have the file extension '.pdb'. Of these formats, DOC (not to be confused with Microsoft Word data files) and Plucker are non-proprietary. The eBook format that ", (emphasized) Story, " reads is DOC. This format is commonly used for eBooks not under copyright.^^"; print "Tools such as ", (emphasized) "bibelot", " and ", (emphasized) "txt2pdbdoc", " convert text files into DOC format pdb files. In particular, ", (emphasized) "bibelot", " has special features for handling Project Gutenberg Etexts.^^"; print "Please report ", (emphasized) Story, " bugs, errors or misfeatures to ", (emphasized) "simon_baldwin@@64yahoo.com", ".^^"; print "[Please press SPACE to continue...]^"; ]; ! ! print_license() ! ! Display a licensing screen. ! [ print_license; print "^Copyright (C) 2003 Simon Baldwin (", (emphasized) "simon_baldwin@@64yahoo.com", ")"; print "^^This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.^^"; print "This program is distributed in the hope that it will be useful, but ", (emphasized) "WITHOUT ANY WARRANTY", "; without even the implied warranty of ", (emphasized) "MERCHANTABILITY", " or ", (emphasized) "FITNESS FOR A PARTICULAR PURPOSE", ". See the GNU General Public License for more details.^^"; print "You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.^^"; print "[Please press SPACE to continue...]^"; ]; ! ! print_notes() ! ! Display a few notes about the program. ! [ print_notes; print "^", (emphasized) Story, " tries to tailor its behavior to the Glk library it is running with, but second guessing Glk's text paging is a science that is far from exact. Here are some notes about selected Glk libraries:^^"; glk_set_style (style_BlockQuote); print "Xglk - ", (string) Story, " behaves moderately well with this Glk library, given the major limitation that it always returns zero when asked for its main text window dimensions. For this case, ", (string) Story, " defaults the window dimensions to 72 columns and 40 lines, which seems to allow it to run reasonably well, though it may be necessary to resize the Glulxe frame for comfortable reading. Page Up and Page down, and other normal paging keys, work, as does clicking the mouse on ", (string) Story, "'s title bar. When asked, Xglk denies that it recognizes the function keys, though in fact it does. Xglk displays the full ", (string) Story, " GUI toolbar."; glk_set_style (style_Normal); print "^^"; glk_set_style (style_BlockQuote); print "Glkterm - ", (string) Story, " behaves well with this Glk library, though its visual appearance is, understandably, rather bland. Glkterm uses Page Up and Page Down, Home, and End, itself, so the Up and Down arrows are the best ways to page through a book with this Glk library. This Glk library does not show the GUI toolbar."; glk_set_style (style_Normal); print "^^"; glk_set_style (style_BlockQuote); print "Cheapglk - ", (string) Story," does not look at its best with this Glk library, and you should probably avoid it. Cheapglk's inability to read a single character rather than a line, no windowing, and no Glk timers make this a poor choice of Glk library for reading eBooks."; glk_set_style (style_Normal); print "^^"; glk_set_style (style_BlockQuote); print "Winglk - Perhaps the best Glk library for ", (string) Story, ", which behaves very well when used in Winglulxe. All of the paging keys and mouse presses work, as do function keys and the GUI toolbar, and ", (string) Story, " is able to adjust itself for the Winglk window size, resulting in an attractive display. There is one possible strange behavior on start up, where Winglulxe may display the file selection dialog for an eBook, without displaying the main screen describing why the program is trying to open a file; ", (string) Story, " takes steps to try to avoid this happening."; glk_set_style (style_Normal); print "^^"; glk_set_style (style_BlockQuote); print "Macglk - Untested with ", (string) Story, ", but should work fine, in theory."; glk_set_style (style_Normal); print "^^"; print (emphasized) Story, " has no particular limitations on the size of eBook you can read with it, though it will be upset if the pdb file you load contains more than 32,767 records (the Bible eBook contains under 2,000, so you should be safe with even the largest of eBooks). It also works with non-English eBooks, provided they are in one of the Latin-1 languages.^^"; print "Pdb files having a record size larger than 4,096 bytes will cause problems, though I've seen none that are built this way (and I'm not sure it's valid to do this). Pdb records that decode to more than 8,192 characters will also cause problems, but again, I've not seen any that approach this limit.^^"; print "The DOC file format is not well specified, and as a result several DOC encoding tools take a lax view of setting up data fields in their pdb output files. ", (emphasized) Story, " tries to cope as best it can.^^"; print (emphasized) Story, " tries not to break words in the middle, but will readily break lines in half in order to try to fit a page of a book into the display window. It might be better if it could avoid doing this.^^"; print "Of the tools available for creating DOC format pdb files, ", (emphasized) "bibelot", " seems to do the best job of formatting text for handheld devices.^^"; print "Some DOC format pdb eBooks contain predefined bookmarks, as well as book text. ", (emphasized) Story, " ought to be able to understand and offer these bookmarks, but at present, it does not.^^"; print "None of the DOC format pdb records offers a safe way to identify an eBook, so when saving bookmarks in ", (preformatted) "GLKEBOOK.BMK", ", ", (emphasized) Story, " generates a CRC-32 from the eBook's pdb header, its record 0, and the first book text record. This should be reasonably unique, but there is the danger, though somewhat remote, of CRC-32 clashes. If a clash occurs, the worst that happens is that ", (emphasized) Story, " restores an incorrect current reading position and bookmark.^^"; print (emphasized) Story, " handles DOC compression types 1 and 2. If it sees type 1,026, it treats it as 2, though I've no real idea what type 1,026 really means.^^"; print "Using percentages rather than page numbers as a measure of progress through a book seems odd, but is useful when the actual uncompressed size of the text isn't known, and when the page size is variable. In theory, the DOC format contains the uncompressed text size in a header field, but many DOC file encoders don't fill it in, so it's unreliable. The alternative is to decompress the complete book at start up, but on all but small books this is too slow to be usable.^^"; print (emphasized) Story, " assumes, when working with percentages, that all the records in the eBook decompress to about the same size. They normally do, but should a pdb file contain records that vary wildly in size, some of the percentages may be inaccurate, perhaps very inaccurate.^^"; print "Pdb DOC files are often (inaccurately) named with a .prc extension. If you come across one of these, ", (emphasized) Story, " will load it as if it were a standard .pdb file; the filename's extension is immaterial.^^"; print "Have I mentioned that ", (emphasized) "www.memoware.com", " offers a large collection of free DOC format eBooks?^^"; print "This is ", (emphasized) Story, " release ", 52->0 * 256 + 53->0, ", serial number "; glk_put_buffer (54, 6); print ". Please report bugs, errors, or misfeatures to ", (emphasized) "simon_baldwin@@64yahoo.com", ".^^"; print "[Please press SPACE to continue...]^"; ]; !----------------------------------------------------------------------------- ! Bottom line messaging and modal display. !----------------------------------------------------------------------------- ! Message display window, one line at display base, and note of the current ! message for repaints. Global message_window = GLK_NULL; Global message_current = 0; ! ! message_paint() ! ! Repaint any current message window. ! [ message_paint stream; ! Ignore the call if no displayed message window. if (message_window == GLK_NULL) return; ! Clear window and set output stream. glk_window_clear (message_window); stream = glk_stream_get_current (); glk_set_window (message_window); ! Print the message, and restore the output stream. print " ", (string) message_current; glk_stream_set_current (stream); ]; ! ! message_show() ! ! Display a message line, or close the message window if message is null. ! [ message_show win _message; ! Set this message as the current message. message_current = _message; ! If message is null, close the window and return. if (message_current == 0) { if (message_window ~= GLK_NULL) { glk_window_close (message_window, GLK_NULL); message_window = GLK_NULL; } return; } ! If the window isn't open, try to open it. If it fails to open, ! just use the main window, with an attempt at styling. if (message_window == GLK_NULL) { message_window = glk_window_open (win, winmethod_Below|winmethod_Fixed, 1, wintype_TextGrid, 0); if (message_window == GLK_NULL) { print "^^ "; glk_set_style (style_Emphasized); print "*** ", (string) message_current, " ***"; glk_set_style (style_Normal); print "^^"; return; } } ! Print the message to the message window. message_paint (); ]; ! ! modal_show_title() ! ! Display a static title bar. Title is a string array for the right hand ! side of the title bar; the left takes the story name. ! Array display_winsize --> 1; [ modal_show_title titlewin title stream width; ! Ignore if no upper window. if (titlewin == GLK_NULL) return; ! Clear window and set output stream. glk_window_clear (titlewin); stream = glk_stream_get_current (); glk_set_window (titlewin); ! Print the program name at window left. glk_window_move_cursor (titlewin, 1, 0); print (string) Story; ! If no right hand side title, restore original output stream ! and return. if (title == 0) { glk_stream_set_current (stream); return; } ! Find the window's width, default if not gettable. glk_window_get_size (titlewin, display_winsize, GLK_NULL); if (display_winsize-->0 == 0) width = DEFAULT_WIDTH; else width = display_winsize-->0; ! Display the title string at title window right edge. glk_window_move_cursor (titlewin, width - title->0 - 1, 0); glk_put_buffer (title + 1, title->0); ! Restore original output stream. glk_stream_set_current (stream); ]; ! ! modal_keypress_wait() ! ! Helper for modal information screens; wait for a keypress. Title is a ! string for the title bar right. ! [ modal_keypress_wait titlewin title event ev_type; ! Temporarily hide the toolbar. toolbar_set_hidden (true); ! Title paint, then wait for a character before proceeding. modal_show_title (titlewin, title); do { ! Wait for an event, extract type. event = event_wait (); ev_type = event-->0; ! On redraw or arrange events, repaint necessary windows. ! Ignore all other request types. switch (ev_type) { evtype_Arrange, evtype_Redraw: modal_show_title (titlewin, title); message_paint (); } } until (ev_type == evtype_CharInput); ! Reinstate any hidden toolbar. toolbar_set_hidden (false); ]; !----------------------------------------------------------------------------- ! Display management. !----------------------------------------------------------------------------- ! Book display messages and constants. Constant HINT_STRING "Please press '?' for a list of GlkeBook commands"; Constant CHAR_QUIT_STRING "Please select a new eBook, or cancel to quit GlkeBook..."; Constant GUI_QUIT_STRING "Quit GlkeBook? [y/n]"; Constant GUI_NEW_BOOK_STRING "Please select a new eBook..."; Constant AUTO_TIMEOUT 3000; ! Modal display screen title strings. Array HELP_STRING string "Help"; Array LICENSE_STRING string "License"; Array NOTES_STRING string "Notes and Limitations"; ! Flush wait timeout period and counter limit. Constant FLUSH_TIMEOUT 250; Constant FLUSH_TIMEOUT_COUNT 4; ! Pseudo-actions, passed to apply action. Constant DISPLAY_CLEAR_AUTO -1; Constant DISPLAY_CLEAR_MESSAGE -2; ! Display windows, main and title top line. Global display_mainwin = GLK_NULL; Global display_titlewin = GLK_NULL; ! Automatic advance flag, bottom line message, running flag. Global display_auto = false; Global display_message = 0; Global display_running = true; ! ! display_initialize() ! ! Initialize the two main display windows. ! [ display_initialize; ! Set Glk as the i/o system. @setiosys 2 0; ! Open the main window, and set it. If it fails, quit. display_mainwin = glk_window_open (GLK_NULL, 0, 0, wintype_TextBuffer, 0); if (display_mainwin == GLK_NULL) quit; glk_set_window (display_mainwin); ! Create a toolbar at the top of the main window -- this may fail. toolbar_create (display_mainwin); ! Try for a title window -- this may fail. display_titlewin = glk_window_open (display_mainwin, winmethod_Above|winmethod_Fixed, 1, wintype_TextGrid, 0); ]; ! ! display_flush_wait() ! ! Glkebook tends to display an intro screen, then go straight into a fileref ! selection. Some Glk libraries (Winglk) behave oddly with this, taking the ! literal interpretation of not displaying main window contents until a ! glk_select() call. The result is a "floating" file selection dialog on ! Glkebook startup -- no explanations or intro screen. ! ! To try to improve things, this function sits in a brief glk_select() loop ! waiting for a timer event. This should flush any pending display output. ! Call before anything that calls glk_fileref_create_by_prompt(). ! [ display_flush_wait timeouts event ev_type; ! If we don't have timers, do nothing. if (~~glk_gestalt (gestalt_Timer, 0)) return; ! Start timers with the flush timeout period. glk_request_timer_events (FLUSH_TIMEOUT); timeouts = FLUSH_TIMEOUT_COUNT; ! Wait with glk_select() until enough timeouts received. while (timeouts > 0) { ! Wait for an event, extract type. event = event_wait (); ev_type = event-->0; ! On redraw or arrange events, repaint necessary windows; ! we have to paint the title window different ways depending ! on where we were called from. On timeout, decrement the ! count of timeouts received. Ignore other request types. switch (ev_type) { evtype_Arrange, evtype_Redraw: ! Redraw size-sensitive windows; if no book progress ! to update, fall back to modal title display. if (~~book_show_progress (display_titlewin, display_auto)) modal_show_title (display_titlewin, 0); message_paint (); evtype_Timer: ! Decrement count of timeouts. timeouts--; } } ! Turn off timers. glk_request_timer_events (0); ]; ! ! display_apply_action() ! ! Apply an action to the display. Return true on success, false with errno ! on error. ! [ display_apply_action action_code new_message new_auto status; ! Default new auto and message; we look for changes later. new_auto = false; new_message = 0; ! Apply the given action. switch (action_code) { DISPLAY_CLEAR_AUTO: ! Pseudo-action to clear auto only -- restore message. new_message = display_message; DISPLAY_CLEAR_MESSAGE: ! Pseudo-action to clear message only -- restore auto. new_auto = display_auto; ACTION_CHAR_UNKNOWN: ! Invalid command. new_message = HINT_STRING; ACTION_GUI_NEW_BOOK: ! Open a new book in place of the current one. message_show (display_mainwin, GUI_NEW_BOOK_STRING); display_flush_wait (); status = book_change (); message_show (display_mainwin, display_message); ! Compensate for the damage of glk_fileref_create. event_reset (); toolbar_paint (); ! If book switched, update display, else report error. if (status) { if (~~book_apply_action (display_mainwin, ACTION_REDRAW)) return false; } else new_message = strerror (errno); ACTION_CHAR_QUIT: ! Try to open a new book in place of the current one; if ! the fileref is canceled, quit. message_show (display_mainwin, CHAR_QUIT_STRING); display_flush_wait (); status = book_change (); message_show (display_mainwin, display_message); ! Compensate for the damage of glk_fileref_create. event_reset (); toolbar_paint (); ! If book switched, update display, else report error. if (status) { if (~~book_apply_action (display_mainwin, ACTION_REDRAW)) return false; } else { if (errno == E_PDB_BAD_REF) display_running = false; else new_message = strerror (errno); } ACTION_TOGGLE_AUTO: ! Toggle auto advance if we have timers. if (glk_gestalt (gestalt_Timer, 0)) new_auto = ~~display_auto; ACTION_AUTO_SHORT: ! Move forward a couple of lines, leaving auto on. if (~~book_apply_action (display_mainwin, ACTION_NEXT_SHORT)) return false; new_auto = display_auto; ACTION_HELP, ACTION_LICENSE, ACTION_NOTES: ! Clear any message immediately. message_show (display_mainwin, 0); ! Display modal information screen. glk_window_clear (display_mainwin); switch (action_code) { ACTION_HELP: print_help (display_mainwin, display_titlewin, toolbar_win); modal_keypress_wait (display_titlewin, HELP_STRING); ACTION_LICENSE: print_license (); modal_keypress_wait (display_titlewin, LICENSE_STRING); ACTION_NOTES: print_notes (); modal_keypress_wait (display_titlewin, NOTES_STRING); } ! Redraw the current displayable book page. if (~~book_apply_action (display_mainwin, ACTION_REDRAW)) return false; default: ! Invalid display action. errno = E_DISPLAY_BAD_ACTION; return false; } ! If the message changed, update it. if (new_message ~= display_message) { display_message = new_message; message_show (display_mainwin, display_message); } ! Set timers to reflect any new auto mode. We have to be careful ! not to conflict with the toolbar over timer events. if (new_auto ~= display_auto) { display_auto = new_auto; if (display_auto) glk_request_timer_events (AUTO_TIMEOUT); else { if (~~toolbar_is_timing ()) glk_request_timer_events (0); } } ! Action applied successfully. return true; ]; ! ! display_confirm_quit() ! ! Wait for a yes/no confirmation to quit. Return true on yes, false on no. ! [ display_confirm_quit event ev_type ev_char; ! Wait until confirmation. message_show (display_mainwin, GUI_QUIT_STRING); do { ! Wait for an event, extract type and possible character. event = event_wait (); ev_type = event-->0; ev_char = event-->2; ! On redraw or arrange events, repaint necessary windows. ! Ignore all other request types. switch (ev_type) { evtype_Arrange, evtype_Redraw: book_show_progress (display_titlewin, display_auto); message_paint (); } } until (ev_type == evtype_CharInput && ev_char == 'y' or 'Y' or 'n' or 'N'); ! Restore original message, return true if confirmed. message_show (display_mainwin, display_message); return ev_char == 'y' or 'Y'; ]; ! ! display_main_loop() ! ! Display an open eBook page by page. The concept of page is a looseish ! one as it's not possible to second guess Glk pagination accurately. ! [ display_main_loop event action_code; ! Initialize auto and displayed message. display_auto = false; display_message = 0; ! Start displayable book, update toolbar. book_start (); if (~~book_apply_action (display_mainwin, ACTION_REDRAW)) fatal (); toolbar_paint (); ! Keep displaying pages until asked to stop. display_running = true; while (display_running) { ! Update the book progress window, including auto flag, ! and repaint any displayed message line. book_show_progress (display_titlewin, display_auto); message_paint (); ! Get next Glk event, and convert to an action code. event = event_wait (); action_code = event_translate (event); ! Handle nudge specially while in auto mode -- just turn off ! auto mode. if (display_auto && action_code == ACTION_NUDGE) { if (~~display_apply_action (DISPLAY_CLEAR_AUTO)) fatal (); continue; } ! Special case GUI quit, to which we apply confirmation. if (action_code == ACTION_GUI_QUIT) { ! Confirm, and clear running flag if confirmed. if (display_confirm_quit ()) display_running = false; ! Restart auto -- timer is stopped by toolbar. if (display_running && display_auto) { if (~~display_apply_action (DISPLAY_CLEAR_AUTO)) fatal (); if (~~display_apply_action (ACTION_TOGGLE_AUTO)) fatal (); } continue; } ! Try this action code as a display action. if (display_apply_action (action_code)) continue; else { if (errno ~= E_DISPLAY_BAD_ACTION) fatal (); } ! Not a display action; try as a direct book action. if (book_apply_action (display_mainwin, action_code)) { ! Except on redraw, cancel any auto advance, and ! clear message; continue. if (action_code ~= ACTION_REDRAW) { if (~~display_apply_action (DISPLAY_CLEAR_AUTO)) fatal (); if (~~display_apply_action (DISPLAY_CLEAR_MESSAGE)) fatal (); } continue; } else { if (errno ~= E_BOOK_BAD_ACTION) fatal (); } } ! Clear any message, and ensure timers are turned off. if (~~display_apply_action (DISPLAY_CLEAR_AUTO)) fatal (); if (~~display_apply_action (DISPLAY_CLEAR_MESSAGE)) fatal (); ! Close the displayable book, cancel any pending event requests. book_end (); event_reset (); ]; !----------------------------------------------------------------------------- ! Introduction and main program. !----------------------------------------------------------------------------- ! ! introduction() ! ! Display the introductory screen. ! [ introduction win; glk_window_clear (win); glk_set_style (style_Header); print "^", (string) Story; glk_set_style (style_Normal); print (string) Headline, "Release ", 52->0 * 256 + 53->0, " / Serial number "; glk_put_buffer (54, 6); print " / Inform v"; glk_put_buffer (44, 4); print "(G"; glk_put_buffer (48, 4); print ")^^"; print (emphasized) Story, " reads eBooks distributed as DOC format pdb files for PalmOS handheld devices.^^"; print "A large selection of ready-made PalmOS eBook titles is available, many free of charge, at ", (emphasized) "www.memoware.com", ".^^"; print "eBook pdb files for the Palm hold books in one of several formats -- DOC, TomeRaider, Plucker, iSilo, and so on, and all have the file extension '.pdb'. Of these formats, DOC (not to be confused with Microsoft Word data files) and Plucker are non-proprietary. The eBook format that ", (emphasized) Story, " reads is DOC. This format is commonly used for eBooks not under copyright.^^"; print "Tools such as ", (emphasized) "bibelot", " and ", (emphasized) "txt2pdbdoc", " convert text files into DOC format pdb files. In particular, ", (emphasized) "bibelot", " has special features for handling Project Gutenberg Etexts.^^"; print "Please select an eBook to read..."; ]; ! ! main() ! ! Main program entry point. Set up Glk i/o, open windows, and display the ! introduction screen. Open an eBook, and if we can get one, display it. ! [ main opened; ! Set up Glk i/o and open all windows. display_initialize (); ! Display the introductory screen. introduction (display_mainwin); modal_show_title (display_titlewin, 0); ! Hide the toolbar until needed. toolbar_set_hidden (true); ! Try to open books until one opens, or the fileref is canceled ! by the user. do { ! Synchronize display, then try to open a book. display_flush_wait (); opened = pdb_open_ebook (); ! Open failed - unless invalid fileref, indicate why. if (~~opened) { if (errno ~= E_PDB_BAD_REF) message_show (display_mainwin, strerror (errno)); } } until (opened || errno == E_PDB_BAD_REF); ! If we open a book successfully, display it, and any others. if (opened) { ! Clear any current message, and restore toolbar. message_show (display_mainwin, 0); event_reset (); toolbar_set_hidden (false); ! Display the book, and close the last one when done. display_main_loop (); pdb_close_ebook (); } ];