Creating Business Applications With REBOL
By: Nick Antonaccio Updated: 12-16-2015 Learn to solve common business data management problems with a versatile development tool that's simple enough for "non-programmers". ** It's recommended that you first read http://re-bol.com/rebol_quick_start.html and try the app builder at http://withoutwritingcode.com for a quick introduction to Rebol coding. Then return to read this text for a more complete look at all of Rebol's capabilities. Also, be sure to see the short examples at http://re-bol.com/short_rebol_examples.r for a fast and interesting overview of Rebol's simple/productive coding style. See http://re-bol.com/examples.txt for many more Rebol code examples. Go to http://rebolforum.com to ask questions. See also 68 YouTube video tutorials about REBOL (10 hours of video). The previous introduction to this text has been removed. It's available here and in a shorter version here A slideshow presentation covering the previous introduction is available here.
Contents:
1. A Crash Course Introduction to REBOL
1.1 Installing and Running Programs
1.2 Opening REBOL Directly to the Console
1.3 Some Short Code Examples to Whet Your Appetite
1.4 Basics of REBOL Coding
1.5 Conditional Evaluations
1.6 Some More Useful Functions
2. Lists, Tables, and the "Foreach" Function
2.1 Managing Spreadsheet-Like Data
2.2 Some Simple List Algorithms (Count, Sum, Average, Max/Min)
2.3 Searching
2.4 Gathering Data, and the "Copy" Function
2.5 List Comparison Functions
2.6 Creating Lists From User Input
2.7 Three Useful Data Storage Programs: Inventory, Contacts, Schedule
2.8 Working With Tables of Data: Columns and Rows
2.9 Additional List/Block/Series Functions and Techniques
2.10 Sorting Lists and Tables of Data
2.11 CSV Files and the "Parse" Function
2.12 Two Paypal Report Programs, Analyzed
2.13 Some Perspective about Studying These Topics
3. Using GUI Windows and Widgets to Input and Display Data
3.1 Basic Layout Guidelines and Widgets
3.2 Breathing Life Into GUI Programs - Performing Actions
3.3 GUI Language Reference
3.4 A Telling Comparison
4. Quick Review and Clarification
5. SOME COMPLETE GUI APPLICATION EXAMPLES
5.1 Generic Text Field Saver
5.2 Calculator
5.3 File Editor
5.4 Web Page Editor
5.5 Inventory List Creator
5.6 Inventory Sorter and Viewer
5.7 Contacts Viewer
5.8 Minimal Retail Cash Register and Sales Report System
5.9 Email
5.10 Scheduler
5.11 Parts Database
5.12 Time Clock and Payroll Report
5.13 Blogger
5.14 FTP Group Chat
5.15 Group Reminder
5.16 A Univeral Report Generator, For Paypal and any other CSV Table Data
5.17 Reviewing and Using the Code You've Seen To Model New Applications
6. User Defined Functions and Imported Code Modules
6.1 "Do", "Does", and "Func"
6.2 Return Values
6.3 Scope
6.4 Function Documentation
6.5 Doing Imported Code
6.6 Separating Form and Function in GUIs - The Check Writer App
6.7 A Full Featured Group Note Sharing App
7. A Few Useful Data Visualization Tools
7.1 Displaying and Sorting Data Using Spreadsheet-Like GUI Grids
7.2 Creating Graphs, Plots, and Charts with "Q-Plot"
7.3 Drawing Charts Using Raw GUI Code
7.4 Creating 3D Graphs With r3D
7.5 Using the Google Chart API
7.6 Using the "Nano-Sheets" Spreadsheet App
8. Using REBOL to Create Presentations
8.1 REBOL as Presentation Software
8.2 Some Basic Layout Ideas and a Simple Code Framework for Presentations
8.3 Using Tab Panels and Menus to Present Information
8.4 Show.r - A Useful Line-By-Line Presentation System
8.5 Creating "Screen Shot" Images of GUIs
8.6 Embedding Binary Resources (images, sounds, files etc.) in Code
8.7 Playing Sounds
8.8 Launching Code in Separate Processes
8.9 Running Command Line Applications
8.10 Creating Simple Animations
8.11 A Simple Animation Framework for Presentations
8.12 Using Animated GIF Images
8.13 And That's Just the Beginning
9. Makedoc And Other Useful REBOL Productivity Tools
9.1 Makedoc.r - HTML Document Builder
9.2 An Improved Text Editor
9.3 GUI Builders and Learning Tools
10. Real World Concerns and Examples: Why "Programming" > Office Software
10.1 An Expanded Inventory Program
10.2 Receipt Printer
10.3 Advanced Time Clock and Automated Payroll Reports
11. More REBOL Language Fundamentals
11.1 Comments
11.2 Function Refinements
11.3 White Space and Indentation
11.4 Multi Line Strings, Quotes, and Concatenation
11.5 More About Variables
11.6 Data Types
11.7 Random Values
11.8 More About Reading, Writing, Loading, and Saving to and from Varied Sources
11.9 Understanding Return Values and the Order of Evaluation
11.10 More About Conditional Evaluations
11.11 More About Loops
11.12 More About Why/How Blocks are Useful
11.13 REBOL Strings
12. More Essential Topics
12.1 Built-In Help and Online Resources
12.2 Saving and Running REBOL Scripts
12.3 "Compiling" REBOL Programs - Distributing Packaged .EXE Files
12.4 Common REBOL Errors, and How to Fix Them
13. Creating Web Applications using REBOL CGI
13.1 An HTML Crash Course
13.2 A Standard CGI Template to Memorize
14. Example CGI Applications
14.1 Form Mail
14.2 A Generic Drop Down List Application
14.3 Photo Album
14.4 Simple Interactive REBOL Web Site Console
14.5 Attendance
14.6 Bulletin Board
14.7 GET vs POST Example
14.8 Group Note System
14.9 Generic Form Handler
14.10 File Uploader
14.11 File Downloader
14.12 A Complete Web Server Management Application
14.13 The RebolForum.com CGI Code
14.14 Etsy Account Manager
14.15 A Note About Working With Web Servers
14.16 WAP - Cell Phone Browser CGI Apps (deprecated)
15. Organizing Efficient Data Structures and Algorithms
15.1 A Simple Loop Example
15.2 A Real Life Example: Checkout Register and Cashier Report System
16. Additional Topics
16.1 Objects
16.2 Ports - Fine Grained Access to Files, Email, Network and More
16.3 Console and CGI Email Apps Using Ports
16.4 Network Ports - Transferring Data and Files with HTTP
16.5 Transferring Binary Files Through TCP Network Sockets
16.6 Transferring Data Through UDP Network Ports
16.7 Parse (REBOL's Answer to Regular Expressions)
16.8 Using Parse to Load Speadsheet CSV Files and Other Structured Data
16.9 Using Parse's Pattern Matching Mode to Search Data
16.10 Responding to Special Events in a GUI - "Feel"
16.11 2D Drawing, Graphics, and Animation
16.12 3D Graphics with r3D
16.13 Several 3D Scripts Using Raw REBOL Draw Dialect
16.14 Sprite Sheets
16.15 Multitasking
16.16 Using DLLs and Shared Code Files in REBOL
16.17 A Multiple Network Security Camera App Using The Window's Webcam DLL
16.18 REBOL as a Browser Plugin
16.19 Using Databases
16.20 Menus
16.21 Creating Multi Column GUI Text Lists (Data Grids) From Scratch
16.22 RebGUI
16.23 RebGUI Apps - Spreadsheet, Rolodex, Member Manager, Editor, POS system
16.24 Creating PDF files using pdf-maker.r
16.25 Bar Codes
16.26 Creating .swf Files with REBOL/Flash
16.27 Printing With REBOL
16.28 A Remote Check Printing Application
16.29 Creating Apps on Platforms That Don't Support GUI Interfaces
16.30 Encryption and Security
16.31 Rebcode
16.32 Useful REBOL Tools: XML, Zip, Database, Network, Web Server, and More
16.33 6 REBOL Flavors
16.34 Bindology, Dialects, Metaprogramming and Other Advanced Topics
17. REBOL on Android, Open Source R3 (Saphirion Builds), and RED
17.1 Open Source
17.2 Creating an Android Working Environment - Necessary Tools
17.3 R3 GUI Basics
17.4 Simple Requestors
17.5 Layout
17.6 Styles
17.7 Some More Simple Examples
17.8 Additional Essential Resources
17.9 RED
18. Implementing Multi-User Data Management Applications with Rebol
18.1 Multi-User Database Systems In Rebol
18.2 The Typical REBOL 101 Example
18.3 Multi-User Databases
18.4 A Longer Example
18.5 Obtaining Dynamically Assigned Server Addresses
18.6 Serving Clients HTML Form Interfaces
18.7 Simplicity
19. Building Mobile and Web Apps with jsLinb & Sigma Visual Builder
19.1 What are the jsLinb Library and Sigma Visual Builder?
19.2 Installing Sigma Builder on a Web Server
19.3 Basic jsLinb Code and Sigma IDE Examples
19.4 Connecting jsLinb Apps to Rebol CGI Server Applications
19.5 Example Apps made with jsLinb and Rebol CGI Code
19.6 Saving and Deploying your jsLinb Apps in Sigma IDE
19.7 Some Data Grid Examples
19.8 Powerful Layout Widgets
19.9 jsLinb and Sigma Builder Documentation Features
19.10 Using the jsLinb Databinder to Collect and Set Form Data
19.11 A Larger Example App
19.12 Connecting to Stand-Alone Rebol Server Apps
19.13 CrossUI
19.14 A Powerful Addition to the Rebol Toolkit
20. REAL WORLD CASE STUDIES - Learning To Think In Code
20.1 Case: Scheduling Teachers
20.2 Case: A Simple Image Gallery CGI Program
20.3 Case: Days Between Two Dates Calculator
20.4 Case: Simple Search
20.5 Case: A Simple Calculator Application
20.6 Case: A Backup Music Generator (Chord Accompaniment Player)
20.7 Case: FTP Tool
20.8 Case: The "Jeoparody" Training Program
20.9 Case: Scheduling Teachers, Part Two
20.10 Case: An Online Member Page CGI Program
20.11 Case: A CGI Event Calendar
20.12 Case: Media Player (Wave/Mp3 Jukebox)
20.13 Case: Guitar Chord Chart Printer
20.14 Case: Web Site Content Management System (CMS), Sitebuilder.cgi
20.15 Case: Downloading Directories - A Server Spidering App
20.16 Case: Vegetable Gardening
20.17 Case: An Additional Teacher Automation Project
21. Game Programming to Improve Algorithmic Thought and Graphic Skills
21.1 Case: More About Creative Algorithmic Thought: a Tetris Clone
21.2 Case: More Full Program Loops: Ski, Snake, and Invaders
21.3 Case: A GUI Playing Card Framework (Creating a Freecell Clone)
21.4 Case: Creating the REBOL "Demo"
22. Other Scripts
22.1 Thumbnail Maker
22.2 Loops and Conditions - A Simple Data Storage App
22.3 Listview Multi Column Data Grid Example
22.4 Image Effector
22.5 Little Menu Example
22.6 Shoot-Em-Up Video Game
22.7 Bingo Board
22.8 Voice Alarms
22.9 Odds and Ends
23. Learning More About REBOL - Important Documentation Links
24. Beyond REBOL
25. About The Author
25.1 My Businesses
25.2 Client List and Previous Experience
25.3 Contact Me
1. A Crash Course Introduction to REBOL
1.1 Installing and Running Programs
To get started, download and install REBOL/View from http://www.rebol.com/download-view.html (it takes just a few seconds).
Once it's installed, run REBOL (Start -> Programs -> REBOL -> REBOL View), then click the "Console" Icon.
Type "editor none" at the prompt - that will run REBOL's built in text editor.
At this point, you are ready to start typing in REBOL programs. Copy/paste each example from this tutorial into the REBOL editor to see what the code does. Try it right now. Paste the following code into the REBOL editor, then press [F5] on your keyboard to save and run the program. You can save the file using the default "temp.txt" file name, as prompted, or rename it if you'd like. If you see the REBOL security requestor, select "Allow all":
REBOL []
alert "Hello World!"
If you save your program with a ".r" extension in the file name (i.e., "myprogram.r"), then you can also click your saved program's file icon, and it will run just like any normal executable (.exe) file. Try saving the program above on your desktop as "hello.r", then run it by clicking the hello.r icon on your desktop with your mouse.
1.2 Opening REBOL Directly to the Console
Before typing in or pasting any more code, adjust the following option in the REBOL interpreter: click the "User" menu in the graphic Viewtop that opens by default with REBOL, and uncheck "Open Desktop On Startup". That'll save you the trouble of clicking the "Console" button every time you start REBOL.
Setting your email account information and other user settings, is also recommended at this point.
1.3 Some Short Code Examples to Whet Your Appetite
Here are some REBOL program examples which demonstrate the simple and concise nature of REBOL code. Paste each program into the REBOL editor and press [F5] to see it run. Read briefly through each line of the programs to familiarize yourself with what REBOL code looks like. You'll understand exactly how everything works, very shortly.
Here's a short and useful example that saves text field data to a text file. It can be used as the basis for entering and saving categorical units of data of almost every type, for receipts, notes, etc.
REBOL [title: "Text Field Saver"]
view layout [
f1: field
f2: field
f3: field
btn "Save Fields" [
write/append %fields.csv rejoin [
mold f1/text ", " mold f2/text ", " mold f3/text newline
]
alert "Added to File"
]
]
Here's an example of a text editor program that allows you to read, edit, and save any text file:
REBOL [title: "Text Editor"]
view layout [
h1 "Text Editor:"
f: field 600 "filename.txt"
a: area 600x350
across
btn "Load" [
f/text: request-file
show f
filename: to-file f/text
a/text: read filename
show a
]
btn "Save" [
filename: to-file request-file/save/file f/text
write filename a/text
alert "Saved"
]
]
Here's a variation of the program above, repurposed as a web page editor (this program can actually be used to edit real, live web pages on the Internet):
REBOL [title: "Web Page Editor"]
view layout [
h1 "Web Page Editor:"
f: field 600 "ftp://user:pass@site.com/public_html/page.html"
a: area 600x350
across
btn "Load" [
a/text: read to-url f/text
show a
]
btn "Save" [
write (to-url f/text) a/text
alert "Saved"
]
]
Here's a basic calculator app:
REBOL [title: "Calculator"]
view layout [
origin 0 space 0x0 across
style btn btn 50x50 [append f/text face/text show f]
f: field 200x40 font-size 20 return
btn "1" btn "2" btn "3" btn " + " return
btn "4" btn "5" btn "6" btn " - " return
btn "7" btn "8" btn "9" btn " * " return
btn "0" btn "." btn " / " btn "=" [
attempt [f/text: form do f/text show f]
]
]
Here's a variation of the Paypal example from this tutorial's introduction. It downloads a Paypal account file from the web and reports the sum of all gross account transactions, displays all purchases made from the name "Saoud Gorn", and computes the total of all transactions from "Ourliptef.com" which occured between midnight and noon hours. Try running it on a computer that's connected to the Internet:
REBOL [title: "Paypal Reports"]
sum1: sum2: $0
foreach line at read/lines http://re-bol.com/Download.csv 2 [
sum1: sum1 + to-money pick row: parse/all line "," 8
if find row/4 "Saoud" [print rejoin [row/1 ", Saoud Gorn: " row/8]]
if find row/4 "Ourliptef.com" [
if (0:00am <= time: to-time row/2) and (time <= 12:00pm) [
sum2: sum2 + to-money row/8
]
]
]
alert join "GROSS ACCOUNT TRANSACTIONS: " sum1
alert join "2012 Ourliptef.com Morning Total: " sum2
This example extends the reports above with graphs of the collected data (Internet connection required for this example too):
REBOL [title: "Paypal Report Charts"]
transactions: copy []
saoud: copy []
dates: copy []
foreach line at read/lines http://re-bol.com/Download.csv 2 [
row: parse/all line ","
append transactions to-integer row/8
if find row/4 "Saoud" [
append saoud to-integer row/8
append dates replace row/1 "/2012" ""
]
]
if not exists? %q-plot.r [write %q-plot.r read http://re-bol.com/q-plot.r]
do %q-plot.r
view center-face quick-plot [
594x400
bars [(data: copy transactions)]
label "All Paypal Transactions"
]
view center-face quick-plot [
495x530
pen blue
pie [(data: copy saoud)] labels [(data: copy dates)] explode [1 2 3]
title "Saoud" style vh2
]
Here's a slighty more mature version of the first example in this section. This program creates an inventory list using a simple GUI form (a window with some text fields and buttons). The file created could be used, for example, to determine re-order requirements, to calculate inventory and sales tax due, or sent to an accountant to be imported and used in a spreadsheet, etc.:
REBOL [title: "Inventory"]
view layout [
text "SKU:"
f1: field
text "Cost:"
f2: field "1.00"
text "Quantity:"
f3: field
across
btn "Save" [
write/append %inventory.txt rejoin [
mold f1/text " " mold f2/text " " mold f3/text newline
]
alert "Saved"
]
btn "View Data" [editor %inventory.txt]
]
This program allows users to view the inventory data created by the program above, sorted by any chosen column:
REBOL [title: "Sort Inventory"]
inventory: load %inventory.txt
blocked: copy []
foreach [sku cost qty] inventory [
append/only blocked reduce [
sku
to-money cost
to-integer qty
]
]
field-name: request-list "Choose Field To Sort By:" [
"sku" "cost" "qty"
]
field: select ["sku" 1 "cost" 2 "qty" 3] field-name
order: request-list "Ascending or Descending:" ["ascending" "descending"]
either order = "ascending" [
sort/compare blocked func [a b] [(at a field) < (at b field)]
][
sort/compare blocked func [a b] [(at a field) > (at b field)]
]
foreach item blocked [
print rejoin [
"SKU: " item/1 " COST: " item/2 " QTY: " item/3 newline
]
]
halt
Here's a little contact database app that displays user information in a tabular display:
REBOL [title: "Contacts"]
users: [
"John Smith" "123 Tomline Lane Forest Hills, NJ" "555-1234"
"Paul Thompson" "234 Georgetown Pl. Peanut Grove, AL" "555-2345"
"Jim Persee" "345 Pickles Pike Orange Grove, FL" "555-3456"
"George Jones" "456 Topforge Court Mountain Creek, CO" ""
"Tim Paulson" "" "555-5678"
]
gui: [
backdrop white
across
style header text black 200
header "Name:" header "Address:" header "Phone:" return
]
foreach [name address phone] users [
append gui compose [
field (name) field (address) field (phone) return
]
]
view layout gui
Here's a simple email app:
REBOL [title: "Email"]
view layout[
h1 "Send:"
btn "Server settings" [
system/schemes/default/host: request-text/title "SMTP Server:"
system/schemes/pop/host: request-text/title "POP Server:"
system/schemes/default/user: request-text/title "SMTP User Name:"
system/schemes/default/pass: request-text/title "SMTP Password:"
system/user/email: to-email request-text/title "Your Email Addr:"
]
a: field "user@website.com"
s: field "Subject"
b: area
btn "Send"[
send/subject to-email a/text b/text s/text
alert "Sent"
]
h1 "Read:"
f: field "pop://user:pass@site.com"
btn "Read" [editor read to-url f/text]
]
Here's a scheduling app that allows users to create events on any day. The user can then click days on the calendar to see the scheduled events:
REBOL [title: "Schedule"]
view center-face gui: layout [
btn "Date" [date/text: form request-date show date]
date: field
text "Event Title:"
event: field
text "Time:"
time: field
text "Notes:"
notes: field
btn "Add Appointment" [
write/append %appts.txt rejoin [
mold date/text newline
mold event/text newline
mold time/text newline
mold notes/text newline
]
date/text: "" event/text: "" time/text: "" notes/text: ""
show gui
alert "Added"
]
a: area
btn "View Schedule" [
today: form request-date
foreach [date event time notes] load %appts.txt [
if date = today [
a/text: copy ""
append a/text form rejoin [
date newline
event newline
time newline
notes newline newline
]
show a
]
]
]
]
Here's a small but fully functional cash register application:
REBOL [title: "Minimal Cash Register"]
view gui: layout [
style fld field 80
across
text "Cashier:" cashier: fld
text "Item:" item: fld
text "Price:" price: fld [
if error? try [to-money price/text] [alert "Price error" return]
append a/text reduce [mold item/text " " price/text newline]
item/text: copy "" price/text: copy ""
sum: 0
foreach [item price] load a/text [sum: sum + to-money price]
subtotal/text: form sum
tax/text: form sum * .06
total/text: form sum * 1.06
focus item
show gui
]
return
a: area 600x300
return
text "Subtotal:" subtotal: fld
text "Tax:" tax: fld
text "Total:" total: fld
btn "Save" [
items: replace/all (mold load a/text) newline " "
write/append %sales.txt rejoin [
items newline cashier/text newline now/date newline
]
clear-fields gui
a/text: copy ""
show gui
]
]
This program computes a sum all sales made on the current day:
REBOL [title: "Daily Total"]
sales: read/lines %sales.txt
sum: $0
foreach [items cashier date] sales [
if now/date = to-date date [
foreach [item price] load items [
sum: sum + to-money price
]
]
]
alert rejoin ["Total sales today: " sum]
Here's a full screen slide show presentation example:
REBOL [title: "Simple Presentation"]
slides: [
[
at 0x0 box system/view/screen-face/size white [unview]
at 20x20 h1 blue "Slide 1"
box black 2000x2
text "This slide takes up the full screen."
text "Adding images is easy:"
image logo.gif
image stop.gif
image info.gif
image exclamation.gif
text "Click anywhere on the screen for next slide..."
box black 2000x2
]
[
at 0x0 box system/view/screen-face/size effect [
gradient 1x1 tan brown
] [unview]
at 20x20 h1 blue "Slide 2"
box black 2000x2
text "Gradients and color effects are easy in REBOL:"
box effect [gradient 123.23.56 254.0.12]
box effect [gradient blue gold/2]
text "Click anywhere on the screen to close..."
box black 2000x2
]
]
foreach slide slides [
view/options center-face layout slide 'no-title
]
Here's a parts database application:
REBOL [title: "Parts"]
write/append %data.txt ""
database: load %data.txt
view center-face gui: layout [
text "Parts in Stock:"
name-list: text-list blue 400x100 data sort (extract database 4) [
if value = none [return]
marker: index? find database value
n/text: pick database marker
a/text: pick database (marker + 1)
p/text: pick database (marker + 2)
o/text: pick database (marker + 3)
show gui
]
text "Part Name:" n: field 400
text "Manufacturer:" a: field 400
text "SKU:" p: field 400
text "Notes:" o: area 400x100
across
btn "Save" [
if n/text = "" [alert "You must enter a Part name." return]
if find (extract database 4) n/text [
either true = request "Overwrite existing record?" [
remove/part (find database n/text) 4
] [
return
]
]
save %data.txt repend database [n/text a/text p/text o/text]
name-list/data: sort (extract copy database 4)
show name-list
]
btn "Delete" [
if true = request rejoin ["Delete " n/text "?"] [
remove/part (find database n/text) 4
save %data.txt database
do-face clear-button 1
name-list/data: sort (extract copy database 4)
show name-list
]
]
clear-button: btn "New" [
n/text: copy ""
a/text: copy ""
p/text: copy ""
o/text: copy ""
show gui
]
]
Here's a spreadsheet application, originally written in REBOL by Carl Sassenrath, which can inherently use the entire REBOL language and all it's features to process cell data (math, graphics, Internet, file and network protocols, parse, native dialogs, GUI, and all other general purpose capabilities of the language are available to functions in this tiny 68 line program):
REBOL [Title: "Rebocalc" Authors: ["Carl Sassenrath" "Nick Antonaccio"]]
csize: 100x20 max-x: 8 max-y: 16
pane: []
xy: csize / 2 + 1 * 1x0
yx: csize + 1 * 0x1
layout [
cell: field csize edge none [enter face compute face/para/scroll: 0x0]
label: text csize white black bold center
]
char: #"A"
repeat x max-x [
append pane make label [offset: xy text: char]
set in last pane 'offset xy
xy: csize + 1 * 1x0 + xy
char: char + 1
]
repeat y max-y [
append pane make label [offset: yx text: y size: csize * 1x2 / 2]
yx: csize + 1 * 0x1 + yx
]
xy: csize * 1x2 / 2 + 1
cells: tail pane
repeat y max-y [
char: #"A"
repeat x max-x [
v: to-word join char y
set v none
char: char + 1
append pane make cell [offset: xy text: none var: v formula: none]
xy: csize + 1 * 1x0 + xy
]
xy: csize * 1x2 / 2 + 1 + (xy * 0x1)
]
enter: func [face /local data] [
if empty? face/text [exit]
set face/var face/text
data: either face/text/1 = #"=" [next face/text][face/text]
if error? try [data: load data] [exit]
if find [
integer! decimal! money! time! date! tuple! pair!
] type?/word :data [set face/var data exit]
if face/text/1 = #"=" [face/formula: :data]
]
compute: has [blk] [
unfocus
foreach cell cells [
if cell/formula [
either cell/text = "formula" [
cell/text: join "=" form cell/formula
show cell return
][
if error? cell/text: try [do cell/formula] [
cell/text: "ERROR!"
]
]
set cell/var cell/text
show cell
]
]
]
lo: layout [
bx: box second span? pane
text "Example: type '7' into A1, '19' into B1, '=a1 + b1' into C1"
text "Type 'formula' into any cell to edit an existing formula (C1)."
]
bx/pane: pane
view center-face lo
The process of learning a programming language is similar to learning any spoken language (English, French, Spanish, etc.). If you move a person from the United States to Spain, you can be fairly certain that within a year, they will be able to speak Spanish adequately, even if they aren't provided any appropriate structured Spanish training or guidance. Guidance certainly helps clarify the process, but a key essential component is immersion. Immersion in code works the same way. It may be painful and confusing at first to see or comprehend a totally foreign language and environment, but diving right into code is required if you want to become proficient at "speaking" REBOL. Run each example in this section, and along the way, try changing some text headers, button labels, text field sizes, and other obvious properties to see how the programs change. Getting used to using the REBOL interpreter, becoming aware that code examples in this text are malleable, and opening your mind to the prospect and activity of actually typing REBOL code, is an important first step.
1.4 Basics of REBOL Coding
Computer programming is about processing data - that's all computers do internally (although the results of that data processing can appear, and actually end up being, magically more human). Everything you learn about in this text, therefore, will necessarily have to do with inputting, manipulating, and outputting processed data.
Every REBOL program must begin with the following header:
REBOL []
Function words perform actions upon data values. The following function examples display some data values (text, in this case) and request useful data values from users (any text after a semicolon in these examples is a human readable "comment", and is ignored completely by the REBOL interpreter):
REBOL []
alert "Hello world" ; "ALERT" is the function word here.
editor "Hello world" ; "Hello world" is the text data parameter.
print "Hello world" ;
wait 2 ; "Wait" is the function here, "2" is the data.
request-date ; Requestor functions get some data from a user.
request-pass
request-text/title "What is your Name?"
request-file
request-list "Choose a color:" ["Red" "Green" "Blue"]
request ["Size:" "Small" "Medium" "Large"]
request-color
Be sure to paste EVERY code example into the REBOL editor, and watch each line run.
In REBOL, the output of one function (the "return value") can be used as the input ("argument" or "parameter") of another function:
; Here, the "editor" function edits whatever date is input by the user:
editor request-date
; Here, the "alert" function displays whatever text is input by the user:
alert request-text
In REBOL you can assign data to a label word (also called a "variable"), using the colon symbol. Once data is assigned to a word label, you can use that word anywhere to refer to the assigned value:
REBOL []
balance: $53940.23 - $234
print balance
name: request-text/title "Name:"
print name
date: request-date
print date
alert "Click [OK] to continue"
You can join together, or concatenate, data values using the "rejoin" function. Try adding this line to the end of the program above:
alert rejoin [name ", your balance on " date " is " balance]
There are a variety of useful values built into REBOL:
REBOL []
alert rejoin ["Right now the date and time is: " now]
alert rejoin ["The date is: " now/date]
alert rejoin ["The time is: " now/time]
alert rejoin ["The value of PI is " pi]
alert rejoin ["The months are " system/locale/months]
alert rejoin ["The days are " system/locale/days]
REBOL can perform useful calculations on many types of values:
REBOL []
alert rejoin ["5 + 7 = " 5 + 7]
alert rejoin ["Five days ago was " now/date - 5]
alert rejoin ["Five minutes ago was " now/time - 00:05]
alert rejoin ["Added coordinates: " 23x54 + 19x31]
alert rejoin ["Multiplied coordinates: " 22x66 * 2]
alert rejoin ["A multiplied coordinate matrix: " 22x66 * 2x3]
alert rejoin ["Added tuple values: " 192.168.1.1 + 0.0.0.37]
alert rejoin ["The RGB color value of purple - brown is: " purple - brown]
Remember, programming is fundamentally about managing data, so REBOL's ability to appropriately handle operations and computations with common data types leads to greater simplicity and productivity for programmers.
1.5 Conditional Evaluations
Conditional evaluations can be performed using the syntax: "if (this is true) [do this]":
REBOL []
if (now/time > 6:00am) [alert "It's time to get up!"]
Notice the natural handling of the time value in the example above. No special formatting or parsing is required to use that value. REBOL natively "understands" how to perform appropriate computations with time and other common data types.
Use the "either" evaluation to do one thing if a condition is true, and another if the condition is false:
REBOL []
user: "sa98df"
pass: "008uqwefbvuweq"
userpass: request-pass
either (userpass = reduce [user pass]) [
alert rejoin ["Welcome back " user "!"]
][
alert "Incorrect username/password combination"
]
In the code above:
- The word label (variable) "user" is assigned to a text value.
- The variable "pass" is assigned to some text.
- A username/password combination is requested from the user, and the result of that function is labeled "userpass".
- An "either" conditional evaluation is performed on the returned "userpass" value. If the userpass value equals the set "user" and "pass" variables, the user is alerted with a welcome message. Otherwise, the user is alert with an error message.
1.6 Some More Useful Functions
Try pasting every individual line below into the REBOL interpreter console to see how each function can be used to perform useful actions:
REBOL []
print read http://rebol.com ; "read" retrieves the data from many sources
editor http://rebol.com ; the built in editor can also read many sources
print read %./ ; the % symbol is used for local files and folders
editor %./
write %temp.txt "test" ; write takes TWO parameters (file name and data)
editor %temp.txt
editor request-file/only ; "only" refinement limits choice to 1 file
write clipboard:// (read http://rebol.com) ; 2nd parameter in parentheses
editor clipboard://
print read dns://msn.com ; REBOL can read many built in protocols
print read nntp://news.grc.com
write/binary %/c/bay.jpg (read/binary http://rebol.com/view/bay.jpg)
write/binary %tada.wav (read/binary %/c/windows/media/tada.wav)
write/binary %temp.dat (compress read http://rebol.com) ; COMPRESS DATA
print decompress read/binary %temp.dat ; DECOMPRESS DATA
print read ftp://user:pass@website.com/name.txt ; user/pass required
write ftp://user:pass@website.com/name.txt "text" ; user/pass required
editor ftp://user:pass@website.com/name.txt ; can save changes to server!
editor pop://user:pass@website.com ; read all emails in this account
send user@website.com "Hello" ; send email
send user@website.com (read %file.txt) ; email the text from this file
send/attach user@website.com "My photos" [%pic1.jpg %pic2.jpg pic3.jpg]
name: ask "Enter your name" print name ; request a user value in console
call/show "notepad.exe c:\config.sys" ; run an OS shell command
browse http://re-bol.com ; open system default web browser to a page
view layout [image %pic1.jpg] ; view an image
view layout [image request-file/only] ; view a user selected image
insert s: open sound:// load request-file/only wait s close s ; play sound
insert s: open sound:// load %/c/windows/media/tada.wav wait s close s
rename %temp.txt %temp2.txt ; change file name
write %temp.txt read %temp2.txt ; copy file
write/append %temp2.txt "" ; create file (or if it exists, do nothing)
delete %temp2.txt
change-dir %../
what-dir
list-dir
make-dir %./temp
print read %./
attempt [print 0 / 0] ; test for and handle errors
if error? try [print 0 / 0] [alert "*** ERROR: divide by zero"]
parse "asdf#qwer#zxcv" "#" ; split strings at character set
trim " asdf89w we " ; remove white space at the beginning and end
replace/all "xaxbxcxd" "x" "q" ; replace all occurrences of "x" with "q"
checksum read %file.txt ; compute a checksum to ensure validity
print dehex "a%20space" ; convert from URL encoded string
print to-url "a space" ; convert to URL encoded string
print detab "tab separated" ; convert tabs to spaces
print enbase/base "a string" 64 ; convert string or bin to base 64, 16, 2
print encloak "my data" "my pass" ; encrypt and decrpyt data (AES, blow-
print decloak "µÜiûŽz®" "my pass" ; fish and other formats also supported
read-cgi ; neatly parse all data submitted from a web page form
for i 1 99 3 [print i] ; count from 1 to 99, by steps of 3
halt ; "HALT" stops the REBOL console from closing,
; so you can see the printed results.
In order to see each of the lines above execute, paste them directly into the REBOL console, instead of into the editor. When running code directly in the console, it's not necessary to include the REBOL[] header, or the "halt" function:
Really take a look at how much computing ability is enabled by each of the functions above. That short collection of one line code snippets demonstrates how to do many of the most commonly useful tasks performed by a computer:
- Reading data from and writing data to files on a hard drive, thumb drive, etc.
- Reading/writing data from/to web servers and other network sources, the system clipboard, user input, etc.
- Reading emails, sending emails, sending attached files by email.
- Displaying images.
- Playing sounds.
- Navigating and manipulating folders and files on the computer.
- Compressing, decompressing, encrypting, and decrypting data.
- Running third party programs on the computer.
- Reading, parsing, and converting back and forth between common data types and values.
And those lines are just a cursory introduction to a handful of built in REBOL functions. There are hundreds more. At this point in the tutorial, simply read the examples and paste them into the REBOL interpreter console to introduce yourself to the syntax, and to witness their resulting action. You will see these functions, and others, used repeatedly throughout this tutorial and in real working code, for as long as you study and use REBOL. Eventually, you will get to know their syntax and refinements by heart, but you can always refer to reference documentation while you're learning. If you're serious about learning to program, you should take some time now to try changing the parameters of each function to do some useful work (try reading the contents of different file names, send some emails to yourself, compress and decompress some data and view the results with the editor function, etc.)
You can get a list of all function words by typing the "what" function into the REBOL console:
what ; press the [ESC] key to stop the listing
You can see the syntax, parameters, and refinements of any function using the "help" function:
help print
help prin
help read
help write
You can learn more about all the useful functions built in to REBOL by running the following program. Try it now:
write %wordbrowser.r read http://re-bol.com/wordbrowser.r
do %wordbrowser.r
By learning to combine simple functions with a bit of conditional evaluation (if/then) thinking, along with some list processing techniques, you can accomplish truly useful programming goals that go far beyond the capabilities of even complex office suite programs (much more about 'list processing' will be covered shortly).
The important thing to understand at this point is that functions exist in REBOL, to perform actions on all sorts of useful data. Learning to recognize functions and the data parameters which follow them, when you see them in code, is an important first step in learning to read and write REBOL. Eventually memorizing the syntax and appropriate use patterns of all built in functions is a necessary goal if you want to write code fluently.
The benefit of pasting (or even better typing) every single example into the REBOL editor and/or console, cannot be overstated. Concepts will become more understandable, and important code details will be explicitly clarified as this text progresses. For now, working by rote is the best way to continue learning. Read and execute each example, and pay attention to which words are functions and which words are data arguments.
2. Lists, Tables, and the "Foreach" Function
2.1 Managing Spreadsheet-Like Data
2.1.1 Warning
NOTE: This section of the tutorial is the longest and most difficult to grasp at first read. Read through it once to introduce yourself to all the topics, and skim the code structures. Be prepared for it - the code is going to get hairy. Just press on, absorb what you can, and continue to read through the entire section. You'll refer back to it later in much greater detail, once you've seen how all the concepts, functions, and code patterns fit together to create useful programs.
2.1.2 Blocks
Most useful business programs process lists of data. Tables of data are actually dealt with programatically as consecutive lists of items. A list or "block" of data is created in REBOL by surrounding values with square brackets:
REBOL []
names: ["Bob" "Tom" "Bill"]
To perform an operation/computation with/to each data item in the block, use the foreach function. Foreach syntax can be read like this: "foreach (labeled item) in (this labeled block) [perform this operation with/to each labeled item]:
REBOL []
names: ["Bob" "Tom" "Bill"] ; create a block of text items labeled "names"
foreach name names [print name] ; print each name value in the block
halt
This example prints each value stored in the built-in "system/locale/months" block:
REBOL []
months: system/locale/months ; set the variable "months" to the values
foreach month months [print month] ; print each month value
halt
Note that in the example above, the variable words "months" and "month" could be changed to any other desired, arbitrarily determined, label:
REBOL []
foo: system/locale/months
foreach bar foo [print bar] ; variable labels are arbitrary
halt
Labeling the system/locale/months block is also not required. Without the label, the code is shorter, but perhaps just a bit harder to read:
REBOL []
foreach month system/locale/months [print month]
halt
Learning to read and think in terms of "foreach item in list [do this to each item]" is one of the most important fundamental concepts to grasp in programming. You'll see numerous repeated examples in this text. Be aware every time you see the word "foreach".
You can obtain lists of data from a variety of different sources. Notice that the "load" function is typically used to read lists of data. This example prints the files in the current folder on the hard drive:
REBOL []
folder: load %.
foreach file folder [print file]
halt
This example loads the list from a file stored on a web site:
REBOL []
names: load http://re-bol.com/names.txt
foreach name names [print name]
halt
NOTE: you can write the data required for the above example to your own web server, using the following line of code. Note that the "save" function is typically used to write lists of data:
REBOL []
save ftp://user:pass@site.com/folder/names.txt ["Bob" "Tom" "Bill"]
2.2 Some Simple List Algorithms (Count, Sum, Average, Max/Min)
2.2.1 Counting Items
The "length?" function counts the number of items in a list:
REBOL []
receipts: [$5.23 $95.98 $7.46 $34] ; a list labeled "receipts"
alert rejoin ["There are " length? receipts " receipts in the list."]
You can assign counts to variable labels and use the values later:
REBOL []
month-count: length? system/locale/months
day-count: length? system/locale/days
alert rejoin ["There are " month-count " months and " day-count " days."]
Another way to count items in a list is to create a counter variable, initially set to 0. Use a foreach loop to go through each item in the list, and increment (add 1) to the count variable:
REBOL []
count: 0
receipts: [$5.23 $95.98 $7.46 $34]
foreach receipt receipts [count: count + 1] ; increment count by 1
alert rejoin ["There are " count " receipts in the list."]
Here's an alternate syntax for incrementing counter variables:
REBOL []
count: 0
receipts: [$5.23 $95.98 $7.46 $34]
foreach receipt receipts [++ count] ; increment count by 1
alert rejoin ["There are " count " receipts in the list."]
This example counts the number of months in a year and the number of days in a week, using counter variables:
REBOL []
month-count: 0
day-count: 0
foreach month system/locale/months [++ month-count]
foreach day system/locale/days [++ day-count]
alert rejoin ["There are " month-count " months and " day-count " days."]
Counter variables are particularly useful when you only want to count certain items in a list. The following example counts only items that are number values:
REBOL []
count: 0
list: ["screws" 14 "nuts" 38 "bolts" 23]
foreach item list [
; Increment only if item type is integer:
if (type? item) = integer! [++ count]
]
alert rejoin ["The count of all number values in the list is: " count]
2.2.2 Sums
To calculate the sum of numbers in a list, start by assigning a sum variable to 0. Then use a foreach loop to increment the sum by each individual number value. This example starts by assigning the label "balance" to a value of 0. Then the label "receipts" is assigned to a list of money values. Then, each value in the receipts list is added to the balance, and that tallied balance is displayed:
REBOL []
sum: 0 ; a sum variable, initially set to 0
receipts: [$5.23 $95.98 $7.46 $34] ; a list, labeled "receipts"
foreach item receipts [sum: sum + item] ; add them up
alert rejoin ["The sum of all receipts is: " sum]
You could total only the items in a list which contain number values, for example, like this:
REBOL []
sum: 0
list: ["screws" 14 "nuts" 38 "bolts" 23]
foreach item list [
if (type? item) = integer! [ ; only if item type is integer
sum: sum + item ; add item to total
]
]
alert rejoin ["The total of all number values in the list is: " sum]
2.2.3 Averages
Computing the average value of items in a list is simply a matter of dividing the sum by the count:
REBOL []
sum: 0
receipts: [$5.23 $95.98 $7.46 $34]
foreach item receipts [sum: sum + item]
average: sum / (length? receipts)
alert rejoin ["The average balance of all receipts is: " average]
2.2.4 Maximums and Minimums
REBOL has built in "maximum-of" and "minimum-of" functions:
REBOL []
receipts: [$5.23 $95.98 $7.46 $34]
print first maximum-of receipts
print first minimum-of receipts
halt
You can perform more complicated max/min comparisons by checking each value with a conditional evaluation. This example looks for the highest receipt value under $50:
REBOL []
highest: $0
receipts: [$5.23 $95.98 $7.46 $34]
foreach receipt receipts [
if (receipt > highest) and (receipt < $50) [highest: receipt]
]
alert rejoin ["Maximum receipt below fifty bucks: " highest]
2.3 Searching
The "find" function is used to perform simple searches:
REBOL []
names: ["John" "Jane" "Bill" "Tom" "Jen" "Mike"]
if find names "Bill" [alert "Yes, Bill is in the list!"]
if not find names "Paul" [alert "No, Paul is not in the list."]
You can determine the index position of a found item in a list, using the "index?" function:
REBOL []
names: ["John" "Jane" "Bill" "Tom" "Jen" "Mike"]
indx: index? find names "Bill"
print rejoin ["Bill is at position " indx " in the list."]
halt
You can search for text within each item in a list using a foreach loop to search each individual value:
REBOL []
names: ["John" "Jane" "Bill" "Tom" "Jen" "Mike"]
foreach name names [
if find name "j" [
print rejoin ["'j' found in " name]
]
]
halt
The "find/any" refinement can be used to search for wildcard characters. The "*" character allows for portions of search text to contain random character strings of any length. The "?" character allows for random character searches of a specified length (at specific character positions within a search term):
REBOL []
names: ["OJ" "John" "Joan" "Jan" "Major Bill" "MJO" "Mike"]
foreach name names [
if find/any name "*jo*" [
print rejoin ["'jo' found in " name]
]
]
print ""
foreach name names [
if find/any name "j*n" [
print rejoin ["'j*n' found in " name]
]
]
print ""
foreach name names [
if find/any name "j??n" [
print rejoin ["'j--n' found in " name]
]
]
halt
2.4 Gathering Data, and the "Copy" Function
When collecting ("aggregating") values into a new block, always use the "copy" function to create the new block. You'll need to do this whenever a sub-list or super-list of values is created based upon conditional evaluations performed on data in a base list:
REBOL []
low-receipts: copy [] ; Create blank list with copy [], NOT []
receipts: [$5.23 $95.98 $7.46 $34]
foreach receipt receipts [
if receipt < $10 [append low-receipts receipt] ; add to blank list
]
print low-receipts
halt
For example, the following line should should NOT be used (it does not contain the word "copy" when creating a blank list):
low-receipts: [] ; WRONG - should be low-receipts: COPY []
The same is true when creating blank string values. Use the "copy" function whenever you create an empty text value that you intend to adjust or add to:
REBOL []
names: copy {} ; Create blank string with copy {}, NOT {}
people: ["Joan" "George" "Phil" "Jane" "Peter" "Tom"]
foreach person people [
if find person "e" [
append names rejoin [person " "] ; This appends to blank string
]
]
print names
halt
2.5 List Comparison Functions
REBOL has a variety of useful built in list comparison functions. You'll use these for determining differences, similarities, and combinations between sets of data:
REBOL []
group1: ["Joan" "George" "Phil" "Jane" "Peter" "Tom"]
group2: ["Paul" "George" "Andy" "Mary" "Tom" "Tom"]
print rejoin ["Group 1: " group1]
print ""
print rejoin ["Group 2: " group2]
print newline
print rejoin ["Intersection: " intersect group1 group2]
print "^/(values shared by both groups)^/^/"
print rejoin ["Difference: " difference group1 group2]
print "^/(values not shared by both groups)^/^/"
print rejoin ["Union: " union group1 group2]
print "^/(all unique values contained in both groups)^/^/"
print rejoin ["Join: " join group1 group2]
print "^/(one group tacked to the end of the other group)^/^/"
print rejoin ["Excluded from Group 2: " exclude group1 group2]
print "^/(values contained in group1, but not contained in group2)^/^/"
print rejoin ["Unique in Group 2: " unique group2]
print "^/(unique values contained in group2)"
halt
2.6 Creating Lists From User Input
2.6.1 Creating New Blocks and Adding Values
You can create a new block using the code pattern below. Simply assign variable labels to "copy []":
REBOL []
items: copy [] ; new empty block named "items"
prices: copy [] ; new empty block named "prices"
Add values to new blocks using the "append" function:
REBOL []
items: copy []
prices: copy []
append items "Screwdriver"
append prices "1.99"
append items "Hammer"
append prices "4.99"
append items "Wrench"
append prices "5.99"
Use the "print", "probe", or "editor" functions to view the data in a block. The "print" function simply prints the values in the block. The "probe" function shows the block data structure (square brackets enclosing the values, quotes around text string values, etc.). The "editor" function opens REBOL's built in text editor, with the block structure displayed:
REBOL []
items: copy []
prices: copy []
append items "Screwdriver"
append prices "1.99"
append items "Hammer"
append prices "4.99"
append items "Wrench"
append prices "5.99"
editor items
editor prices
print rejoin ["ITEMS: " items newline]
print rejoin ["PRICES: " prices newline]
probe items
probe prices
halt
2.6.2 Accepting Data Input from a User
You've already been introduced to the "request-text" function. It accepts text input from a user:
REBOL []
request-text
You can assign a variable label to the data entered by the user, and then use that data later in your program:
REBOL []
price: request-text
alert price
You can add a text title to the request-text function, with the "/title" refinement:
REBOL []
price: request-text/title "Input a dollar value:"
alert price
You can add a default text response using the "/default" refinement:
REBOL []
price: request-text/default "14.99"
alert price
You can combine the "/title" and "/default" refinements:
REBOL []
price: request-text/title/default "Input a dollar value:" "14.99"
alert price
The "ask" function does the same thing, but within the text environment of the REBOL interpreter console (instead of using a popup windowed requestor):
REBOL []
price: ask "Input a dollar value: $"
alert price
2.6.3 Building Blocks from User-Entered Data
Add data to a block, which has been entered by the user, using the code pattern below. Append the variable label of the entered data to the block label:
REBOL []
items: copy []
prices: copy []
item: request-text/title/default "Item:" "screwdriver"
price: request-text/title/default "Price:" "1.99"
append items item
append prices price
The example below uses a "forever" loop to repeatedly perform the "request-text" and "append" operations. A conditional "if" evaluation checks to see if the user enters "" (empty text) in the Item requestor. If so, it stops the forever loop using the "break" function, and displays the data in each block:
REBOL []
items: copy []
prices: copy []
forever [
item: request-text/title "Item:"
if item = "" [break]
price: request-text/title "Price:"
append items item
append prices price
]
print "Items:^/" ; THE ^/ CHARACTER PRINTS A NEWLINE
probe items
print "^/^/Prices:^/"
probe prices
halt
You could just as easily add the entered data to a single block:
REBOL []
inventory: copy []
forever [
item: request-text/title "Item:"
if item = "" [break]
price: request-text/title "Price:"
append inventory item
append inventory price
]
print "Inventory:^/"
probe inventory
halt
2.6.4 Saving and Reading Block Data To/From Files
Save a block to a text file using the "save" function. Remember that in REBOL, file names always begin with the "%" character:
REBOl []
inventory: ["Screwdriver" "1.99" "Hammer" "4.99" "Wrench" "5.99"]
save %inv.txt inventory
alert "Saved"
Load blocked data from a saved file using the "load" function. You can assign a variable label to the loaded data, to use it later in the program:
REBOL []
inventory: load %inv.txt
print "Inventory^/"
probe inventory
halt
You can also append data directly to a file using the "write/append" function. When using the "write/append" function, use the "mold" function to enclose each text value in quotes, and the "rejoin" function to separate each value with a space:
REBOL []
forever [
item: request-text/title "Item:"
if item = "" [break]
price: request-text/title "Price:"
write/append %inv.txt rejoin [
mold item " " mold price " "
]
]
inventory: load %inv.txt
print "Inventory:^/"
probe inventory
halt
2.7 Three Useful Data Storage Programs: Inventory, Contacts, Schedule
The last program above provides a nice template for practical applications of all types. It stores and displays inventory items and prices. Notice that a "title" variable has been added to the header, set to the text "Inventory". It's good practice to assign titles to all your programs:
REBOL [title: "Inventory"]
forever [
item: request-text/title "Item:"
if item = "" [break]
price: request-text/title "Price:"
write/append %inv.txt rejoin [
mold item " " mold price " "
]
]
inventory: load %inv.txt
print "Inventory:^/"
probe inventory
halt
Here's the same program as above, changed slightly to store and display contact information:
REBOL [title: "Contacts"]
forever [
name: request-text/title "Name:"
if name = "" [break]
address: request-text/title "Address:"
phone: request-text/title "Phone:"
write/append %contacts.txt rejoin [
mold name " " mold address " " mold phone " "
]
]
contacts: load %contacts.txt
print "Contacts:^/"
probe contacts
halt
Here it is again, repurposed to hold schedule information:
REBOL [title: "Schedule"]
forever [
event: request-text/title/default "Event Title:" "Meeting with "
if event = "" [break]
date: request-text/title/default "Date:" "1-jan-2013"
time: request-text/title/default "Time:" "12:00pm"
notes: request-text/title/default "Notes:" "Bring: "
write/append %schedule.txt rejoin [
mold event " " mold date " " mold time " " mold notes " "
]
]
schedule: load %schedule.txt
print "Schedule:^/^/"
probe schedule
halt
The types of data you store using these sorts of operations can be adjusted specifically to your particular data management needs for any given task. Your ability to apply this code to practical situations is limited only by your own creativity. All you need to do is change the requestor titles and the variable labels to clarify the type of data being stored.
2.8 Working With Tables of Data: Columns and Rows
Columns within tables of data are arranged in sequential order in blocks. Indentation and white space helps to display columns neatly, within a visual "table" layout. The following table conceptually contains 3 rows of 3 columns of data, but the whole block is still just a sequential list of 9 items:
accounts: [
"Bob" $529.23 21-jan-2013
"Tom" $691.37 13-jan-2013
"Ann" $928.85 19-jan-2013
]
The foreach function in the next example alerts the user with every three consecutive data values in the table (each row of 3 consecutive name, balance, and date column values):
REBOL []
accounts: [
"Bob" $529.23 21-jan-2013
"Tom" $691.37 13-jan-2013
"Ann" $928.85 19-jan-2013
]
foreach [name balance date] accounts [
alert rejoin [
"Name: " name ", Date: " date ", Balance: " balance
]
]
This example displays the computed balance for each person on the given date. The amount displayed is the listed "balance" value for each account, minus a universal "fee" value):
REBOL []
accounts: [
"Bob" $529.23 21-jan-2013
"Tom" $691.37 13-jan-2013
"Ann" $928.85 19-jan-2013
]
fee: $5
foreach [name balance date] accounts [
alert rejoin [name "'s balance on " date " will be " balance - fee]
]
Here's a variation of the above example which displays the sum of values in all accounts:
REBOL []
accounts: [
"Bob" $529.23 21-jan-2013
"Tom" $691.37 13-jan-2013
"Ann" $928.85 19-jan-2013
]
sum: $0
foreach [name balance date] accounts [sum: sum + balance]
alert rejoin ["The total of all balances is: " sum]
Here's a variation that computes the average balance:
REBOL []
accounts: [
"Bob" $529.23 21-jan-2013
"Tom" $691.37 13-jan-2013
"Ann" $928.85 19-jan-2013
]
sum: $0
foreach [name balance date] accounts [sum: sum + balance]
alert rejoin [
"The average of all balances is: "
sum / ((length? accounts) / 3)
]
Here is a variation of the "Schedule" application from the previous section, slightly adjusted using the "foreach" function to format a more cleanly printed data display:
REBOL []
forever [
event: request-text/title "Event Title:"
if event = "" [break]
date: request-text/title/default "Date:" "1-jan-2013"
time: request-text/title/default "Time:" "12:00pm"
notes: request-text/title/default "Notes:" "Bring: "
write/append %schedule.txt rejoin [
mold event " " mold date " " mold time " " mold notes " "
]
]
schedule: load %schedule.txt
print newpage ; "newpage" prints a cleared screen
print "SCHEDULE:^/^/"
foreach [event date time notes] schedule [
print rejoin [
"Event: " event newline
"Date: " date newline
"Time: " time newline
"Notes: " notes newline newline
]
]
halt
Here is the "Inventory" program from the previous section, adjusted slightly to count the number of items and calculate a sum of inventory prices:
REBOL [title: "Inventory"]
forever [
item: request-text/title "Item:"
if item = "" [break]
price: request-text/title "Price:"
write/append %inv.txt rejoin [
mold item " " mold price " "
]
]
inventory: load %inv.txt
count: 0
sum: $0
foreach [item price] inventory [
count: count + 1
sum: sum + to-money price
]
print newpage
print rejoin ["Total # of Items: " count]
print rejoin ["Sum of Prices: " sum]
halt
Here's a variation of the "Contacts" application that searches for saved names, and prints out any matching contact information:
REBOL []
search: request-text/title/default "Search text:" "John"
contacts: load %contacts.txt
print newpage
print rejoin [search " found in:^/"]
foreach [name address phone] contacts [
if find name search [
print rejoin [
"Name: " name newline
"Address: " address newline
"Phone: " phone newline
]
]
]
halt
The ability to conceptually "flatten" tabular data into sequential streams of items, and vice-versa, to think of consecutive groups of items in a list as rows within mapped categorical columns, is fundamentally important to working with all sorts of business data sets. You'll see this concept applied regularly throughout examples in this tutorial and in real working code.
2.9 Additional List/Block/Series Functions and Techniques
REBOL has built-in functions for performing every imaginable manipulation to list content, order, and other block properties - adding, deleting, searching, sorting, comparing, counting, replacing, changing, moving, etc. Here's a quick demonstrative list of functions. Try pasting each line individually into the REBOL interpreter to see how each function works:
REBOL []
names: ["John" "Jane" "Bill" "Tom" "Jen" "Mike"] ; a list of text strings
print "Two ways of printing values, 'probe' and 'print':"
probe names ; "Probe" is like "print", but it shows the actual data
print names ; structure. "Print" attempts to format the displayed data.
print "^/Sorting:"
sorted: sort copy names ; "Sort" sorts values ascending or descending.
probe names ; "Copy" keeps the names block from changing
print sorted
sort/reverse names ; Here, the names block has been sorted without
probe names ; copy, so it's permanently changed.
print "^/Picking items:"
probe first names ; 3 different ways to pick the 1st item:
probe names/1
probe pick names 1
probe second names ; 3 different ways to pick the 2nd item:
probe names/2
probe pick names 2
print "^/Searching:"
probe find names "John" ; How to search a block
probe first find names "John"
probe find/last names "Jane"
probe select names "John" ; Find next item after "John"
print "^/Taking sections of a series:"
probe at names 2
probe skip names 2 ; Skip every two items
probe extract names 3 ; Collect every third item
print "^/Making changes:"
append names "George"
probe names
insert (at names 3) "Lee"
probe names
remove names
probe names
remove find names "Mike"
probe names
change names "Phil"
probe names
change third names "Phil"
probe names
poke names 3 "Phil"
probe names
probe copy/part names 2
replace/all names "Phil" "Al"
probe names
print "^/Skipping around:"
probe head names
probe next names
probe back names
probe last names
probe tail names
probe index? names
print "^/Converting series blocks to strings of text:"
probe form names
probe mold names
print "^/Other Series functions:"
print length? names
probe reverse names
probe clear names
print empty? names
halt
To demonstrate just a few of the functions above, here are some practical examples of common list operations, performed on a block of user contact information. The demonstration block of data is organized as 5 rows of 3 columns of data (name, address, phone), or 15 consecutive items in a list labeled "users". Notice that to maintain the column and row structure, empty strings ("") are placed at positions in the list where there is no data:
REBOL []
users: [
"John Smith" "123 Tomline Lane Forest Hills, NJ" "555-1234"
"Paul Thompson" "234 Georgetown Pl. Peanut Grove, AL" "555-2345"
"Jim Persee" "345 Pickles Pike Orange Grove, FL" "555-3456"
"George Jones" "456 Topforge Court Mountain Creek, CO" ""
"Tim Paulson" "" "555-5678"
]
append users ["Joe Thomas" "" "555-321-7654"] ; append to end of list
probe users
probe (at users 4) ; parentheses are not required
insert (at users 4) [
"Tom Adams" "321 Way Lane Villageville, AZ" "555-987-6543"
]
probe users
remove (at users 4) ; remove 1 item
probe users
; BE CAREFUL - the line above breaks the table structure by removing
; an item entirely, so all other data items are shifted into incorrect
; columns. Instead, either replace the data with an empty place holder
; or remove the address and phone fields too:
remove/part (at users 4) 2 ; remove 2 items
probe users
change (at users 1) "Jonathan Smith"
probe users
remove (at users 1) insert (at users 1) "Jonathan Smith"
probe users
halt
The "extract" function is useful for picking out columns of data from structured blocks:
REBOL []
users: [
"John Smith" "123 Tomline Lane Forest Hills, NJ" "555-1234"
"Paul Thompson" "234 Georgetown Pl. Peanut Grove, AL" "555-2345"
"Jim Persee" "345 Pickles Pike Orange Grove, FL" "555-3456"
"George Jones" "456 Topforge Court Mountain Creek, CO" ""
"Tim Paulson" "" "555-5678"
]
probe extract users 3 ; names
probe extract (at users 2) 3 ; addresses
probe extract (at users 3) 3 ; phone numbers
halt
You can "pick" items at a particular index location in the list:
REBOL []
users: [
"John Smith" "123 Tomline Lane Forest Hills, NJ" "555-1234"
"Paul Thompson" "234 Georgetown Pl. Peanut Grove, AL" "555-2345"
"Jim Persee" "345 Pickles Pike Orange Grove, FL" "555-3456"
"George Jones" "456 Topforge Court Mountain Creek, CO" ""
"Tim Paulson" "" "555-5678"
]
print pick users 1 ; FIRST name
print pick users 2 ; FIRST address
print pick users 3 ; FIRST phone
;
print pick users 4 ; SECOND name
print pick users 5 ; SECOND address
print pick users 6 ; SECOND phone
;
indx: length? users ; index position of the LAST item
print pick users indx ; last item
print pick users (indx - 1) ; second to last item
print pick users (random length? users) ; random item
halt
You can determine the index location at which an item is found, using the "find" function:
indx: index? find users "John Smith"
In REBOL there 4 ways to pick items at such a variable index. Each syntax below does the exact same thing. These are just variations of the "pick" syntax:
print pick users indx
print users/:indx
print compose [users/(indx)] ; put composed values in parentheses
print reduce ['users/(indx)] ; put a tick mark on non-reduced values
Pay particular attention to the "compose" and "reduce" functions. They allow you to convert static words in blocks to evaluated values:
REBOL []
; This example prints "[month]" 12 times:
foreach month system/locale/months [
probe [month]
]
; These examples print all 12 month values:
foreach month system/locale/months [
probe reduce [month]
]
foreach month system/locale/months [
probe compose [(month)]
]
Here's a complete example that requests a name from the user, finds the index of that name in the list, and picks out the name, address, and phone data for that user (located at the found indx, indx + 1, and indx + 2 positions):
REBOL [title: "Search Users"]
users: [
"John Smith" "123 Tomline Lane Forest Hills, NJ" "555-1234"
"Paul Thompson" "234 Georgetown Pl. Peanut Grove, AL" "555-2345"
"Jim Persee" "345 Pickles Pike Orange Grove, FL" "555-3456"
"George Jones" "456 Topforge Court Mountain Creek, CO" ""
"Tim Paulson" "" "555-5678"
]
name: request-text/title/default "Name:" "Jim Persee"
indx: index? find users name
print rejoin [
(pick users indx) newline
(pick users (indx + 1)) newline
(pick users (indx + 2)) newline
]
halt
Here's a version that uses code from the "Contacts" program you saw earlier. It allows you to create your own user database, and then search and display entries with the code above:
REBOL [title: "Search My Stored Contacts"]
; This code is borrowed from the "Contacts" program seen earlier:
forever [
name: request-text/title "Name:"
if name = "" [break]
address: request-text/title "Address:"
phone: request-text/title "Phone:"
write/append %contacts.txt rejoin [
mold name " " mold address " " mold phone " "
]
]
users: load %contacts.txt
; This is a variation of the code above which adds an error check, to
; provide a response if the search text is not found in the data block:
name: request-text/title/default "Search For:" "Jim Persee"
if error? try [indx: index? find users name] [
alert "Name not found" quit
]
print rejoin [
(pick users indx) newline
(pick users (indx + 1)) newline
(pick users (indx + 2)) newline
]
halt
2.10 Sorting Lists and Tables of Data
You can sort a list of data using the "sort" function:
REBOL []
print sort system/locale/months
halt
This example displays a list requestor with the months sorted alphabetically:
REBOL []
request-list "Sorted months:" sort system/locale/months
If you sort a block of values consisting of data types that REBOL understands, the values will be sorted appropriately for their type (i.e., chronologically for dates and times, numerically for numbers, alphabetically for text strings):
REBOL []
probe sort [1 11 111 2 22 222 8 9 5] ; sorted NUMERICALLY
probe sort ["1" "11" "111" "2" "22" "222" "8" "9" "5"] ; ALPHABETICALLY
probe sort [1-jan-2012 1-feb-2012 1-feb-2011] ; sorted CHRONOLOGICALLY
halt
To sort by the first column in a table, use the "sort/skip" refinement. The table below is made up of 5 rows of 3 conceptual columns, so the first item of each row is found by skipping every 3 values:
REBOL []
users: [
"John Smith" "123 Tomline Lane Forest Hills, NJ" "555-1234"
"Paul Thompson" "234 Georgetown Pl. Peanut Grove, AL" "555-2345"
"Jim Persee" "345 Pickles Pike Orange Grove, FL" "555-3456"
"George Jones" "456 Topforge Court Mountain Creek, CO" ""
"Tim Paulson" "" "555-5678"
]
editor sort/skip users 3
Sorting by any other selected column requires that data be restructured into blocks of blocks which clearly define the column structure. For example, this "flat" table, although visually clear, is really just a consecutive list of 15 data items:
users: [
"John Smith" "123 Tomline Lane Forest Hills, NJ" "555-1234"
"Paul Thompson" "234 Georgetown Pl. Peanut Grove, AL" "555-2345"
"Jim Persee" "345 Pickles Pike Orange Grove, FL" "555-3456"
"George Jones" "456 Topforge Court Mountain Creek, CO" ""
"Tim Paulson" "" "555-5678"
]
To sort it by colomn, the data must be represented as follows (notice that conceptual rows are now separated into discrete blocks of 3 columns of data):
blocked-users: [
["John Smith" "123 Tomline Lane Forest Hills, NJ" "555-1234"]
["Paul Thompson" "234 Georgetown Pl. Peanut Grove, AL" "555-2345"]
["Jim Persee" "345 Pickles Pike Orange Grove, FL" "555-3456"]
["George Jones" "456 Topforge Court Mountain Creek, CO" ""]
["Tim Paulson" "" "555-5678"]
]
The following code demonstrates how to convert a flattened block into such a structure of nested row/column blocks:
REBOL []
users: [
"John Smith" "123 Tomline Lane Forest Hills, NJ" "555-1234"
"Paul Thompson" "234 Georgetown Pl. Peanut Grove, AL" "555-2345"
"Jim Persee" "345 Pickles Pike Orange Grove, FL" "555-3456"
"George Jones" "456 Topforge Court Mountain Creek, CO" ""
"Tim Paulson" "" "555-5678"
]
blocked-users: copy []
foreach [name address phone] users [
; APPEND/ONLY inserts blocks as blocks, instead of as individual items
; The REDUCE function convert the words "name", "address", and "phone"
; to text values:
append/only blocked-users reduce [name address phone]
]
editor blocked-users
Now you can use the "/compare" refinement of the sort function to sort by a chosen column (field):
REBOL []
blocked-users: [
["John Smith" "123 Tomline Lane Forest Hills, NJ" "555-1234"]
["Paul Thompson" "234 Georgetown Pl. Peanut Grove, AL" "555-2345"]
["Jim Persee" "345 Pickles Pike Orange Grove, FL" "555-3456"]
["George Jones" "456 Topforge Court Mountain Creek, CO" ""]
["Tim Paulson" "" "555-5678"]
]
field: 2 ; column to sort (address, in this case)
sort/compare blocked-users func [a b] [(at a field) < (at b field)]
editor blocked-users ; sorted by the 2nd field (by address)
To sort in the opposite direction (i.e., descending, as opposed to ascending), just change the "<" operater to ">":
REBOL []
blocked-users: [
["John Smith" "123 Tomline Lane Forest Hills, NJ" "555-1234"]
["Paul Thompson" "234 Georgetown Pl. Peanut Grove, AL" "555-2345"]
["Jim Persee" "345 Pickles Pike Orange Grove, FL" "555-3456"]
["George Jones" "456 Topforge Court Mountain Creek, CO" ""]
["Tim Paulson" "" "555-5678"]
]
field: 2
sort/compare blocked-users func [a b] [(at a field) > (at b field)]
editor blocked-users
Here's a complete example that converts a flat data block to a nested block of blocks, and then sorts by a user-selected field, in a chosen ascending/descending direction:
REBOL [title: "View Sorted Users"]
users: [
"John Smith" "123 Tomline Lane Forest Hills, NJ" "555-1234"
"Paul Thompson" "234 Georgetown Pl. Peanut Grove, AL" "555-2345"
"Jim Persee" "345 Pickles Pike Orange Grove, FL" "555-3456"
"George Jones" "456 Topforge Court Mountain Creek, CO" ""
"Tim Paulson" "" "555-5678"
]
blocked-users: copy []
foreach [name address phone] users [
append/only blocked-users reduce [name address phone]
]
field: to-integer request-list "Choose Field To Sort By:" ["1" "2" "3"]
order: request-list "Ascending or Descending:" ["ascending" "descending"]
either order = "ascending" [
sort/compare blocked-users func [a b] [(at a field) < (at b field)]
][
sort/compare blocked-users func [a b] [(at a field) > (at b field)]
]
editor blocked-users
Here's a version of the program above that uses code from the "Contacts" app presented earlier, which allows you to enter your own "users" contact info, and then sort and display it as above:
REBOL [title: "Sort My Stored Contacts"]
; This code is borrowed from the "Contacts" program seen earlier:
forever [
name: request-text/title "Name:"
if name = "" [break]
address: request-text/title "Address:"
phone: request-text/title "Phone:"
write/append %contacts.txt rejoin [
mold name " " mold address " " mold phone " "
]
]
users: load %contacts.txt
; This is a variation of the code above:
blocked-users: copy []
foreach [name address phone] users [
append/only blocked-users reduce [name address phone]
]
field-name: request-list "Choose Field To Sort By:" [
"Name" "Address" "Phone"
]
; The "select" function chooses the next value in a list, selected by the
; user. In this case if the field-name variable equals "name", the
; "field" variable is set to 1. If the field-name variable equals
; "address", the "field" variable is set to 2. If field-name="phone", the
; "field" variable is set to 3:
field: select ["name" 1 "address" 2 "phone" 3] field-name
order: request-list "Ascending or Descending:" ["ascending" "descending"]
either order = "ascending" [
sort/compare blocked-users func [a b] [(at a field) < (at b field)]
][
sort/compare blocked-users func [a b] [(at a field) > (at b field)]
]
editor blocked-users
Note again that REBOL sorts data appropriately, according to type. If numbers, dates, times, and other recognized data types are stored as string values, the sort will be alphabetical for the chosen field (because that is the appropriate sort order for text):
REBOL []
text-data: [
"1" "1-feb-2012" "1:00am" "abcd"
"11" "1-mar-2012" "1:00pm" "bcde"
"111" "1-feb-2013" "11:00am" "cdef"
"2" "1-mar-2013" "13:00" "defg"
"22" "2-feb-2012" "9:00am" "efgh"
"222" "2-feb-2009" "11:00pm" "fghi"
]
blocked: copy []
foreach [number date time string] text-data [
append/only blocked reduce [number date time string]
]
field-name: request-list "Choose Field To Sort By:" [
"number" "date" "time" "string"
]
field: select ["number" 1 "date" 2 "time" 3 "string" 4] field-name
order: request-list "Ascending or Descending:" ["ascending" "descending"]
either order = "ascending" [
sort/compare blocked func [a b] [(at a field) < (at b field)]
][
sort/compare blocked func [a b] [(at a field) > (at b field)]
]
editor blocked
Convert values to appropriate data types during the process of blocking the "flattened" data, and fields will magically be sorted appropriately (in numerical, chronological, or other data-type-appropriate order):
REBOL []
text-data: [
"1" "1-feb-2012" "1:00am" "abcd"
"11" "1-mar-2012" "1:00pm" "bcde"
"111" "1-feb-2013" "11:00am" "cdef"
"2" "1-mar-2013" "13:00" "defg"
"22" "2-feb-2012" "9:00am" "efgh"
"222" "2-feb-2009" "11:00pm" "fghi"
]
blocked: copy []
foreach [number date time string] text-data [
append/only blocked reduce [
to-integer number
to-date date
to-time time
string
]
]
field-name: request-list "Choose Field To Sort By:" [
"number" "date" "time" "string"
]
field: select ["number" 1 "date" 2 "time" 3 "string" 4] field-name
order: request-list "Ascending or Descending:" ["ascending" "descending"]
either order = "ascending" [
sort/compare blocked func [a b] [(at a field) < (at b field)]
][
sort/compare blocked func [a b] [(at a field) > (at b field)]
]
editor blocked
2.11 CSV Files and the "Parse" Function
"Comma Separated Value" (CSV) files are a universal text format used to store and transfer tables of data. Spreadsheets, database systems, financial software, and other business applications typically can export and import tabular data to and from CSV format.
In CSV files, rows of data are separated by a line break. Column values are most often enclosed in quotes and separated by a comma or other "delimiter" character (sometimes a tab, pipe (|), or other symbol that visually separates the values).
2.11.1 Saving Tabular Data Blocks to CSV Files
You can create a CSV file from a block of REBOL table data, using the "foreach" function. Just rejoin each molded value (value enclosed in quotes), with commas separating each item, and a newline after each row, into a long text string. Then save the string to a file with the extension ".csv":
REBOL [title: "Save CSV"]
users: [
"John Smith" "123 Tomline Lane Forest Hills, NJ" "555-1234"
"Paul Thompson" "234 Georgetown Pl. Peanut Grove, AL" "555-2345"
"Jim Persee" "345 Pickles Pike Orange Grove, FL" "555-3456"
"George Jones" "456 Topforge Court Mountain Creek, CO" ""
"Tim Paulson" "" "555-5678"
]
foreach [name address phone] users [
write/append %users.csv rejoin [
mold name ", " mold address ", " mold phone newline
]
]
Try opening the file above with Excel or another spreadsheet application. Because the particular values in this data block contain commas within the address field, you may need to select "comma", "space", and "merge delimiters", or similar options, in programs such as OpenOffice Calc.
2.11.2 Loading Tabular Data Blocks From CSV Files
To import CSV files into REBOL, use the "read/lines" function to read the file contents, with one text line per item stored in the resulting block. Assign the results of the read/lines function to a variable label. Use "foreach" and REBOL's "parse" function to separate each item in the lines back to individual values. Collect all the resulting sequential values into an empty block, and you're ready to use the data in all the ways you've seen so far:
REBOL [title: "Load CSV - Flat"]
block: copy []
csv: read/lines %users.csv
foreach line csv [
data: parse line ","
append block data
]
probe block
foreach [name address phone] block [
alert rejoin [name ": " address " " phone]
]
halt
The first parameter of the parse function is the data to be parsed (in the case above, each line of the CSV file). The second parameter is the delimiter character(s) used to separate each value. Assign a variable to the output of the parse function, and you can refer to each individual value as needed (using "pick" and other series functions). The code above creates a "flat" block. To create a block of blocks, in which each line of the CSV file is delineated into a separate interior (nested) block, just use the append/only function, as you've seen earlier:
REBOL [title: "Load CSV - Block of Blocks"]
block: copy []
csv: read/lines %users.csv
foreach line csv [
data: parse line ","
append/only block data
]
probe block
foreach line block [probe line]
halt
Parse's "/all" refinement can be used to control how spaces and other characters are treated during the text splitting process (for example, if you want to separate the data at commas contained within each quoted text string). You can use the "trim" function to eliminate extra spaces in values. Other functions such as "replace", "to-(value)", and conditional evaluations, for example, can be useful in converting, excluding, and otherwise processing imported CSV data.
Try downloading account data from Paypal, or export report values from your financial software, and you'll likely see that the most prominent format is CSV. Accountants and others who use spreadsheets to crunch numbers will be able to instantly use CSV files in Excel, and/or export worksheet data to CSV format, for you to import and use in REBOL programs.
You'll learn much more about the extremely powerful "parse" function later. For now, it provides a simple way to import data stored in the common CSV format.
2.12 Two Paypal Report Programs, Analyzed
Take a look at the Paypal code examples you've seen so far in this text. You should be able to follow the code a bit now:
REBOL [title: "Paypal Report"]
; A variable used to calculate the sum is initially set to zero dollars:
sum: $0
; A foreach loop goes through every line in the downloaded CSV file,
; starting at the second line (the first line contains columns labels):
foreach line (at (read/lines http://re-bol.com/Download.csv) 2) [
; The sum is computed, using the money value in column 8:
sum: sum + to-money pick (parse/all line ",") 8
]
; The user is alerted with the total:
alert form sum
Here's the whole program, without comments:
REBOL [title: "Paypal Report"]
sum: $0
foreach line (at (read/lines http://re-bol.com/Download.csv) 2) [
sum: sum + to-money pick (parse/all line ",") 8
]
alert form sum
This example deals with several different columns, and performs conditional evaluations on the name and time fields:
REBOL [title: "Paypal Reports"]
; Variables used to calculate 2 different sums are initially set to $0:
sum1: sum2: $0
; A foreach loop goes through every line in the downloaded CSV file,
; starting at the second line (the first line contains columns labels):
foreach line at read/lines http://re-bol.com/Download.csv 2 [
; The first sum is computed, using the money value in column 8:
sum1: sum1 + to-money pick row: parse/all line "," 8
; If the name column (col #4) contains the text "Saoud", print a
; a concatenated message. That message text consists of the date
; (column 1), the characters ", Saoud Gorn: ", and the money value
; in column 8:
if find row/4 "Saoud" [print rejoin [row/1 ", Saoud Gorn: " row/8]]
; If the name column contains "Ourliptef.com", then perform an
; additional conditional evaluation checking if the time field value
; (column 2) is between midnight and noon. If so, add to the sum2
; variable the money value in column 8:
if find row/4 "Ourliptef.com" [
time: to-time row/2
if (time >= 0:00am) and (time <= 12:00pm) [
sum2: sum2 + to-money row/8
]
]
]
; Alert the user with some concatenated messages displaying the sums:
alert join "GROSS ACCOUNT TRANSACTIONS: " sum1
alert join "2012 Ourliptef.com Morning Total: " sum2
Here's the whole program, without comments:
REBOL [title: "Paypal Reports"]
sum1: sum2: $0
foreach line at read/lines http://re-bol.com/Download.csv 2 [
sum1: sum1 + to-money pick row: parse/all line "," 8
if find row/4 "Saoud" [print rejoin [row/1 ", Saoud Gorn: " row/8]]
if find row/4 "Ourliptef.com" [
time: to-time row/2
if (time >= 0:00am) and (time <= 12:00pm) [
sum2: sum2 + to-money row/8
]
]
]
alert join "GROSS ACCOUNT TRANSACTIONS: " sum1
alert join "2012 Ourliptef.com Morning Total: " sum2
To see the data these scripts are sorting through, take a look at the raw data in the Download.csv file.
2.13 Some Perspective about Studying These Topics
The List and Table Data topics are the most difficult sections in the first half of the tutorial. They will likely require several readings to be fully understood. Start by skimming once, and become familiar with the basic language structures and general code patterns. During the first read, you should be aware that demonstrated functions and code snippets can simply be copied, altered, and pasted for use in other applications. You don't need to memorize or even thoroughly understand how each line of code works. Instead, it's more important to understand that the functions and block/table concepts contained here simply exist and produce the described results. You will learn and interalize the details only by rote, through an extended period of reading, studying, copying, altering, and eventually writing fluently. There is a lot of material here to consume. It will likely take numerous applied hours of coding to fully understand it all.
You may find small snippets of code which provide solutions for the initial problem(s) and curiosities that motivated you to "learn how to program". Spend extra time experimenting with those pieces of code that are most interesting and relevant to your immediate needs. Copy, paste, and run examples in the REBOL interpreter. Get used to editing and altering pieces of code to become more familiar with the syntax. Change variable labels, enter new data values, and try repurposing code examples to fit new data sets. Try to break working code and figure out how to fix it. Get used to USING the REBOL text editor and the interpreter console. Get your hands dirty and bury yourself in the mechanics of typing and running code. Becoming comfortable with the tool set and working environment is huge part of the battle.
You will learn REBOL and all other programming languages in the exact same way you would learn any spoken language: by "speaking" it. You must mimic at first (copy/paste code), and then learn to put together "phrases" that make sense (edit, experiment, and rearrange words), and eventually write larger compositions fluently. You will regularly make mistakes with function syntax as you learn to code, just as children make mistakes with grammar as they learn to speak. You'll only learn by experimenting creatively, and by experiencing errors.
It's important not to let initial confusion and error stop you from learning at this point. Use the materials in this section as a reference for looking up functions and syntax as you progress through the tutorial. Try to understand some key points about the structure of the language, especially the code patterns related to block and table operations, but realize that you'll only internalize so much detail during your first read. Acquire as much understanding and experiment with code as much as your curiosity and motivation allows, then move on and see how other important topics fit together to form useful coding skills.
Once you've made it past the next few sections, and in particular the complete programs section, you will have gotten a solid overview of how all the fundamental concepts are put to use. It's a good idea to review the entire first part of the tutorial at that point, paying closer attention to the fine details of each line of code, memorizing functions, immersing yourself in the logic of each word's operation, etc. For now, read and understand the general conceptual overview, and try not to get stuck on any single topic.
For more information about using lists and tables of data in REBOL, see http://www.rebol.com/docs/core23/rebolcore-6.html.
3. Using GUI Windows and Widgets to Input and Display Data
You've already seen how a number of functions can be used to display and request information from the user (print, request-text, request-list, editor, etc.). For simple utilities, these input/output functions are often all that's needed to build functional scripts. To create more complex programs that allow for both increasingly complex data entry, and increased ease of use, "GUI" or Graphic User Interfaces are typically employed. GUIs are windows, forms, and data entry screens that typically contain "widgets" such as text fields, buttons, drop down selectors, multi-line text areas, data grids, menus, and other recognizable visual components. The requestors you've seen so far are very simple types of GUIs, but they only accept single units of data. Windowed GUIs allow users to view and edit multiple fields of data on a single screen. This tends to be more efficient and less error prone than responding to sequential requests for input, and is the "normal" interface expected by users of business applications. GUI coding requires quite a bit of core knowledge in most programming languages. REBOL makes it easy (in fact, REBOL provides absolutely the simplest way to create GUIs with code).
3.1 Basic Layout Guidelines and Widgets
To create a program window, paste the the following code into the REBOL editor and press [F5] to save and run:
REBOL []
view layout [size 600x440]
To center a program window on your computer screen, use "center-face"
REBOL []
view center-face layout [size 600x440]
You can put a title in your program header, which will appear in the title bar of your program window:
REBOL [title: "My Program"]
view center-face layout [size 600x440]
By default, REBOL program windows have a gray backdrop. You can change that using the "backdrop" word:
REBOL [title: "My Program"]
view center-face layout [size 600x440 backdrop white]
Instead of the "backdrop" word, you can use the following code to change the default color for all items in a GUI. This provides a slightly cleaner feel than REBOL's default grey color:
svv/vid-face/color: white
Here's how you add "widgets" (buttons, text fields, multi-line text areas, drop down lists, etc.) to your program window. Notice that everything in the GUI window code is still contained between square brackets, but it has all been indented 4 spaces. Indentation is not required for multi-line block sections, but makes the code easier to read, and is expected. Notice also that the window sizes automatically to fit the contained widgets:
REBOL [title: "My Program"]
svv/vid-face/color: white
view center-face layout [
field "Type Here"
area "Multi^/line^/text"
text-list data ["first" "second" "third"]
image logo.gif ; this image is built into REBOL
btn "Click Me"
]
You can adjust the size, color, and other properties of a widget by including modifiers next to each widget. Notice the GUI window automatically expands to fit resized widgets:
REBOL [title: "My Program"]
svv/vid-face/color: white
view center-face layout [
field 600 "Type Here"
area 600 "Multi^/line^/text"
text-list 600 data ["first" "second" "third"]
image purple logo.gif
btn red 100 "Click Me"
]
By default, REBOL places widgets below one another in the program window. You can align widgets horizontally using the "across" word. You can change back to the default vertical positioning with the word "below":
REBOL []
view center-face layout [
across
text-list 194
text-list 194
text-list 194
below
field 600
area 600
across
text "" 368
btn 50 "First"
btn 50 "Next"
btn 50 "Prev"
btn 50 "Last"
]
You can change the default starting position of widgets placed on screen using the "origin" word, and adjust default spacing using the "space" word:
REBOL []
view center-face layout [
size 251x251
origin 0x0
space 100x100
across
btn 50x50
btn 50x50
return
btn 50x50
btn 50x50
origin 50x50
btn 50x50
btn 50x50
return
btn 50x50
btn 50x50
]
You can place widgets at a specific coordinate using the "at" word:
REBOL []
view center-face layout [
at 20x50 btn
at 70x100 btn
at 130x150 btn
]
3.2 Breathing Life Into GUI Programs - Performing Actions
Put function words in a block (between square brackets) after GUI widgets, and that function's action will be performed whenever the widget is clicked with a mouse, submitted with the keyboard, or otherwise activated. Notice that the word "value" holds the current/selected value in each widget:
REBOL [title: "My Program"]
svv/vid-face/color: white
view center-face layout [
field "Type Here" [alert value]
area "Multi^/line^/text" [alert value]
text-list data ["first" "second" "third"] [alert value]
image logo.gif [alert "Nice logo"]
btn "Click Me" [alert "Clicked"]
]
You can give any widget a variable label and change a labeled widget's text using the "/text" refinement, followed by a colon. Whenever you make changes to a window's appearance, you must use the "show" function to update the display:
REBOL []
view layout [
size 600x400
field1: field "field 1" ; this field is labeled "field1"
btn "change field1's text" [
; These actions occur when the button is pressed:
field1/text: "You just changed field 1's text!"
show field1
]
]
You can read text from a file into a text area, using the "read" function. In REBOL, file names are always preceded by the percent symbol ("%"):
REBOL []
view layout [
a: area ; this area is labeled "a"
btn "Read" [
; When btn is clicked, a's text is set to data read from file:
a/text: read %temp.txt
show a
]
]
You can write text from a text area to a file, using the "write" function:
REBOL []
view layout [
a: area
btn "Save" [
; When btn is clicked, write to temp.txt file, the text in area:
write %temp.txt a/text
alert "Saved"
]
]
IMPORTANT: GUI text fields are only able to display text ("string") values. Note that the following program produces errors because the values returned by the requestors are NOT string values, but rather other data types recognized by REBOL (file, date, tuple, etc. values):
REBOL []
view layout [
btn "File" [
f1/text: request-file
show f1
]
f1: field
btn "Date" [
f2/text: request-date
show f2
]
f2: field
btn "Color" [
f3/text: request-color
show f3
]
f3: field
]
When copying formatted text values into a text area, use the "form" function to convert data to a text string:
REBOL []
view layout [
btn "File" [
; when btn pressed, set text of field 1 to selected file name:
f1/text: form request-file ; FORM converts file name to text
show f1
]
f1: field
btn "Date" [
; set text of field 2 to selected date:
f2/text: form request-date ; FORM converts date value to text
show f2
]
f2: field
btn "Color" [
; set text of field 3 to selected color:
f3/text: form request-color ; FORM converts color value to text
show f3
]
f3: field
]
You can change other properties of a widget, beyond just the text. Change the coordinate position of a widget using the "/offset" refinement, change it's size using the "/size" refinement, just as you alter it's text using the "/text" refinement. The word "face" allows a widget to refer to itself. In the code below, a button widget changes it's own position, size, and text when clicked:
REBOL []
view layout [
size 594x440
btn "click me" [
face/offset: 200x300
face/size: 150x50
face/text: "I've moved and changed!"
show face
]
]
The "style" word allows you to create new widgets with predefined properties and actions. Here, the label "green-button" is defined as a green btn widget with the text "click me", which when clicked, jumps to a random coordinate within the range 580x420:
REBOL []
view layout [
size 594x440
style green-button btn green "click me" [
face/offset: random 580x420
show face
]
; The word "green-button" now refers to all the above code. Every
; "green-button" shares the same color and text properties, and
; PERFORMS THE SAME ACTIONS when clicked.
at 254x84 green-button
at 19x273 green-button
at 85x348 green-button
at 498x12 green-button
at 341x385 green-button
]
Here's a little puzzle example, with detailed comments describing the layout and thought processes behind every action in the code:
REBOL [title: "Sliding Puzzle"]
; Create a GUI that's centered on the user's screen:
view center-face layout [
; Define some basic layout parameters. "origin 0x0"
; starts the layout in the upper left corner of the
; GUI window. "space 0x0" dictates that there's no
; space between adjacent widgets, and "across" lays
; out consecutive widgets next to each other:
origin 0x0 space 0x0 across
; The section below creates a newly defined button
; widget called "piece", with an action block that
; swaps the current button's position with that of
; the adjacent empty space. That action is run
; whenever one of the buttons is clicked:
style piece button 60x60 [
; The lines below check to see if the clicked button
; is adjacent to the empty space. The "offset"
; refinement contains the position of the given
; widget. The word "face" is used to refer to the
; currently clicked widget. The "empty" button is
; defined later (at the end of the GUI layout).
; It's ok that the empty button is not yet defined,
; because this code is not evaluated until the
; the entire layout is built and "view"ed:
distance: (face/offset - empty/offset)
if not find [0x60 60x0 0x-60 -60x0] distance [exit]
; In English, that reads 'subtract the position of
; the empty space from the position of the clicked
; button (the positions are in the form of
; Horizontal x Vertical coordinate pairs). If that
; difference isn't 60 pixels on one of the 4 sides,
; then don't do anything.' (60 pixels is the size of
; the "piece" button defined above.)
; The next three lines swap the positions of the
; clicked button with the empty button.
; First, create a variable to hold the current
; position of the clicked button:
temp: face/offset
; Next, move the button's position to that of the
; current empty space:
face/offset: empty/offset
; Last, move the empty space (button), to the old
; position occupied by the clicked button:
empty/offset: temp
]
; The lines below draw the "piece" style buttons onto
; the GUI display. Each of these buttons contains all
; of the action code defined for the piece style above:
piece "1" piece "2" piece "3" piece "4" return
piece "5" piece "6" piece "7" piece "8" return
piece "9" piece "10" piece "11" piece "12" return
piece "13" piece "14" piece "15"
; Here's the empty space. Its beveled edge is removed
; to make it look less like a movable piece, and more
; like an empty space:
empty: piece 200.200.200 edge [size: 0]
]
Here's the whole program without comments. It's tiny:
REBOL [title: "Sliding Puzzle"]
view center-face layout [
origin 0x0 space 0x0 across
style piece button 60x60 [
if not find [0x60 60x0 0x-60 -60x0] (face/offset - e/offset)[exit]
temp: face/offset
face/offset: e/offset
e/offset: temp
]
piece "1" piece "2" piece "3" piece "4" return
piece "5" piece "6" piece "7" piece "8" return
piece "9" piece "10" piece "11" piece "12" return
piece "13" piece "14" piece "15"
e: piece 200.200.200 edge [size: 0]
]
3.3 GUI Language Reference
Here are all the main GUI words built in to REBOL's GUI dialect (called "VID") that you should get to know. The first block of "styles" contains all the predefined widgets available. The layout words affect how and where items are positioned, and other layout preferences. The attribute words adjust the appearance and function of widgets. The style facets adjust some specific options that are available for individual widgets:
STYLES-WIDGETS: [
face blank-face IMAGE BACKDROP BACKTILE BOX BAR SENSOR KEY BASE-TEXT
VTEXT TEXT BODY TXT BANNER VH1 VH2 VH3 VH4 LABEL VLAB LBL LAB TITLE
H1 H2 H3 H4 H5 TT CODE BUTTON CHECK CHECK-MARK RADIO CHECK-LINE
RADIO-LINE LED ARROW TOGGLE ROTARY CHOICE DROP-DOWN ICON FIELD INFO
AREA SLIDER SCROLLER PROGRESS PANEL LIST TEXT-LIST ANIM BTN BTN-ENTER
BTN-CANCEL BTN-HELP LOGO-BAR TOG
]
LAYOUT-WORDS: [
return at space pad across below origin guide tab tabs indent style
styles size backcolor backeffect do
]
STYLE-FACETS--ATTRIBUTES: [
edge font para doc feel effect effects keycode rate colors texts help
user-data with bold italic underline left center right top middle
bottom plain of font-size font-name font-color wrap no-wrap as-is
shadow frame bevel ibevel
]
SPECIAL-STYLE-FACETS: [
ARROW: [up right down left] ROTARY: data CHOICE: data DROP-DOWN:
[data rows] FIELD: hide INFO: hide AREA: hide LIST: [supply map
data] TEXT-LIST: data ANIM: [frames rate]
]
You can obtain the word lists above using the following lines of code:
probe extract svv/vid-styles 2
probe remove-each i copy svv/facet-words [function? :i]
probe svv/vid-words
By default, all REBOL GUIs contain the text "REBOL - " in the window title bar. In Windows, you can eliminate that text with the following code. Just set the "tt" variable to hold the title text you want displayed:
tt: "Your Title"
user32.dll: load/library %user32.dll
gf: make routine![return:[int]]user32.dll"GetFocus"
sc: make routine![hw[int]a[string!]return:[int]]user32.dll"SetWindowTextA"
so: :show show: func[face][so[face]hw: gf sc hw tt]
The widgets and techniques you've seen so far are enough to create an overwhelming majority of potentially useful windowed business applications. Here's a collection of useful pieces of code demonstrating how to accomplish various common tasks in GUIs, and some other available widgets. Every piece of code in these examples will be explained in greater detail later in the tutorial. For now, just paste and run these examples to see what they do, and keep the code handy for use when needed:
REBOL [title: "GUI Reference"]
print "GUI Output:^/"
view center-face layout [
h1 "Some More GUI Widgets:"
box red 500x2
drop-down 200 data system/locale/months [
a/text: join "Month: " value show a
]
a: field
slider 200x18 [bar1/data: value show bar1]
bar1: progress
scroller 200x16 [bar2/data: value show bar2]
bar2: progress
across
toggle "Click here" "Click again" [print value]
rotary "Click" "Again" "And Again" [print value]
choice "Choose" "Item 1" "Item 2" "Item 3" [print value]
return
x: radio y: radio z: radio
btn "Get Radio States" [print [x/data y/data z/data]]
return
led
arrow
below
code "Code text"
tt "Typewriter text"
text "Little Text" font-size 8
title "Centered title" 500
]
; The word "value" refers to data contained in a currently active widget:
view layout [
text "Some widgets with values and size/color properties. Try them:"
button red "Click Me" [alert "You clicked the red button."]
f: field 400 "Type some text here, then press the [Enter] key" [
alert value ; SAME AS alert f/text
]
t: text-list 400x300 "Select this line" "Then this one" "Now this" [
alert value ; SAME AS alert t/text
]
check yellow [alert "You clicked the yellow check box."]
button "Quit" [alert "I don't want to stop yet!"]
]
; List Widget:
y: read %. c: 0 x: copy []
foreach i y [append/only x reduce [(c: c + 1) i (size? to-file i)]]
slider-pos: 0
view center-face layout [
across space 0
the-list: list 400x400 [
across space 0x0
text 50 purple
text 250 bold [editor read to-file face/text]
text 100 red italic
return box green 400x1
] supply [
count: count + slider-pos
if none? q: pick x count [face/text: none exit]
face/text: pick q index
]
scroller 16x400 [
slider-pos: (length? x) * value
show the-list
]
]
view layout [
h3 "Just a few effects - fit, flip, emboss:"
area 400x400 load http://rebol.com/view/bay.jpg effect [
Fit Flip Emboss ; you can fit images on most widgets
]
]
effects: [
invert contrast 40 colorize 0.0.200 gradcol 1x1 0.0.255 255.0.0
tint 100 luma -80 multiply 80.0.200 grayscale emboss flip 0x1
flip 1x0 rotate 90 reflect 1x1 blur sharpen aspect tile tile-view
]
view layout [
area 400x400 wrap rejoin [
"And there are MANY more effects:" newline newline form effects
]
]
view layout [area effect [gradient red blue]] ; gradients are color fades
view layout [
size 500x400
backdrop effect [gradient 1x1 tan brown]
box effect [gradient 123.23.56 254.0.12]
box effect [gradient blue gold/2]
]
view layout [
btn "Right/Left Click Me" [alert "left click"] [alert "right click"]
]
panels: layout [
across
btn "Fields" [window/pane: pane1 show window]
btn "Text List" [window/pane: pane2 show window]
return
window: box 400x200
]
pane1: layout/tight [field 400 field 400 area 400]
pane2: layout/tight [text-list 400x200 data system/locale/days]
window/pane: pane1
view center-face panels
svv/vid-face/color: white
alert "New global background color is now white."
; The word "offset" refers to a widget's coordinate position.
; The word "style" builds a new widget with the specified style & actions:
view center-face layout [
size 600x440
h3 "Press the left or right arrow key"
key keycode [left] [alert "You pressed the LEFT arrow key"]
key keycode [right] [alert "You pressed the RIGHT arrow key"]
btn #"a" "Click Me or Press the 'a' Key" [alert "clicked or pressed"]
]
; Here's a little program to show all key codes:
insert-event-func func [f e] [if e/type = 'key [print mold e/key] e]
view layout [text "Type keys to see their character/keycode"]
; How to refer to the main layout window:
view gui: layout [
btn1: btn "Button 1"
btn2: btn "Remove all widgets from window" [
foreach item system/view/screen-face/pane/1/pane [
remove find system/view/screen-face/pane/1/pane item
]
show gui
]
]
; "Feel" and "Engage" together detect events:
view layout [
text "Mouse me." feel [
engage: func [face action event] [
if action = 'up [print "You just released the mouse."]
]
]
]
print "Click anywhere in the window, then click the text."
view center-face layout [
size 400x200
box 400x200 feel [
engage: func [f a e] [ ; f a e = face action event
print rejoin ["Mouse " a " at " e/offset]
]
]
origin
text "Click me" [print "Text clicked"] [print "Text right-clicked"]
box blue [print "Box clicked"]
]
movestyle: [ ; generic click and drag code
engage: func [f a e] [
if a = 'down [
initial-position: e/offset
remove find f/parent-face/pane f
append f/parent-face/pane f
]
if find [over away] a [
f/offset: f/offset + (e/offset - initial-position)
]
show f
]
]
view layout/size [
style moveable-object box 20x20 feel movestyle
at random 600x400 moveable-object (random 255.255.255)
at random 600x400 moveable-object (random 255.255.255)
at random 600x400 moveable-object (random 255.255.255)
at random 600x400 moveable-object (random 255.255.255)
at random 600x400 moveable-object (random 255.255.255)
text "This text and all the boxes are movable" feel movestyle
] 600x440
; The following box code creates a repeating, multitasking loop in a GUI.
; The "within" function checks for graphic collisions:
view center-face layout [
size 400x400
btn1: btn red
at 175x175 btn2: btn green
box 0x0 rate 0 feel [engage: func [f a e] [if a = 'time [
btn1/offset: btn1/offset + 5x5
show btn1
if within? btn1/offset btn2/offset 1x1 [alert "Collision" unview]
]]]
]
view center-face layout [ ; follow all mouse movements
size 600x440
at 270x209 b: btn "Click Me - Aha!" feel [
detect: func [f e] [
if e/type = 'move [
if (within? e/offset b/offset 59x22) [
b/offset: b/offset + ((random 50x50) - (random 50x50))
if not within? b/offset -59x-22 659x462 [
b/offset: 270x209
]
show b
]
]
e
]
]
]
; To trap other events (this example traps and responds to close events):
closer: insert-event-func [
either event/type = 'close [
really: request "Really close the program?"
if really = true [remove-event-func :closer]
] [event] ; always return other events
]
view center-face layout [
text "Close me"
size 600x400
]
insert-event-func [ ; this example traps resize events
either event/type = 'resize [
fs: t1/parent-face/size
t1/offset: fs / 2x2
t2/offset: t1/offset - 50x25
t3/offset: t1/offset - 25x50
show gui none
] [event]
]
svv/vid-face/color: white
view/options gui: layout [
text "Centered in resized window:"
across
t1: text "50x50"
t2: text "- 50x25"
t3: text "- 25x50"
] [resize]
; Use "to-image" to create a SCREEN SHOT of any layout:
picture: to-image layout [
page-to-read: field "http://rebol.com"
btn "Display HTML"
]
save/png %layout.png picture ; save the image to a file
browse %layout.png
flash "Just waiting..." wait 3 alert "Done waiting!" unview
inform layout [btn "Click Me" [flash "Just waiting..." wait 3 unview]]
; Embed files (images, sounds, etc.) in code:
alert "Select a picture from your hard drive:"
system/options/binary-base: 64
editor picture: compress to-string read/binary to-file request-file/only
view layout [image load (to-binary decompress picture)]
; This example embedded image was created with the script above:
logo-pic: load to-binary decompress #{
789C018A0375FC89504E470D0A1A0A0000000D49484452000000640000001808
020000008360CFB90000001374455874536F667477617265005245424F4C2F56
6965778FD916780000033249444154789CD599217402310C86F7CE6227B1481C
1637874362B1382C1687C4A15168240A89C5A2B058ECDEBE47DFFA429276DCEE
10FDCD582F97267FD33F2D7CF47ABDCF32D1ED76E7F3F9ED76FB4EE0743A8D46
A3B6A683A80FFE540562381C1E8FC7144D12DBEDB6951C3B9D4E91648DC7E34C
41B925465D349C14A2CA230BA65EA729E27C3E37CCB43CB228905A3525B1DBED
9A4CED93851C7C193088A0667C0D0603FB5640BFDFB7F648C0D0836B1C41C22E
11D7EBF57038F074BFDF534429BE2693891B4626CE1C59BC7CB95CDC99EEF7FB
66B349F922D65A4B4A8DE0D0B547B9DD85212B6B4CB4D3E994B055FEE8943566
30134626BBDA64052C974BD757A637B1DA2E599959A05EE61F4032D62C55EFBC
6EED01878954188DC80AE714C07126D24F91BBBE6265A129B3D96C2A4085BB64
459FEBF51A1B2692E5A9FA17A428B562EBE595A1F29650AD5C6B9525FD4621E0
A95D73491606F9046C94101A06178B4518E19122023655DA184B03ECA15BE98E
6D9D302E536E8D2C96A5FF0061458FEE9EAA045958720EDCFC82CF145A9E2C7C
52BC6CF0503B8C2B2200DAACD24698A4B710361E6421930E05A85E9484BE51B3
0885AE9727CB22A5591981B73D1AC6A58D2ABD5892DF46C5993DCFF25BC8828E
14538AACEB3390A43C59D890213B5D2AA3D2AC3C59ABD54ACE2E85C29E36DE42
162B8C0AC47F0942B512972CCCF0D91170ED6594ECC130288549ED44744DE52C
771381C571D5AFEDB14B2E79CB022F13C834A056049EFCE35C2A7449877A2B00
2D872635082FEA2D267D8BC047AD910D3875CE9247078A826259FC8234F264E1
9FAD4AAC52015465D973193B3755B611B417FB562A0C66C77EF7001F5463FD83
2CF20F83B2B8E0C22DAE760FA556B32AAF87B86A18C18259CFAA3567C250C7C3
1AE72CD95350531BD93FAE3B6CEADB33188174FCBBD77B7B7A0841DAB6C3EBEE
F13DE8696B6455E222ADCE23F162ECF644064709A47AA8FD3632BFAD78EA5E92
D947500C3BB04CAD419F3D5B05580DC127118E3D2866CAFB8AC6CAFCEB68F895
56796455CF47AAD741F5B957D4D751245980BD569729B723D742A964558FFB4D
EAB6A440BF6ACE54157EB028F7A730B695BDF749D05EA9C1B612C4CF0F396EDC
8E943F5C020000000049454E44AE426082CAEBA2D78A030000
}
view layout [image logo-pic]
write/append %s "" ; A very compact GUI program
view center-face g: layout [
h3 "Name:" x: field h3 "Info:" z: area wrap across
btn "Save" [do-face d 1 save %s repend f [x/text z/text]]
btn "Load" [
c: request-list" Select:" extract (f: load %s) 2
if c = none [return]
x/text: first find f c z/text: select f x/text show g
]
btn "New" [x/text: copy "" z/text: copy "" show g focus x]
d: btn "Delete" [
if true = request "Sure?" [
remove/part (find (f: load %s) x/text) 2 save %s f alert "ok"
]
]
]
; Some examples of the "draw" dialect for creating graphics:
view layout [
box 400x400 black effect [
draw [
pen red
line 0x400 400x50
pen white
box 100x20 300x380
fill-pen green
circle 250x250 100
pen blue
fill-pen orange
line-width 5
spline closed 3 20x20 200x70 150x200
polygon 20x20 200x70 150x200 50x300
]
]
]
view layout [
box 400x220 effect [
draw [
fill-pen 200.100.90
polygon 20x40 200x20 380x40 200x80
fill-pen 200.130.110
polygon 20x40 200x80 200x200 20x100
fill-pen 100.80.50
polygon 200x80 380x40 380x100 200x200
]
gradmul 180.180.210 60.60.90
]
]
view layout [
h3 "Draw On Me:"
scrn: box black 400x400 feel [
engage: func [face action event] [
if find [down over] action [
append scrn/effect/draw event/offset
show scrn
]
if action = 'up [append scrn/effect/draw 'line]
]
] effect [draw [line]]
]
pos: 300x300
view layout [
scrn: box pos black effect [
draw [image logo.gif 0x0 300x0 300x300 0x300]
]
btn "Animate" [
for point 1 450 4 [
scrn/effect/draw: copy reduce [
'image logo.gif
(pos - 300x300)
(1x1 + (as-pair 300 point))
(pos - (as-pair 1 point))
(pos - 300x0)
]
show scrn
]
scrn/effect/draw: copy [
image logo.gif 0x0 300x0 300x300 0x300
]
show scrn
]
]
See http://rebol.com/docs/easy-vid.html and http://rebol.com/docs/view-guide.html for some additional information and examples demonstrating basic GUI techniques.
3.4 A Telling Comparison
To provide a quick idea of how much easier REBOL is than other languages, here's a short example. The following code to create a basic program window with REBOL was presented earlier:
view layout [size 400x300]
It works on every type of computer, in exactly the same way.
Code for the same simple example is presented below in the C++ language. It does the exact same thing as the REBOL one-liner above, except it only works on Microsoft Windows machines. If you want to do the same thing with a Macintosh computer, you need to memorize a completely different page of C++ code. The same is true for Linux or any other operating system. You have to learn enormous chunks of code to do very simple things, and those chunks of code are different for every type of computer. Furthermore, you typically need to spend a semester's worth of time learning very basic things about code syntax and fundamentals about how a computer 'thinks' before you even begin to tackle useful basics like the code below:
#include <windows.h>
/* Declare Windows procedure */
LRESULT CALLBACK WindowProcedure (HWND, UINT, WPARAM, LPARAM);
/* Make the class name into a global variable */
char szClassName[ ] = "C_Example";
int WINAPI
WinMain (HINSTANCE hThisInstance,
HINSTANCE hPrevInstance,
LPSTR lpszArgument,
int nFunsterStil)
{
HWND hwnd;
/* This is the handle for our window */
MSG messages;
/* Here messages to the application are saved */
WNDCLASSEX wincl;
/* Data structure for the windowclass */
/* The Window structure */
wincl.hInstance = hThisInstance;
wincl.lpszClassName = szClassName;
wincl.lpfnWndProc = WindowProcedure;
/* This function is called by windows */
wincl.style = CS_DBLCLKS;
/* Catch double-clicks */
wincl.cbSize = sizeof (WNDCLASSEX);
/* Use default icon and mouse-pointer */
wincl.hIcon = LoadIcon (NULL, IDI_APPLICATION);
wincl.hIconSm = LoadIcon (NULL, IDI_APPLICATION);
wincl.hCursor = LoadCursor (NULL, IDC_ARROW);
wincl.lpszMenuName = NULL;
/* No menu */
wincl.cbClsExtra = 0;
/* No extra bytes after the window class */
wincl.cbWndExtra = 0;
/* structure or the window instance */
/* Use Windows's default color as window background */
wincl.hbrBackground = (HBRUSH) COLOR_BACKGROUND;
/* Register window class. If it fails quit the program */
if (!RegisterClassEx (&wincl))
return 0;
/* The class is registered, let's create the program*/
hwnd = CreateWindowEx (
0,
/* Extended possibilites for variation */
szClassName,
/* Classname */
"C_Example",
/* Title Text */
WS_OVERLAPPEDWINDOW,
/* default window */
CW_USEDEFAULT,
/* Windows decides the position */
CW_USEDEFAULT,
/* where the window ends up on the screen */
400,
/* The programs width */
300,
/* and height in pixels */
HWND_DESKTOP,
/* The window is a child-window to desktop */
NULL,
/* No menu */
hThisInstance,
/* Program Instance handler */
NULL
/* No Window Creation data */
);
/* Make the window visible on the screen */
ShowWindow (hwnd, nFunsterStil);
/* Run the message loop.
It will run until GetMessage() returns 0 */
while (GetMessage (&messages, NULL, 0, 0))
{
/* Translate virtual-key messages
into character messages */
TranslateMessage(&messages);
/* Send message to WindowProcedure */
DispatchMessage(&messages);
}
/* The program return-value is 0 -
The value that PostQuitMessage() gave */
return messages.wParam;
}
/* This function is called by the Windows
function DispatchMessage() */
LRESULT CALLBACK
WindowProcedure (HWND hwnd, UINT message,
WPARAM wParam, LPARAM lParam)
{
switch (message)
/* handle the messages */
{
case WM_DESTROY:
PostQuitMessage (0);
/* send a WM_QUIT to the message queue */
break;
default:
/* for messages that we don't deal with */
return DefWindowProc (hwnd, message,
wParam, lParam);
}
return 0;
}
Yuck. Back to REBOL...
4. Quick Review and Clarification
The list below summarizes some key characteristics of the REBOL language. Knowing how to put these elements to use constitutes a fundamental understanding of how REBOL works:
- To start off, REBOL has hundreds of built-in function words that perform common tasks. As in other languages, function words are typically followed by passed data parameters. Unlike other languages, passed parameters are placed immediately after the function word and are not necessarily enclosed in parentheses. To accomplish a desired goal, functions are arranged in succession, one after another. The value(s) returned by one function are often used as the argument(s) input to another function. Line terminators are not required at any point, and all expressions are evaluated in left to right order, then vertically down through the code. Empty white space (spaces, tabs, newlines, etc.) can be inserted as desired to make code more readable. Text after a semicolon and before a new line is treated as a comment. You can complete significant work by simply knowing the predefined functions in the language, and organizing them into a useful order.
- REBOL contains a rich set of conditional structures, which can be used to manage program flow and data processing activities. If, either, and other typical structures are supported.
- Because many common types of data values are automatically recognized and handled natively by REBOL, calculating, looping, and making conditional decisions based upon data content is straightforward and natural to perform, without any external modules or toolkits. Numbers, text strings, money values, times, tuples, URLs, binary representations of images, sounds, etc. are all automatically handled. REBOL can increment, compare, and perform proper computations on most common types of data (i.e., the interpreter automatically knows that 5:32am + 00:35:15 = 6:07:15am, and it can automatically apply visual effects to raw binary image data, etc.). Network resources and Internet protocols (http documents, ftp directories, email accounts, dns services, etc.) can also be accessed natively, just as easily as local files. Data of any type can be written to and read from virtually any connected device or resource (i.e., "write %file.txt data" works just as easily as "write ftp://user:pass@website.com data", using the same common syntax). The percent symbol ("%") and the syntax "%(/drive)/path/path/.../file.ext" are used cross-platform to refer to local file values on any operating system.
- Any data or code can be assigned a word label. The colon character (":") is used to assign word labels to constants, variable values, evaluated expressions, and data/action blocks of any type. Once assigned, variable words can be used to represent all of the data contained in the given expression, block, etc. Just put a colon at the end of a word, and thereafter it represents all the following data.
- Multiple pieces of data are stored in "blocks", which are delineated by starting and ending brackets ("[]"). Blocks can contain data of any type: groups of text strings, arrays of binary data, other enclosed blocks, etc. Data items contained in blocks are separated by white space. Blocks can be automatically treated as lists of data, called "series", and manipulated using built-in functions that enable searching, sorting, ordering, and otherwise organizing the blocked data. Blocks are used to delineate most of the syntactic structures in REBOL (i.e., actions resulting from conditional evaluations, GUI widget layouts, etc.).
- "Foreach" function can be used to process lists and tables of data. Rows and columns if data in tables can be processed using the format: "foreach [col1 col2 col3...] table-block [do something to each row pof values]". Data can be saved to CSV files by rejoining molded (quoted) text and delimiters. CSV files can be read using the "read/lines" and "parse" functions. Sorting data by column with the sort/compare function requires that rows be saved in nested blocks, rather than as "flat" sequential lists of items. Data stored as (quoted) text string values is sorted alphabetically. Data stored or converted to specific data type values is sorted appropriately for the type.
- The syntax "view layout [block]" is used to create basic GUI layouts. You can add widgets to the layout simply by placing widget identifier words inside the block: "button", "field", "text-list", etc. Color, position, spacing, and other facet words can be added after each widget identifier. Action blocks added immediately after any widget will perform the enclosed functions whenever the widget is activated (i.e., when the widget is clicked with a mouse, when the enter key pressed, etc.). Path refinements can be used to refer to items in the GUI layout (i.e., "face/offset" refers to the position of the selected widget face, "face/text" to it's text, etc.). Those simple guidelines can be used to create useful GUIs for data input and output, in a way that's native (doesn't require any external toolkits) and much easier than any other language.
5. SOME COMPLETE GUI APPLICATION EXAMPLES
The examples in this section were presented at the very beginning of the tutorial as demonstrations. At this point in the tutorial, you should now be able to understand every bit of code in each program! Every example in this section is documented with detailed line-by-line explanations of what each function, variable, and language construct does. Run every example in the REBOL interpreter, and read every line of code, along with all the comments. Really pay attention to the material in this section - it's one of the most formative in the entire text. Not only are the applications useful as a basis for more personalized production pieces of software, the logic and code patterns demonstrated here will form a strong fundamental understanding about how to create other imagined pieces of software.
5.1 Generic Text Field Saver
Little programs like this form the initial basis for all types of simple data entry app. The entered data is stored in CSV file format, so it can be easily opened by a spreadsheet or other program:
REBOL [title: "Text Field Saver"]
; The words "view layout" create a GUI window:
view layout [
; Create 3 text fields widgets labeled f1, f2, and f3:
f1: field
f2: field
f3: field
; Create a button widget:
btn "Save" [
; When the button is clicked, write to the file fields.txt some
; rejoined text. The /append refinement of the write function
; ensures that data is ADDED to the end of the existing text file,
; instead of erasing the file and writing totally new data to it.
; If the fields.csv file doesn't exist, it's created:
write/append %fields.csv rejoin [
; The "mold" function surrounds text with quotes.
; So the concatenated text written to the fields.txt file
; includes the quoted text from each field widget above,
; each separated by a comma and a quote, and completed with
; a carriage return:
mold f1/text ", " mold f2/text ", " mold f3/text newline
]
; Alert the user when the data has been saved:
alert "Saved"
]
]
Here's the whole program without comments. It's tiny:
REBOL [title: "Text Field Saver"]
view layout [
f1: field
f2: field
f3: field
btn "Save" [
write/append %fields.txt rejoin [
mold f1/text ", " mold f2/text ", " mold f3/text newline
]
alert "Saved"
]
]
5.2 Calculator
This calculator is as basic as could be, but adding advanced math functions and other useful capabilities is easy. Imagine adding industry specific operations such as amortization calculations. The potential to add genuinely useful unique features is limitless (see the next example below, which performs currency conversion operations that draw from current live rates on the Internet, for example). At this point, try to understand the fundamental layout and operation of the GUI widgets:
REBOL [title: "Calculator"]
; Create a GUI Window:
view layout [
; Set the layout properties so that widgets are placed immediately
; next to one another, starting at the top left corner of the screen:
origin 0 space 0x0 across
; Here's a text field, labeled "f". It's size is 200 pixels across
; and 40 pixels down. The font size of text in the field is set to
; 20. After this field widget, the "return" word is used to jump
; to the beginning of a new line:
f: field 200x40 font-size 20 return
; The "style" word below is used to create a new widget called "btn",
; which is a button, sized 50 pixels by 50 pixels. When clicked, the
; button appends the text on it's face to the text field widget above,
; labeled "f", then the display is updated using the "show" function:
style btn btn 50x50 [append f/text face/text show f]
; Below the field widget are 4 lines of buttons, displaying either
; numbers or operators on their face. Each of these buttons performs
; the actions defined above in the "style" definition (appends its
; face text to the field widget display):
btn "1" btn "2" btn "3" btn " + " return
btn "4" btn "5" btn "6" btn " - " return
btn "7" btn "8" btn "9" btn " * " return
btn "0" btn "." btn " / " btn "=" [
; When the "=" button is pressed, the "f" field text is set to the
; result of the expression displayed in the "f" field text (the
; evaluation is performed using the "do" function), and the
; display is updated with the "show" function. Remember, the
; "form" function is used to convert the result to a text string
; value, which is the only type of data that text field widgets
; display. The "attempt" function is used to keep the program
; from crashing if the user tries to enter an illegal expression,
; such as division by zero or incomplete expressions (i.e., 1 + ):
attempt [f/text: form do f/text show f]
]
]
Here's the whole program, without comments:
REBOL [title: "Calculator"]
view layout [
origin 0 space 0x0 across
f: field 200x40 font-size 20 return
style btn btn 50x50 [append f/text face/text show f]
btn "1" btn "2" btn "3" btn " + " return
btn "4" btn "5" btn "6" btn " - " return
btn "7" btn "8" btn "9" btn " * " return
btn "0" btn "." btn " / " btn "=" [
attempt [f/text: form do f/text show f]
]
]
The following example downloads and parses the current (live) US Dollar exchange rates from http://x-rates.com and allows the user to select from a list of currencies to convert to, then performs and displays the conversion from USD to the selected currency. This example will likely be a bit too advanced to understand completely at this point in the tutorial, but it's good to run it in the REBOL interpreter and browse through the code to recognize functions such as "read", "find", "parse", "attempt", etc., to which you've already been introduced. See if you can get a general concept of what the code is doing:
REBOL [title: "Currency Rate Conversion Calculator"]
view center-face layout [
origin 0 space 0x0 across
f: field 200x40 font-size 20
return
style btn btn 50x50 [append f/text face/text show f]
btn "1" btn "2" btn "3" btn " + " return
btn "4" btn "5" btn "6" btn " - " return
btn "7" btn "8" btn "9" btn " * " return
btn "0" btn "." btn " / " btn "=" [
attempt [f/text: form do f/text show f]
] return
btn 200x35 "Convert" [
x: copy []
html: read http://www.x-rates.com/table/?from=USD&amount=1.00
html: find html "src='/themes/bootstrap/images/xrates_sm_tm.png'"
parse html [
any [
thru {from=USD} copy link to {</a>} (append x link)
] to end
]
rates: copy []
foreach rate x [
parse rate [thru {to=} copy c to {'>}]
parse rate [thru {'>} copy v to end]
if not error? try [to-integer v] [append rates reduce [c v]]
]
currency: request-list "Select Currency:" extract rates 2
rate: to-decimal select rates currency
attempt [alert rejoin [currency ": " (rate * to-decimal f/text)]]
]
]
5.3 File Editor
Next is an example of a text editor that allows you to read, edit, and save any text file. Here's the GUI layout code. It consists of a "h1" header text widget displaying the text "Text Editor:", a 600 pixel wide field widget labeled "f" containing the text "filename.txt", a 600x350 pixel area widget labeled "a", and two buttons aligned across the screen (next to each other):
REBOL []
view layout [
h1 "Text Editor:"
f: field 600 "filename.txt"
a: area 600x350
across
btn "Load" []
btn "Save" []
]
And here's the full code that makes it run:
REBOL []
view layout [
h1 "Text Editor:"
f: field 600 "filename.txt"
a: area 600x350
across
btn "Load" [
; When the load button is pressed, request a file name from the
; user, and display it in the "f" field. Be sure to update the
; display using the "show" function:
f/text: request-file
show f
; In order to load the file, the text version of file name
; displayed in the field must be converted from text to a file
; value:
filename: to-file f/text
; Set text in the area widget to the data read from file location,
; and update the display:
a/text: read filename
show a
]
btn "Save" [
; When the save button is clicked, request a file name from the
; user. The default file name shown in the requestor is the text
; currently displayed in the "f" field:
filename: to-file request-file/save/file f/text
; Write to the selected file name, the text contained in the "a"
; area widget:
write filename a/text
; Alert the user when the file is saved:
alert "Saved"
]
]
Here's the whole program without comments:
REBOL [title: "Text Editor"]
view layout [
h1 "Text Editor:"
f: field 600 "filename.txt"
a: area 600x350
across
btn "Load" [
f/text: request-file
show f
filename: to-file f/text
a/text: read filename
show a
]
btn "Save" [
filename: to-file request-file/save/file f/text
write filename a/text
alert "Saved"
]
]
5.4 Web Page Editor
REBOL can read and write to FTP (web site) servers just as easily as it can to local files. All you need to know is an account username/password, folder location on the server (often "public_html"), and a file name to edit. Here's a variation of the program above repurposed as a web page editor:
REBOL [title: "Web Page Editor"]
view layout [
h1 "Web Page Editor:"
f: field 600 "ftp://user:pass@site.com/public_html/file.txt"
a: area 600x350
across
btn "Load" [
; Be sure to convert the file name text in "a" area widget to a
; URL data value. Set text in the area widget to the data read
; from the URL location:
a/text: read to-url f/text
show a
]
btn "Save" [
; Covert the text in "a" area widget to a URL value and write the
; data in the "a" area widget to the URL:
write (to-url f/text) a/text
alert "Saved"
]
]
Here's the whole program without comments:
REBOL [title: "Web Page Editor"]
view layout [
h1 "Web Page Editor:"
f: field 600 "ftp://user:pass@site.com/public_html/page.html"
a: area 600x350
across
btn "Load" [
a/text: read to-url f/text
show a
]
btn "Save" [
write (to-url f/text) a/text
alert "Saved"
]
]
5.5 Inventory List Creator
This is another simple GUI field example. It's very similar to the first example, but improved a bit with text labels for each field and repurposed to satisfy a very concise specialized task. It creates an inventory list using a simple GUI form. The file created is a list that can be used as needed to re-order required items, to calculate inventory and sales tax due, etc. Here's the GUI layout code:
REBOL [title: "Inventory"]
view layout [
text "SKU:"
f1: field
text "Cost:"
f2: field "1.00"
text "Quantity:"
f3: field
across
btn "Save" []
btn "View Data" []
]
And here's the code that makes the entire inventory program run:
REBOL [title: "Inventory"]
view layout [
text "SKU:"
f1: field
text "Cost:"
f2: field "1.00"
text "Quantity:"
f3: field
across
btn "Save" [
write/append %inventory.txt rejoin [
mold f1/text " " mold f2/text " " mold f3/text newline
]
alert "Saved"
]
; A button is added to allow viewing/editing of the saved data, using
; REBOL text editor function:
btn "View Data" [editor %inventory.txt]
]
Here's whole program without comments:
REBOL [title: "Inventory"]
view layout [
text "SKU:"
f1: field
text "Cost:"
f2: field "1.00"
text "Quantity:"
f3: field
across
btn "Save" [
write/append %inventory.txt rejoin [
mold f1/text " " mold f2/text " " mold f3/text newline
]
alert "Saved"
]
btn "View Data" [editor %inventory.txt]
]
5.6 Inventory Sorter and Viewer
The inventory program creates lists of data that look like this (saved in the file "inventory.txt"):
"932984729812" "1.00" "14"
"392328389483" "2.59" "93"
"602374822852" "4.92" "3"
This little program allows users to view the inventory data, sorted by a chosen column. You've already seen most of the important code patterns in this example, during the discussion about sorting columns of values in a table:
REBOL [title: "Sort Inventory"]
; Load the data block from the inventory.txt file and label it
: "inventory":
inventory: load %inventory.txt
; Create a new empty block named "blocked":
blocked: copy []
; Convert the "flat" data in the "inventory" block to "blocked" format
; (with rows delineated as separate blocks). Remember, as shown earlier,
; the "append/only" function refinement makes this easy to do. Just run
; a "foreach" function on every 3 items in the "inventory" block, and
; add each group of three items as a new block within the "blocked" block.
; Notice that during the process, the "cost" and "qty" are converted from
; strings to money and number values:
foreach [sku cost qty] inventory [
append/only blocked reduce [
sku
to-money cost
to-integer qty
]
]
; Use the "request-list" function to allow the user to select a column to
; sort by. Assign the response to the variable "field-name":
field-name: request-list "Choose Field To Sort By:" [
"sku" "cost" "qty"
]
; The "select" function chooses the next value in a list, selected by the
; user. In this case if the field-name variable equals "sku", the "field"
; variable is set to 1. If field-name="cost", the "field" variable is set
; to 2. If field-name="qty", then "field" is set to 3:
field: select ["sku" 1 "cost" 2 "qty" 3] field-name
; Use the "request-list" function to allow the user to select an order to
; sort by. Assign the response to the label "order":
order: request-list "Ascending or Descending:" ["ascending" "descending"]
; Sort by the appropriate "field" column, ascending or descending,
; depending on the value of the "order" variable:
either order = "ascending" [
sort/compare blocked func [a b] [(at a field) < (at b field)]
][
sort/compare blocked func [a b] [(at a field) > (at b field)]
]
; Use the foreach function to loop through the sorted "blocked" block of
; data. Print the 3 items in each block within the "blocked" block:
foreach item blocked [
print rejoin [
"SKU: " item/1 ; The 1st item in each block is the SKU.
" COST: " item/2 ; 2nd item is the cost.
" QTY: " item/3 ; 3rd item is the quantity.
newline ; print a carriage return at the end
]
]
halt
Here's the whole program without comments:
REBOL [title: "Sort Inventory"]
inventory: load %inventory.txt
blocked: copy []
foreach [sku cost qty] inventory [
append/only blocked reduce [
sku
to-money cost
to-integer qty
]
]
field-name: request-list "Choose Field To Sort By:" [
"sku" "cost" "qty"
]
field: select ["sku" 1 "cost" 2 "qty" 3] field-name
order: request-list "Ascending or Descending:" ["ascending" "descending"]
either order = "ascending" [
sort/compare blocked func [a b] [(at a field) < (at b field)]
][
sort/compare blocked func [a b] [(at a field) > (at b field)]
]
foreach item blocked [
print rejoin [
"SKU: " item/1
" COST: " item/2
" QTY: " item/3
newline
]
]
halt
5.7 Contacts Viewer
Here's a contact database app that displays user information in a tabular display:
REBOL [title: "Contacts"]
; First create a block of user data. 15 values - 5 rows, 3 columns:
users: [
"John Smith" "123 Tomline Lane Forest Hills, NJ" "555-1234"
"Paul Thompson" "234 Georgetown Pl. Peanut Grove, AL" "555-2345"
"Jim Persee" "345 Pickles Pike Orange Grove, FL" "555-3456"
"George Jones" "456 Topforge Court Mountain Creek, CO" ""
"Tim Paulson" "" "555-5678"
]
; Next, define a GUI layout block. White background, widgets positioned
; next to each other horizontally, a new "header" text widget style which
; is 200 pixels wide, and 3 of those widgets containing appropriate header
; labels for each column of data, followed by a new GUI line:
gui: [
backdrop white
across
style header text 200
header "Name:" header "Address:" header "Phone:" return
]
; Use the foreach function to loop through each row in the user block:
foreach [name address phone] users [
; For each row in the user block, add the following code to the GUI
; layout block:
append gui compose [
; Add a new field widget containing each row's name, address, and
; phone values. The "compose" function above converts the
; parenthesized words below to evaluated values (the text data
; in each row, instead of the text "name", "address", "phone"):
field (name) field (address) field (phone) return
]
]
; Show the constructed GUI block:
view layout gui
Here's the whole program without comments:
REBOL [title: "Contacts"]
users: [
"John Smith" "123 Tomline Lane Forest Hills, NJ" "555-1234"
"Paul Thompson" "234 Georgetown Pl. Peanut Grove, AL" "555-2345"
"Jim Persee" "345 Pickles Pike Orange Grove, FL" "555-3456"
"George Jones" "456 Topforge Court Mountain Creek, CO" ""
"Tim Paulson" "" "555-5678"
]
gui: [
backdrop white
across
style header text black 200
header "Name:" header "Address:" header "Phone:" return
]
foreach [name address phone] users [
append gui compose [
field (name) field (address) field (phone) return
]
]
view layout gui
This app combines some the techniques used in the previous two examples, to create a prettier sorted inventory display:
REBOL [title: "Sort Inventory"]
; First, load some data created by the inventory app. Assign the loaded
; block to the variable label "inventory":
inventory: load %inventory.txt
; Create a new empty block:
blocked: copy []
; Create a block of blocks, so that the data can be sorted by columns,
; with column data converted from text to money and integer values.
; The "reduce" function works just like "compose", but does not require
; parentheses:
foreach [sku cost qty] inventory [
append/only blocked reduce [
sku
to-money cost
to-integer qty
]
]
; Use the "request-list" requestor to get a sort column from the user.
; Assign the user's response to the variable label "field-name":
field-name: request-list "Choose Field To Sort By:" [
"sku" "cost" "qty"
]
; Used the "select" function to assign a number value to the column
; chosen above. Assign that number to variable label "field":
field: select ["sku" 1 "cost" 2 "qty" 3] field-name
; Use the "request-list" requestor to get a sort order from the user.
; Assign the user's response to the variable label "order":
order: request-list "Ascending or Descending:" ["ascending" "descending"]
; If the chosen sort order is "ascending", sort the block of blocks in
; ascending order, based on the chosen "field" column. Otherwise, sort
; it in descending order:
either order = "ascending" [
sort/compare blocked func [a b] [(at a field) < (at b field)]
][
sort/compare blocked func [a b] [(at a field) > (at b field)]
]
; Create a GUI block with some 200 pixel wide text headers:
gui: [
backdrop white
across
style header text black 200
header "SKU:" header "Cost:" header "Qty:" return
]
; For each row in the block of blocks, append 3 fields containing the
; name, address, and phone text, to the GUI layout:
foreach row blocked [
append gui compose [
field (form row/1) field (form row/2) field (form row/3) return
]
]
; View the GUI layout:
view center-face layout gui
Here's the whole program without comments:
REBOL [title: "Sort Inventory"]
inventory: load %inventory.txt
blocked: copy []
foreach [sku cost qty] inventory [
append/only blocked reduce [
sku
to-money cost
to-integer qty
]
]
field-name: request-list "Choose Field To Sort By:" [
"sku" "cost" "qty"
]
field: select ["sku" 1 "cost" 2 "qty" 3] field-name
order: request-list "Ascending or Descending:" ["ascending" "descending"]
either order = "ascending" [
sort/compare blocked func [a b] [(at a field) < (at b field)]
][
sort/compare blocked func [a b] [(at a field) > (at b field)]
]
gui: [
backdrop white
across
style header text black 200
header "SKU:" header "Cost:" header "Qty:" return
]
foreach row blocked [
append gui compose [
field (form row/1) field (form row/2) field (form row/3) return
]
]
view center-face layout gui
5.8 Minimal Retail Cash Register and Sales Report System
The example below is a trivial POS ("Point of Sale", or retail cash register) program:
REBOL [title: "Minimal Cash Register"]
view gui: layout [
; Create a new widget style named "fld". It's just a text field,
; 80 pixels wide:
style fld field 80
; Place consecutive widgets next to each other:
across
; Here are 3 text labels and 3 text entry fields to allow users to
; enter a cashier name, item name, and price. The text entry fields
; are labeled appropriately:
text "Cashier:" cashier: fld
text "Item:" item: fld
text "Price:" price: fld [
; When the user enters a price, perform the following operations:
; First, check to make sure the price entered can be converted to
; a money value. If not, alert the user with an error message and
; exit the action block:
if error? try [to-money price/text] [alert "Price error" return]
; Otherwise, add the quoted item text, and the price to the area
; widget (separated by a few spaces):
append a/text reduce [mold item/text " " price/text newline]
; Then erase the text in the item and price entry fields:
item/text: copy "" price/text: copy ""
; Now, compute a subtoal of all the entered items. Update the
; subtotal, tax, and total amount fields with the appropriate
; computed values:
sum: 0
foreach [item price] load a/text [sum: sum + to-money price]
subtotal/text: form sum
tax/text: form sum * .06
total/text: form sum * 1.06
; Put the cursor back in the "item" field, and update the display:
focus item
show gui
]
; On a new row, put an area widget, 600 pixels across and 300 tall,
; labeled "a":
return
a: area 600x300
; On a new line, 3 more text and field widgets, labeled appropriately,
: and a button with the text "Save":
return
text "Subtotal:" subtotal: fld
text "Tax:" tax: fld
text "Total:" total: fld
btn "Save" [
; When the button is pressed, do the following:
; first create a quoted block of items in the area widget.
; Replace all the newline characters with a space, so that all
; the data is on one line. Label the whole chunk of data "items":
items: replace/all (mold load a/text) newline " "
; Append the "items" data, the cashier name, and the date to the
; file "sales.txt" (all separated by carriage returns):
write/append %sales.txt rejoin [
items newline cashier/text newline now/date newline
]
; Erase all the text in every field widget, erase the text in the
; the area widget, and update the display:
clear-fields gui
a/text: copy ""
show gui
]
]
The code below reports the total sum of cash register sales for the current day:
REBOL [title: "Daily Total"]
; Read in each line of the "sales.txt" block, and label it "sales":
sales: read/lines %sales.txt
; Label a variable to hold a computed sum, and set it initially to $0:
sum: $0
; Use a foreach loop to go through every 3 items in the sales data
; (remember each group of items, cashier, and date were separated by a
; newline in the code above):
foreach [items cashier date] sales [
; If the today's date is foun in any saved date entry:
if now/date = to-date date [
; Use a foreach loop to go through the values in the "items" data:
foreach [item price] load items [
; Add the price value to the computed sum:
sum: sum + to-money price
]
]
]
; Alert the user with the computed sum:
alert rejoin ["Total sales today: " sum]
The report below shows the total of all items sold on any chosen day, by any chosen cashier:
REBOL [title: "Cashier Report"]
; Start by requesting a selected date and the name of a cashier:
report-date: request-date
report-cashier: request-text/title "Cashier:"
; Read in the lines of the "sales.txt" file:
sales: read/lines %sales.txt
; Prepare to compute a sum value:
sum: $0
; Use a foreach loop to go through each entry in the sales data:
foreach [items cashier date] sales [
; Perform 2 conditional evaluations to check for any entries in which
; the selected cashier name and date match:
if ((report-cashier = cashier) and (report-date = to-date date)) [
; For any entry that matches, add to the sum:
foreach [item price] load items [
sum: sum + to-money price
]
]
]
; Alert the user with a concatenated message:
alert rejoin ["Total for " report-cashier " on " report-date ": " sum]
Here are all 3 programs, without comments:
REBOL [title: "Minimal Cash Register"]
view gui: layout [
style fld field 80
across
text "Cashier:" cashier: fld
text "Item:" item: fld
text "Price:" price: fld [
if error? try [to-money price/text] [alert "Price error" return]
append a/text reduce [mold item/text " " price/text newline]
item/text: copy "" price/text: copy ""
sum: 0
foreach [item price] load a/text [sum: sum + to-money price]
subtotal/text: form sum
tax/text: form sum * .06
total/text: form sum * 1.06
focus item
show gui
]
return
a: area 600x300
return
text "Subtotal:" subtotal: fld
text "Tax:" tax: fld
text "Total:" total: fld
btn "Save" [
items: replace/all (mold load a/text) newline " "
write/append %sales.txt rejoin [
items newline cashier/text newline now/date newline
]
clear-fields gui
a/text: copy ""
show gui
]
]
REBOL [title: "Daily Total"]
sales: read/lines %sales.txt
sum: $0
foreach [items cashier date] sales [
if now/date = to-date date [
foreach [item price] load items [
sum: sum + to-money price
]
]
]
alert rejoin ["Total sales today: " sum]
REBOL [title: "Cashier Report"]
report-date: request-date
report-cashier: request-text/title "Cashier:"
sales: read/lines %sales.txt
sum: $0
foreach [items cashier date] sales [
if ((report-cashier = cashier) and (report-date = to-date date)) [
foreach [item price] load items [
sum: sum + to-money price
]
]
]
alert rejoin ["Total for " report-cashier " on " report-date ": " sum]
5.9 Email
This example is a complete graphical email client that can be used to read and send messages. It has a button feature which allows the user to enter all necessary email account settings:
REBOL [Title: "Little Email Client"]
; The line below creates the program's GUI window:
view layout [
; This line adds a text label to the GUI:
h1 "Send Email:"
; This line adds a button to the GUI:
btn "Server settings" [
; When the button is clicked, the following lines are run.
; These lines set all the email user account information
; required to send and receive email. The settings are gotten
; from the user with the "request-text" function, and assigned
; to their appropriate locations in the REBOL system:
system/schemes/default/host: request-text/title "SMTP Server:"
system/schemes/pop/host: request-text/title "POP Server:"
system/schemes/default/user: request-text/title "SMTP User Name:"
system/schemes/default/pass: request-text/title "SMTP Password:"
system/user/email: to-email request-text/title "Your Email Addr:"
]
; This line creates a text entry field, containing the default text
; "recipient@website.com". The variable word "address" is assigned to
; this widget:
address: field "recipient@website.com"
; Heres another text entry field, for the email subject line:
subject: field "Subject"
; This line creates a larger, multi-line text entry area for the body
; text of the email:
body: area "Body"
; Here's a button displaying the word "send". The functions inside
; its action block are executed whenever the button is clicked:
btn "Send" [
; This line does most of the work. It uses the REBOL "send"
; function to send the email. The send function, with its
; "/subject" refinement accepts three parameters. It's passed the
; current text contained in each field labeled above (referred to
; as "address/text" "body/text" and "subject/text"). The
; "to-email" function ensures that the address text is treated as
; an email data value:
send/subject (to-email address/text) body/text subject/text
; This line alerts the user when the previous line is complete:
alert "Message Sent."
]
; Here's another text label:
h1 "Read Email:"
; Here's another text entry field. The user's email account info is
; entered here.
mailbox: field "pop://user:pass@site.com"
; This last button has an action block that reads messages from a
; specified mailbox. It only takes one line of code:
btn "Read" [
; The "to-url" function ensures that the text in the mailbox field
; is treated as a URL. The contents of the mailbox are read and
; displayed using REBOL's built-in text editor:
editor read to-url mailbox/text
]
]
Here's the whole program without comments:
view layout[
h1 "Send:"
btn "Server settings" [
system/schemes/default/host: request-text/title "SMTP Server:"
system/schemes/pop/host: request-text/title "POP Server:"
system/schemes/default/user: request-text/title "SMTP User Name:"
system/schemes/default/pass: request-text/title "SMTP Password:"
system/user/email: to-email request-text/title "Your Email Addr:"
]
a: field "user@website.com"
s: field "Subject"
b: area
btn "Send"[
send/subject to-email a/text b/text s/text
alert "Sent"
]
h1 "Read:"
f: field "pop://user:pass@site.com"
btn "Read" [editor read to-url f/text]
]
5.10 Scheduler
Here's the GUI code for a scheduling app that allows users to create events on any day. Later, the user can click any day on the calendar to see the scheduled events:
view center-face gui: layout [
btn "Date" []
date: field
text "Event Title:"
event: field
text "Time:"
time: field
text "Notes:"
notes: field
btn "Add Appoinment" []
a: area
btn "View Schedule" []
]
Here's the final code that makes the whole scheduling app run. This entire program is really nothing more complex than the field saver, inventory, cash register, and other GUI examples you've seen so far. There are just a few more widgets, and of course a date requester to allow the user to select dates, but the same GUI, file reading/writing, and foreach block management are repurposed to enter, save, load, and display calendar data:
view center-face gui: layout [
btn "Date" [date/text: form request-date show date]
date: field
text "Event Title:"
event: field
text "Time:"
time: field
text "Notes:"
notes: field
btn "Add Appoinment" [
write/append %appts.txt rejoin [
mold date/text newline
mold event/text newline
mold time/text newline
mold notes/text newline
]
date/text: "" event/text: "" time/text: "" notes/text: ""
show gui
alert "Added"
]
a: area
btn "View Schedule" [
today: form request-date
foreach [date event time notes] load %appts.txt [
if date = today [
a/text: copy ""
append a/text form rejoin [
date newline
event newline
time newline
notes newline newline
]
show a
]
]
]
]
5.11 Parts Database
Here's an application that allows workers to add, delete, edit, and view manufactured parts. This is another quintessential simple text field storage application. It can be used as shown here, to save parts information, but by adjusting just a few lines of code and text labels, it could be easily adapted to store contacts information, or any other type of related fields of blocked text data. Be sure to pay special attention to how the text-list widget is used, and be particularly aware of how the "pick" function is used to select consecutive values after a found index:
REBOL [title: "Parts]
; The line below writes a new empty data file to the hard drive, if it
; doesn't already exist. If the file DOES already exist, then this
; function simply writes an empty string to it (i.e., leaves the file
; alone):
write/append %data.txt ""
; This line loads all saved records from the database file:
database: load %data.txt
; Here's the GUI window:
view center-face gui: layout [
; Here's a text label to instruct the user:
text "Load an existing record:"
; This text list displays an alphabetically sorted list of the
; names found in the database (every forth item). The number
; pair indicates the widget's pixel size:
name-list: text-list blue 400x100 data (sort extract database 4) [
; The following line is included to avoid potential errors.
; When an item in the text list is clicked, we first check that
; the selected data (represented by the word "value") is NOT
; equal to nothing. If so, exit the widget's action block
; (the "return" word quits the text-list's action routine):
if value = none [return]
; The following code finds the selected part in the loaded
; database. The display fields in the GUI are then set
; to show the found part, and each of the 3 items after
; it in the database (part name = field 1, manufacturer =
; field 2, SKU = field 3, notes = field 4):
marker: index? find database value
n/text: pick database marker
a/text: pick database (marker + 1)
p/text: pick database (marker + 2)
o/text: pick database (marker + 3)
; Update the display to show the changed text fields (notice
; the "gui" label defined above - it refers to the entire GUI
; layout):
show gui
]
; Here are the text display fields, and some text labels to show
; what should be typed into each field:
text "Part Name:" n: field 400
text "Manufacturer:" a: field 400
text "SKU:" p: field 400
text "Notes:" o: area 400x100
; The "across" word adds widgets to the GUI next to one another,
; instead of beneath one another, which is the default behavior
; (the following 3 buttons will appear next to each other):
across
; Here's a GUI button to let the user save the contents of the
; text fields to the database:
btn "Save" [
; When this button is clicked, make sure the required field
; contains some text. If not, notify the user, and then exit
; this button's routine (the "return" word quits the save
; button's action block):
if n/text = "" [alert "You must enter a part name." return]
; Now run through every forth item in the database to check if
; the part already exists. If so, give the user the option to
; overwrite that record. If they respond yes, delete the old
; record from the database ("remove/part" deletes 4 items at
; the location where the selected part is found). If the user
; responds no, escape out of the save button's routine:
if find (extract database 4) n/text [
either true = request "Overwrite existing record?" [
remove/part (find database n/text) 4
] [
return
]
]
; Now update the database with the new data, and write it to
; the hard drive. The "repend" function appends the evaluated
; variables inside the brackets (in this case a block of 4
; separate text strings contained in the GUI fields) to the
; database:
save %data.txt repend database [n/text a/text p/text o/text]
; Update the text-list to show the added record:
name-list/data: sort (extract copy database 4)
show name-list
]
; This button allows the user to clear the screen and enter a
; new record:
btn "Delete" [
; When this button is clicked, the code below gives the user
; the option to delete the selected record. If the user
; selects "yes", the "remove/part" function deletes 4 items
; from the database, at the location where the selected part
; is found. The database is saved, and the text fields are
; cleared ("do-face" runs the action block of the
; "clear-button" widget above, to clear the GUI fields), then
; the part name list is updated:
if true = request rejoin ["Delete " n/text "?"] [
remove/part (find database n/text) 4
save %data.txt database
do-face clear-button 1
name-list/data: sort (extract copy database 4)
show name-list
]
]
clear-button: btn "New" [
; When this button is clicked, set the text of each field to an
; empty string:
n/text: copy ""
a/text: copy ""
p/text: copy ""
o/text: copy ""
; As always, when any on data in the GUI is changed, the
; screen must be updated:
show gui
]
]
Here's the whole program without comments:
REBOL [title: "Parts"]
write/append %data.txt ""
database: load %data.txt
view center-face gui: layout [
text "Parts in Stock:"
name-list: text-list blue 400x100 data sort (extract database 4) [
if value = none [return]
marker: index? find database value
n/text: pick database marker
a/text: pick database (marker + 1)
p/text: pick database (marker + 2)
o/text: pick database (marker + 3)
show gui
]
text "Part Name:" n: field 400
text "Manufacturer:" a: field 400
text "SKU:" p: field 400
text "Notes:" o: area 400x100
across
btn "Save" [
if n/text = "" [alert "You must enter a part name." return]
if find (extract database 4) n/text [
either true = request "Overwrite existing record?" [
remove/part (find database n/text) 4
] [
return
]
]
save %data.txt repend database [n/text a/text p/text o/text]
name-list/data: sort (extract copy database 4)
show name-list
]
btn "Delete" [
if true = request rejoin ["Delete " n/text "?"] [
remove/part (find database n/text) 4
save %data.txt database
do-face clear-button 1
name-list/data: sort (extract copy database 4)
show name-list
]
]
clear-button: btn "New" [
n/text: copy ""
a/text: copy ""
p/text: copy ""
o/text: copy ""
show gui
]
]
Here's the same program as above, repurposed as a contacts database app (quite a bit more versatile than the earlier example). The field labels have simply been changed:
REBOL [title: "Contacts"]
write/append %data.txt ""
database: load %data.txt
view center-face gui: layout [
text "Existing Contacts:"
name-list: text-list blue 400x100 data sort (extract database 4) [
if value = none [return]
marker: index? find database value
n/text: pick database marker
a/text: pick database (marker + 1)
p/text: pick database (marker + 2)
o/text: pick database (marker + 3)
show gui
]
text "Name:" n: field 400
text "Address:" a: field 400
text "Phone:" p: field 400
text "Notes:" o: area 400x100
across
btn "Save" [
if n/text = "" [alert "You must enter a name." return]
if find (extract database 4) n/text [
either true = request "Overwrite existing record?" [
remove/part (find database n/text) 4
] [
return
]
]
save %data.txt repend database [n/text a/text p/text o/text]
name-list/data: sort (extract copy database 4)
show name-list
]
btn "Delete" [
if true = request rejoin ["Delete " n/text "?"] [
remove/part (find database n/text) 4
save %data.txt database
do-face clear-button 1
name-list/data: sort (extract copy database 4)
show name-list
]
]
clear-button: btn "New" [
n/text: copy ""
a/text: copy ""
p/text: copy ""
o/text: copy ""
show gui
]
]
5.12 Time Clock and Payroll Report
Here's a time clock program that can be used to log employee punch-in and punch-out times for shift work. The GUI allows for easy addition and deletion of employee names, and a full audit history backup of every entry made to the sign ins, is saved to the hard drive each time any employee signs in or out.
REBOL [title: "Time Clock"]
; First, check to see if an employee file has been created. If not,
; create it. This list will be displayed in a text-list widget. It
; contains the item "(Add New...)", which will be used to allow users to
; add new names to the list:
unless exists? %employees [write %employees {"John Smith" "(Add New...)"}]
; Initialize a current-employee value to an empty string:
cur-employee: copy ""
view center-face layout [
; Here's the text list widget that displays the employee names, sorted
; alphabetically. It's labeled "tl1":
tl1: text-list 400x400 data sort load %employees [
; When the user selects a name from the list, set the
; "cur-employee" variable to hold the selected name:
cur-employee: value
; If the user selected "(Add New...)", run the code required to
; add a name to employee list:
if cur-employee = "(Add New...)" [
; First, request the name from the user, and save the quoted
; value to the %employees file:
write/append %employees mold trim request-text/title "Name:"
; Reload the new employee list into the text-list widget, and
; update the display:
tl1/data: sort load %employees show tl1
]
]
; The "key" widget does not display any face in the GUI. It just
; waits for a key to be pressed, and performs the desired actions:
key #"^~" [
; First, get the name of the employee to be deleted, and assign
; it the variable label "del-emp":
del-emp: copy to-string tl1/picked
; Next, load the employee list and assign it the label "temp-emp":
temp-emp: sort load %employees
; Confirm that the user actually wants to delete the employee:
if true = request/confirm rejoin ["REMOVE " del-emp "?"] [
; Find the employee name in the employee list, remove the
; name, and assign the variable label "new-list" to the
; pruned employee list:
new-list: head remove/part find temp-emp del-emp 1
; Save the pruned employee list to the %employees file:
save %employees new-list
; Update the text-list widget and alert the user that the
; action has been completed:
tl1/data: sort load %employees show tl1
alert rejoin [del-emp " removed."]
]
]
; Here's a button to allow employees to clock in and out of shifts:
btn "Clock IN/OUT" [
; First make sure that a name is selected. If not, alert the user
; and exit the button's action routine:
if ((cur-employee = "") or (cur-employee = "(Add New...)")) [
alert "You must select your name." return
]
; Concatenate the quoted name and time values inside square
; brackets, and beginning with a carriage return. Assign that
; text the variable label "record":
record: rejoin [
newline {[} mold cur-employee { "} mold now {"]}
]
; Confirm with the employee that the selected name and clock time
; are correct:
either true = request/confirm rejoin [
record " -- IS YOUR NAME AND THE TIME CORRECT?"
] [
; This routine saves a backup of the current time sheet in
; a folder named "clock_history". First, the folder is
; created if it doesn't exist:
make-dir %./clock_history/
; Next, a file name is created by rejoining the folder name
; and the current day and time. Illegal file name characters
; are replaced (i.e., "/" and ":" can't be used in file names
; on most operating systems). The text of the existing
; %time_sheet.txt file is written to the date stamped backup
; file name:
write rejoin [
%./clock_history/
replace/all replace form now "/" "--" ":" "_"
] read %time_sheet.txt
; Write the new record to the %time_sheet.txt file:
write/append %time_sheet.txt record
; Alert the user that the operation is complete:
alert rejoin [
uppercase copy cur-employee ", YOUR TIME IS LOGGED."
]
] [
; If the user replied negatively to the confirmation above,
; alert the user that the action is cancelled, and exit the
; routine:
alert "CANCELED"
return
]
]
]
Here's the whole program without comments:
REBOL [title: "Time Clock"]
unless exists? %employees [write %employees {"John Smith" "(Add New...)"}]
cur-employee: copy ""
view center-face layout [
tl1: text-list 400x400 data sort load %employees [
cur-employee: value
if cur-employee = "(Add New...)" [
write/append %employees mold trim request-text/title "Name:"
tl1/data: sort load %employees show tl1
]
]
key #"^~" [
del-emp: copy to-string tl1/picked
temp-emp: sort load %employees
if true = request/confirm rejoin ["REMOVE " del-emp "?"] [
new-list: head remove/part find temp-emp del-emp 1
save %employees new-list
tl1/data: sort load %employees show tl1
alert rejoin [del-emp " removed."]
]
]
btn "Clock IN/OUT" [
if ((cur-employee = "") or (cur-employee = "(Add New...)")) [
alert "You must select your name." return
]
record: rejoin [
newline {[} mold cur-employee { "} mold now {"]}
]
either true = request/confirm rejoin [
record " -- IS YOUR NAME AND THE TIME CORRECT?"
] [
make-dir %./clock_history/
write rejoin [
%./clock_history/
replace/all replace form now "/" "--" ":" "_"
] read %time_sheet.txt
write/append %time_sheet.txt record
alert rejoin [
uppercase copy cur-employee ", YOUR TIME IS LOGGED."
]
] [
alert "CANCELED"
return
]
]
]
Here's a program that creates payroll reports of hours worked by employees, logged by the program above, between any selected start and end dates:
REBOL [title: "Payroll Report"]
; Request start and end dates from a user:
timeclock-start-date: request-date
timeclock-end-date: request-date
; Initials a "totals" variable to an empty string. This string will be
; used to collect (aggregate) the entire output report text:
totals: copy ""
; Load the employee list, and assign it the variable label "names":
names: load %employees
; Load all the saved time sheet information, and assign it the variable
; label "log":
log: load %time_sheet.txt
; Use a foreach loop to collect the log information for each employee:
foreach name names [
; Don't try to collect info for the "(Add New...)" entry in the list:
if name <> "(Add New...)" [
; Create a new block to hold report information for the current
; employee, initially just containing the employee name. Assign
; the variable label "times" to the block:
times: copy reduce [name]
; Use foreach to loop through every entry in the time sheet log:
foreach record log [
; If the current employee name matches the name in the current
; entry in the log file, add the record to the report:
if name = log-name: record/1 [
; Split up the date and time in the current date/time
; entry (data/time is the second item in each log entry).
; Assign the variable "date-time" to the result:
date-time: parse record/2 "/"
; Convert the date portion of the result above to a REBOL
; date value. Assign the label "log-date" to that value:
log-date: to-date date-time/1
; Convert the time portion of the result above to a REBOL
; time value. Assign the label "log-time" to that value:
log-time: to-time first parse date-time/2 "-"
; If the date in the current timesheet entry is within the
; start and end date period selected by the user, add the
; current entry to the employee's report:
if (
(log-date >= timeclock-start-date) and
(log-date <= timeclock-end-date)
) [
append times log-date
append times log-time
]
]
]
; Append the employee name and a carriage return to the final
; report text:
append totals rejoin [name ":" newline]
; Initialize a sum variable to 0:
total-hours: 0
; Go through the entire collected "times" block created above
; to calculate end-times - start-times. Remember that the first
; item in the block was the user's name, so start this process at
; the second item:
foreach [in-date in-time out-date out-time] (at times 2) [
; Append some nicely formatted text containing the start and
; end days and times, to the final report text:
append totals rejoin [
newline
" in: " in-date ", " in-time
" out: " out-date ", " out-time " "
]
; Add the total hours worked to the "total-hours" sum. Wrap
; this routine in an error check, just in case a start or end
; time is missing:
if error? try [
total-hours: total-hours + (out-time - in-time)
] [
alert rejoin [
"Missing login or Missing logout: " name
]
]
]
; Append the employee's total computed hours to the final report,
; along with some concatenated text and carriage returns, to
; create a nicely formatted printout:
append totals rejoin [
newline newline
" TOTAL HOURS: " total-hours
newline newline newline
]
]
]
; Write the final report to a file name containing the start and end dates
; chosen for the report, and open it using Window's "notepad" application
; (you could simply use REBOL's "editor" function to do this):
write filename: copy rejoin [
%timeclock_report-- timeclock-start-date
"_to_" timeclock-end-date ".txt"
] totals
call/show rejoin ["notepad " to-local-file filename]
Here's the whole program without comments:
REBOL [title: "Payroll Report"]
timeclock-start-date: request-date
timeclock-end-date: request-date
totals: copy ""
names: load %employees
log: load %time_sheet.txt
foreach name names [
if name <> "(Add New...)" [
times: copy reduce [name]
foreach record log [
if name = log-name: record/1 [
date-time: parse record/2 "/"
log-date: to-date date-time/1
log-time: to-time first parse date-time/2 "-"
if (
(log-date >= timeclock-start-date) and
(log-date <= timeclock-end-date)
) [
append times log-date
append times log-time
]
]
]
append totals rejoin [name ":" newline]
total-hours: 0
foreach [in-date in-time out-date out-time] (at times 2) [
append totals rejoin [
newline
" in: " in-date ", " in-time
" out: " out-date ", " out-time " "
]
if error? try [
total-hours: total-hours + (out-time - in-time)
] [
alert rejoin [
"Missing login or Missing logout: " name
]
]
]
append totals rejoin [
newline newline
" TOTAL HOURS: " total-hours
newline newline newline
]
]
]
write filename: copy rejoin [
%timeclock_report-- timeclock-start-date
"_to_" timeclock-end-date ".txt"
] totals
call/show rejoin ["notepad " to-local-file filename]
5.13 Blogger
This program allows users to create and add entries to an online blog page. The GUI has text fields which allow the user to enter a title, link, and blog text, as well as a button to select an image file which will be uploaded and included in the blog entry. When the "Upload" button is clicked, an HTML code file is created and uploaded to the user's web server, along with the image (you'll learn more about HTML later in the tutorial - for now just pay attention to the fact that a bunch of foreign HTML code is concatenated together with values entered by the user). Be sure to edit the ftp-url and html-url variables to represent actual user account information (you'll need a user name, password, and folder on a web server to run this example).
REBOL [title: "Blogger"]
; Store the blog file name in a variable:
page: "blog.html"
; Store the FTP account address, with username and password:
ftp-url: ftp://user:pass@site.com/public_html/folder/
; Store the blog's web page URL in a variable:
html-url: join http://site.com/folder/ page
; Create a 1 pixel blank image to upload, for when the user
; doesn't want to upload any image for a given blog entry:
save/png %dot.png to-image layout/tight [box white 1x1]
; Here's the GUI:
view center-face gui: layout [
; Display the blog URL in the GUI (the "form" function converts
; the URL data type created above, to a string data type required
; by the h2 widget):
h2 (form html-url)
; Here's a descriptive header and a text entry field where the user
; can enter a title for the blog. The field is labeled by the
; variable word "t":
text "Title:" t: field 400
; Header and entry field for a URL link to be included in the blog:
text "Link:" l: field 400
; Header and selection button for an image to be included in the
; blog. When the button is clicked, a file requestor pops up. The
; button's text is set to display the selected file name:
text "Image:" i: btn 400 [i/text: request-file show i]
; Header and text entry field for the blog text:
text "Text:" x: area 400x100
; Layout all the following widgets across the screen:
across
; Here's the GUI button which does most of the work:
btn "Upload" [
; When the button is clicked, first try to read the existing
; blog text (HTML source). If it's readable, assign that data
; the variable label "existing-text". If it's not readable,
; create the folder and an empty blog file on the FTP server,
; then assign the "existing-text" variable an emptry string:
if error? try [existing-text: read html-url] [
make-dir ftp-url
write (join ftp-url page) ""
existing-text: copy ""
]
; Assign the variable word "picture" to the image file name
; chosen above ("last split-path" trims off the directory path
; portion of the file name (i.e., %/C/folder/myimage.jpg would
; be trimmed to %myimage.jpg)):
picture: last split-path to-file i/text
; Write the image to the FTP server:
write/binary (join ftp-url picture) (read/binary to-file i/text)
; Write the following rejoined HTML to the blog file on the FTP
; server:
write (join ftp-url page) rejoin [
; First add the title, enclosed in "h1" tags:
{<h1>} t/text {</h1>}
; Now add a link to the uploaded picture, followed by 2
; HTML line break tags:
{<img src="} picture {"><br><br>}
; Add the date and time, followed by 5 spaces, no line breaks:
now/date { } now/time { }
; Add a link tag (because there were no line breaks above,
; this link appears on the same line as the date and time in
; the blog), followed by 2 line breaks:
{<a href="} l/text {">} l/text {</a><br><br>}
; Put the blog text inside a table which takes up 80% of the
; screen. Center the table on the screen (so it's indented).
; Use the "pre" tag to make sure that the text is formatted
; exactly the way it was typed in by the user (with carriage
; returns and spaces as entered into the area widget above),
; and use the "strong" tag to bold the text. Follow that
; entire section of HTML with a line break and then a
; "horizontal rule" tag (a separator line):
{<center><table width=80%><tr><td><pre><strong>}
x/text
{</strong></pre></td></tr></table></center><br><hr>}
; Add the previously existing blog HTML below the new entry:
existing-text
]
; After the blog has been updated, open the blog URL in the
; browser:
browse html-url
]
; Add a GUI button to open the blog URL in the browser:
btn "View" [browse html-url]
; This GUI button allows the user to edit the existing blog file
; (the HTML data created by this program), using REBOL's built in
; text editor. Remember that REBOL's editor has the ability to
; read and SAVE data directly to/from FTP files, so this editor
; enables the user to actually edit and MAKE CHANGES to the blog
; file on the FTP server:
btn "Edit" [editor (join ftp-url page)]
]
Here's the whole program, without comments:
REBOL [title: "Blogger"]
page: "blog.html"
ftp-url: ftp://user:pass@site.com/public_html/folder/
html-url: join http://site.com/folder/ page
save/png %dot.png to-image layout/tight [box white 1x1] ; blank image
view center-face gui: layout [
h2 (form html-url)
text "Title:" t: field 400
text "Link:" l: field 400
text "Image:" i: btn 400 [i/text: request-file show i]
text "Text:" x: area 400x100
across
btn "Upload" [
if error? try [existing-text: read html-url] [
make-dir ftp-url
write (join ftp-url page) ""
existing-text: copy ""
]
picture: last split-path to-file i/text
write/binary (join ftp-url picture) (read/binary to-file i/text)
write (join ftp-url page) rejoin [
{<h1>} t/text {</h1>}
{<img src="} picture {"><br><br>}
now/date { } now/time { }
{<a href="} l/text {">} l/text {</a><br><br>}
{<center><table width=80%><tr><td><pre><strong>}
x/text
{</strong></pre></td></tr></table></center><br><hr>}
existing-text
]
browse html-url
]
btn "View" [browse html-url]
btn "Edit" [editor (join ftp-url page)]
]
5.14 FTP Group Chat
This example is a simple chat application that lets users send instant text messages back and forth across the Internet. The chat "rooms" are created by dynamically creating, reading, appending, and saving text files via ftp (to use the program, you'll need access to an available ftp server: ftp address, username, and password. Nothing else needs to be configured on the server). This enables unlimited private conversation spaces that can be used to connect any number of individuals or teams within a group. This program runs entirely in the REBOL console (i.e., there is no graphic user interface - it's all printed text), so it can be used even on simple devices which don't have GUI support. It includes password protected access for administrators to erase chat contents. It also allows users to pause activity momentarily, and requires a username/password to continue (["secret" "password"], in this example case).
REBOL [title: "FTP Chat Room"]
; The following line gets the URL of a text file on the user's web server
; to use for the chat. The ftp username, password, domain, and filename
; must be entered in the format shown:
webserver: to-url ask trim/lines {
URL of text file on your server (ftp://user:pass@site.com/chat.txt):
}
; The following line gets the user's name:
name: ask "Enter your name: "
; The following line writes some text to the webserver file (obtained
; above), indicating that the user has entered the chat. The "/append"
; refinement adds to the existing text in the webserver file (as opposed
; to erasing what's already there). Using "rejoin", the text written to
; the webserver is the combined value of the user's name, some static
; text, the current date and time, and a carriage return. If the file
; doesn't exist, it is created. Any number of "room" files can be
; automatically created on the web server:
write/append webserver rejoin [now ": " name " has entered the room.^/"]
; Now the program uses a "forever" loop to continually wait for user
; input, and to do appropriate things with that input:
forever [
; First, read the messages that are currently in the "webserver" text
; file, and assign the variable word "current-chat" to that text:
current-chat: read webserver
; Clear the screen:
prin "^(1B)[J"
; Display a greeting and some instructions:
print rejoin [
"--------------------------------------------------"
newline {You are logged in as: } name newline
{Type "room" to switch chat rooms.} newline
{Type "lock" to pause/lock your chat.} newline
{Type "quit" to end your chat.} newline
{Type "clear" to erase the current chat.} newline
{Press [ENTER] to periodically update the display.} newline
"--------------------------------------------------" newline
]
print rejoin ["Here's the current chat text at: " webserver newline]
print current-chat
; In the line below, the "ask" function is used to get some text from
; the user. The returned text (the text entered by the user) is
; assigned the label "entered-text", and concatenated with the user's
; name and the text " says: ". This prepares it to be added to the
; webserver file and displayed in the chat. Notice that the user
; must first respond to the "ask" function, before the rejoin
; evaluation can occur:
sent-message: copy rejoin [
name " says: "
entered-text: ask "You say: "
]
; The "switch" structure below is used to check for commands in the
; text entered by the user. If the user enters "quit", "clear",
; "room", or "lock", appropriate actions occur:
switch/default entered-text [ ; "switch" handles multiple "if" cases
; If the user typed "quit", stop the forever loop (exit the
; program):
"quit" [break]
; If the user typed "clear", erase the current text chat. But
; first, ask user for the administrator username/password:
"clear" [
; "if/else" does the same thing as "either" (deprecated):
adminu: ask "Admin Username: "
adminp: ask "Admin Password: "
if/else ((adminu = "secret") and (adminp = "password")) [
write webserver ""
] [
ask trim/auto {
^LYou must know the administrator password to clear
the room!
}
]
]
; If the user typed "room", request a new FTP address, and run
; some code that was presented earlier in the program, using the
; newly entered "webserver" variable, to effectively change chat
; "rooms":
"room" [
; Add a message the chat file, indicating that the user has
; left the chat:
write/append webserver rejoin [
now ": " name " has left the room." newline
]
; Get the URL of a new chat text file (the new room address).
; Use the old address as the default displayed URL:
webserver: to-url ask rejoin [
{New Web Server Address (} to-string webserver {): }
]
; Display a message in the newly chosen chat text file,
; showing that the user has entered the chat:
write/append webserver rejoin [
now ": " name " has entered the room." newline
]
]
"lock" [
; Display a message to the user that the program will be
; paused:
ask trim/lines {
^LPress [Enter] to resume. You'll need the correct
username and password to continue.
}
; Don't go on until the user gets the password right:
forever [
; The while loop below continually asks the user for
; a password, until correct:
while [
adminu: ask "Admin Username: "
adminp: ask "Admin Password: "
((adminu <> "secret") or (adminp <> "password"))
][
ask "^LIncorrect password - look in the source!"
]
; After the user has entered the correct username and
; password, exit the forever loop and continue with
; the program:
break
]
]
][
; The following line is the default case for the switch structure:
; as long as the entered message is not blank ([Enter]), write the
; entered message to the web server (append it to the current chat
; text):
if entered-text <> "" [
write/append webserver rejoin [sent-message newline]
]
]
]
; When the "forever" loop is exited, do the following:
prin "^(1B)[J" ; clear screen
print "Goodbye!"
write/append webserver rejoin [now ": " name " has closed chat." newline]
wait 1
The bulk of this program runs within the "forever" loop, and uses the conditional "switch" structure to decide how to respond to user input. This is a classic outline that can be adjusted to match a variety of generalized situations in which the computer repeatedly waits for and responds to user interaction at the command prompt.
5.15 Group Reminder
This program operates along the same lines as the FTP Chat app. It stores reminder notes for a group of users in a text file accessible by FTP. Unlike the FTP chat application, this program makes use of GUI features, and focuses on popping up when new messages are sent to the group. Users can run the program at any time to type a new text reminder into a text area widget. The program can also be started and minimized to run in the background and listen for reminders. In listen mode, a GUI window will pop up with a message whenever someone else in the group adds a new reminder. The system can be used to share event reminders, notes, task lists, or any other relevant and time sensitive information. This app only requires that someone in the group has access to an FTP server. Just like the FTP chat program, nothing needs to be configured on the server. All that's required is a username and password, and a connection to the Internet.
REBOL [title: "Group Reminder System"]
; This variable is set to store the username, password, folder, file and
; URL of the FTP server at which the reminder texts are stored:
group-url: ftp://user:pass@site.com/public_html/reminders.txt
; The request-list function here allows the user to choose between adding
; a new reminder text, or listening in the background. The user's
; response is stored in the variable "menu":
menu: request-list "" ["Create New Reminder" "Listen For Reminders"]
; The code pattern below was demonstrated earlier in the "Parts" program.
; It creates a new file, if the file doesn't exist. Otherwise, it does
; nothing:
write/append %reminders.txt ""
; If the user chose to create a new reminder, do the following code:
if menu = "Create New Reminder" [
; Create a GUI window with some header text, an area widget, and a
; button:
view center-face layout [
h3 "Add a new reminder for the group:"
a: area wrap
btn "Submit" [
; When clicked, run the following code with an error check,
; just in case an Internet connection isn't available:
if error? try [
; Attempt to write the area widget text to the FTP file:
write/append group-url mold a/text
; If there's an error writing, alert the user and quit:
] [alert "ERROR: Not Saved (check Internet connection)" quit]
; Otherwise, alert with user with a success message, and end:
alert "Saved"
quit
]
]
]
; If the user chose to listen for reminders, do the following code:
if menu = "Listen For Reminders" [
; Print a waiting message in the console. This can be minimized:
print "Listening for reminders..."
; Repeat the following code endlessly:
forever [
; Waiting a few seconds saves a lot of bandwidth:
wait 5
; "Attempt" is just like "if error? try" - it just doesn't do
; anything if an error occurs. The point of attempting the
; following code is to ensure the program continues to run if
; the Internet connection is disabled, or the FTP file can't be
; read for any other reason:
attempt [
; Read the text in the FTP file, and assign it the label
; "reminders":
reminders: read group-url
; If a new reminder has been added by anyone in the group,
; the "reminders" text will not match the text stored in the
; file reminders.txt:
if reminders <> read %reminders.txt [
; If that's the case, update the reminders.txt file, with
; the new text read from the FTP file:
write %reminders.txt reminders
; Create a blank string to store the current reminders:
remind: copy {}
; Reverse the order of all reminders in the FTP file, so
; that the newest messages are first, then loop through
; them with the "foreach" function:
foreach reminder (reverse load reminders) [
; Append each reminder to the blank text string
; created above, separated by 2 blank lines:
append remind rejoin [reminder newline newline]
]
; Open a GUI window and display the formatted reminder
; text in an area widget:
view center-face layout [
h3 "Reminders for the group:"
area remind
]
]
]
]
]
Here's the whole program without comments:
REBOL [title: "Group Reminder System"]
group-url: ftp://user:pass@site.com/public_html/reminders.txt
menu: request-list "" ["Create New Reminder" "Listen For Reminders"]
write/append %reminders.txt ""
if "" = read group-url [write group-url {""}]
if menu = "Create New Reminder" [
view center-face layout [
h3 "Add a new reminder for the group:"
a: area wrap
btn "Submit" [
if error? try [
write/append group-url mold a/text
] [alert "ERROR: Not Saved (check Internet connection)" quit]
alert "Saved"
quit
]
]
]
if menu = "Listen For Reminders" [
print "Listening for reminders..."
forever [
wait 5
attempt [
reminders: read group-url
if reminders <> read %reminders.txt [
write %reminders.txt reminders
remind: copy {}
foreach reminder (reverse load reminders) [
append remind rejoin [reminder newline newline]
]
view center-face layout [
h3 "Reminders for the group:"
area remind
]
]
]
]
]
5.16 A Univeral Report Generator, For Paypal and any other CSV Table Data
This example creates reports on columns and rows of data in a .csv table. The script demonstrates typical CSV file operations using parse, foreach, and simple series functions. The code performs sums upon columns, and selective calculations upon specified fields, based upon conditional evaluations, searches, etc. Practical column and row based reporting capabilities such as this are a simple and useful skill in REBOL. Using basic permutations of techniques shown here, it's easy to surpass the capabilities of spreadsheets and other "office" reporting tools.
Actual data for use in testing this example can be downloaded as follows:
- Log into your Paypal account
- Click on My Account -> History -> Download History
- Pick a range of dates
- Select "Comma Delimited - All Activity" (where it's labeled "File Types to Download")
- Save the file to %Download.csv.
At the time this example code was created, the author's CSV file download contained the following fields ("A" through "AP" - 42 columns).
Date, Time, Time Zone, Name, Type, Status, Currency, Gross, Fee, Net, From Email Address, To Email Address, Transaction ID, Counterparty Status, Address Status, Item Title, Item ID, Shipping and Handling Amount, Insurance Amount, Sales Tax, Option 1 Name, Option 1 Value, Option 2 Name, Option 2 Value, Auction Site, Buyer ID, Item URL, Closing Date, Escrow Id, Invoice Id, Reference Txn ID, Invoice Number, Custom Number, Receipt ID, Balance, Address Line 1, Address Line 2/District/Neighborhood, Town/City, State/Province/Region/County/Territory/Prefecture/Republic, Zip/Postal Code, Country, Contact Phone Number
The code automatically handles tables with any arbitrary number of columns, allowing fields to be referred to by labels present in the first line of the .csv file.
The script is simple to understand:
REBOL [title: "Paypal Reports"]
; First, read in the lines of the CSV file
; (as described earlier in the CSV/parse section of this tutorial):
filename: request-file/only/file %Download.csv
lines: read/lines filename
; The first row contains column labels. Let's convert that CSV line to a
; block, using the parse/all function:
labels: copy parse/all lines/1 ","
; Remove extra spaces from each of the labels in the "labels" block:
foreach label labels [trim label]
; Now we'll convert the CSV lines into a REBOL data block. First,
; create an empty block to store the data:
database: copy []
; The data values start at line two of the CSV file. Use foreach and
; parse/all to separate the individual values in each line, and collect
; them into a block of blocks (as described in the CSV/parse section):
foreach line (at lines 2) [
parsed: parse/all line ","
append/only database parsed
]
; For the first report, let's get a list of every person in the "Name"
; Column. Find the index number of the "Name" column in the "labels"
; block. We'll use that to pick out name values from each row:
name-index: index? find labels "Name"
; Create an empty text string to collect the names:
names: copy {}
; Loop through the database, picking the item at the name-index location
; from each row
foreach row database [
append names rejoin ["Name: " (pick row name-index) newline]
]
; Display the results:
editor names
; Now let's view every transaction in which the name field includes
; "Netflix". It's basically the same routine as above, only now with an
; added conditional evaluation that uses the "find" function. And for
; this report, I want to see the amount paid. I'll set a variable to
; refer to the index position of the "Net" column:
net-index: index? find labels "Net"
amounts: copy {}
foreach row database [
if find/only (pick row name-index) "Netflix" [
append amounts rejoin ["Amount: " (pick row net-index) newline]
]
]
editor amounts
; Now, let's get a sum of every Netflix transaction between January and
; December, 2012. The dates are stored in a column labeled "Date", with
; a format like: "12/26/2012" (month/day/year). We'll need to convert
; that to REBOL's internal format (day-month-year)
date-index: index? find labels "Date"
; You've seen earlier how to collect sums. Start by setting a sum
; variable to 0. We'll add values to the sum to get a total:
sum: $0
; Now loop through the database and perform some conditional evaluations
; on every row:
foreach row database [
if find/only (pick row name-index) "Netflix" [
; We'll use "parse" to separate the date text at the
; "/" character:
date: parse (pick row date-index) "/"
; Pick the month using the values in system/locale/months
month: pick system/locale/months to-integer date/1
; Then rearrange and put back together the date pieces using
; "rejoin":
reb-date: to-date rejoin [date/2 "-" month "-" date/3]
; If the date is within the chosen range, add the value to
; the sum:
if ((reb-date >= 1-jan-2012) and (reb-date <= 31-dec-2012)) [
sum: sum + (to-money pick row net-index)
]
]
]
; Show the answer:
alert form sum
Here's the entire program without comments:
REBOL [title: "Paypal Reports"]
filename: request-file/only/file %Download.csv
lines: read/lines filename
labels: copy parse/all lines/1 ","
foreach label labels [trim label]
database: copy []
foreach line (at lines 2) [
parsed: parse/all line ","
append/only database parsed
]
name-index: index? find labels "Name"
names: copy {}
foreach row database [
append names rejoin ["Name: " (pick row name-index) newline]
]
editor names
net-index: index? find labels "Net"
amounts: copy {}
foreach row database [
if find/only (pick row name-index) "Netflix" [
append amounts rejoin ["Amount: " (pick row net-index) newline]
]
]
editor amounts
date-index: index? find labels "Date"
sum: $0
foreach row database [
if find/only (pick row name-index) "Netflix" [
date: parse (pick row date-index) "/"
month: pick system/locale/months to-integer date/1
reb-date: to-date rejoin [date/2 "-" month "-" date/3]
if ((reb-date >= 1-jan-2012) and (reb-date <= 31-dec-2012)) [
sum: sum + (to-money pick row net-index)
]
]
]
alert form sum
If you want to use this example to create reports for any other CSV file, just copy and paste the first part of the script. It loads and parses a user selected CSV file, and creates a block of labels from the first line:
REBOL [title: "Reports"]
filename: request-file/only/file %filename.csv
lines: read/lines filename
labels: copy parse/all lines/1 ","
foreach label labels [trim label]
database: copy []
foreach line (at lines 2) [
parsed: parse/all line ","
append/only database parsed
]
You can use this line of code to view the list of column labels in the CSV file:
editor labels
Next, assign variable(s) to the column label(s), on which you want to perform computations:
name-index: index? find labels "Name"
date-index: index? find labels "Date"
net-index: index? find labels "Net"
Set any initial variables needed to perform computations (sum, max/min, etc.), or to hold blocks of collected values:
sum: $0
names: copy {}
amounts: copy {}
Use foreach loop(s) to perform conditional evaluations and desired computations on every row of data:
foreach row database [
append names rejoin ["Name: " (pick row name-index) newline]
]
foreach row database [
if find/only (pick row name-index) "Netflix" [
append amounts rejoin ["Amount: " (pick row net-index) newline]
]
]
foreach row database [
if find/only (pick row name-index) "Netflix" [
date: parse (pick row date-index) "/"
month: pick system/locale/months to-integer date/1
reb-date: to-date rejoin [date/2 "-" month "-" date/3]
if ((reb-date >= 1-jan-2012) and (reb-date <= 31-dec-2012)) [
sum: sum + (to-money pick row net-index)
]
]
]
Display all the computed or collected results:
editor names
editor amounts
alert form sum
Of course, the computations above involve quite a bit of evaluation and data processing, in order to demonstrate useful reusable code snippets (such as converting dates to REBOL values that can be manipulated intelligently). In most cases, simple sums, averages, searches, and other common computations will require far less code. You can shorten code even further by using numbers to refer to columns. Simple reports rarely require much more code than the first example in this tutorial:
sum: $0
foreach line at read/lines http://re-bol.com/Download.csv 2 [
sum: sum + to-money pick (parse/all line ",") 8
]
alert form sum
5.17 Reviewing and Using the Code You've Seen To Model New Applications
You're already well on your way to understanding how to build useful business applications. At this point, it's wise to go back and review every section in the tutorial. The perspective you have now will help to clarify the value of concepts that were skimmed during a first read. Focus on experimenting with individual lines of code, memorizing functions, and understanding the details of syntax that may have been confusing the first time around. Try repurposing, altering, and editing the complete programs you've seen to far, to be useful in data management activities that apply to your own interests. Here are some ideas for experimentation:
- Add columns to data blocks in the existing apps (i.e, perhaps "email" and "mobile", "home", "work" fields to the contacts examples, or "name" a "title" fields to messages in the reminder app).
- Add new computations to the reporting examples (perhaps average, max, and min functions to columns in the Paypal reports).
- Apply the CSV, sorting, computing, and reporting code patterns you've seen to columns of data exported from other desktop applications and web sites.
- Apply new and more complex conditional operations to computations (perhaps apply tax only to items in a Paypal file that were shipped from a certain state, or apply a fee to prices of items that are made by a certain manufacturer in the "Parts" database app).
- Create a few small GUI apps that save CSV files which can be imported into a spreadsheet (perhaps add a CSV export feature to the "Contacts" or "Parts" examples, and especially try exporting computed reports as CSV files).
- Add list features to the email program (perhaps allow email addresses to be chosen from a contact list, or send emails to a group of users).
- Add useful features to the file editor and web page editor programs (maybe try storing a history of edited files, or FTP login credentials).
- Combine the Inventory and Cash Register programs, so that users can select inventory items from a drop down list in the cash register app.
- Add unique computation routines to the calculator app.
- Experiment with the "parse" and "find" functions (perhaps try adding a search feature to the schedule and email programs, or send pre-formed responses to email messages that contain a given string of text in the subject).
- Experiment with printing and displaying formatted data in the console and in GUI fields (perhaps build an email program that parses the header of each email message, and displays each part on separately printed lines in the console or in separate area widgets in a GUI).
- Add image and file sharing features to the Group Reminders app.
- Add images to stored records in the Parts database app.
- Build GUI versions of the FTP chat and Paypal Reports applications. Use text-list widgets to display messages in chat and to select column names and computation options in reports.
- Experiment with GUI layouts. Position text headers, fields, buttons, areas, text lists, etc. in ways that allow data to be arranged coherently and pleasantly on screen, and which invite intuitive user interactions and natural work flow patterns.
- Experiment with how colors, fonts, widget sizes, images, and other cosmetic features can be used to change the appearance of applications.
You'll see much more about how to approach tasks like this, as the tutorial progresses, but exploring creatively and learning to use the tools you've seen so far, will go a long way towards building fundamental coding ability. If you learn nothing more than the code presented up to this point, you will already be able to create a wide variety of practical business applications that would be impossible to implement quite so personally using generic off-the-shelf software products. Continue learning, and that potential will increase dramatically.
6. User Defined Functions and Imported Code Modules
6.1 "Do", "Does", and "Func"
REBOL's built-in functions satisfy many fundamental needs. To achieve more complex or specific computations, you can create your own function definitions.
Data and function words contained in blocks can be evaluated (their actions performed and their data values assigned) using the "do" word. Because of this, any block of code can essentially be treated as a function. That's a powerful key element of the REBOL language design:
some-actions: [
alert "Here is one action."
print "Here's a second action."
write %/c/anotheraction.txt "Here's a third action."
alert "Writing to the hard drive was the third action."
]
do some-actions
New function words can also be defined using the "does" and "func" words. "Does" is included directly after a word label definition, and forces a block to be evaluated every time the word is encountered:
more-actions: does [
alert "Counting some more actions: 4"
alert "And another: 5"
alert "And finally: 6"
]
; Now, to use that function, just type the word label:
more-actions
Here's a useful function to clear the command line screen in the REBOL interpreter.
cls: does [prin "^(1B)[J"]
cls
6.1.1 "Func"
The "func" word creates an executable block in the same way as "does", but additionally allows you to pass your own specified parameters to the newly defined function word. The first block in a func definition contains the name(s) of the variable(s) to be passed. The second block contains the actions to be taken with those variables. Here's the "func" syntax:
func [names of variables to be passed] [
actions to be taken with those variables
]
This function definition:
sqr-add-var: func [num1 num2] [print square-root (num1 + num2)]
Can be used as follows. Notice that no brackets, braces, or parentheses are required to contain the data arguments. Data parameters simply follow the function word, on the same line of code:
sqr-add-var 12 4 ; prints "4", the square root of 12 + 4 (16)
sqr-add-var 96 48 ; prints "12", the square root of 96 + 48 (144)
Here's a simple function to display images:
display: func [filename] [view layout [image load to-file filename]]
display (request-file)
6.2 Return Values
By default, the last value evaluated by a function is returned when the function is complete:
concatenate: func [string1 string2] [join string1 string2]
string3: concatenate "Hello " "there."
print string3
You can also use the word "return" to end the function with a return value. This can be helpful when breaking out of loops (the "for" function performs a count operation - it will be covered in depth in a later section of this tutorial):
stop-at: func [num] [
for i 1 99 1 [
if (i = num) [return i]
print i
]
return num
]
print stop-at 38
6.3 Scope
By default, values used inside functions are treated as global, which means that if any variables are changed inside a function, they will be changed throughout the rest of your program:
x: 10
change-x-globally: func [y z] [x: y + z]
change-x-globally 10 20
print x
You can change this default behavior, and specify that any value be treated as local to the function (not changed throughout the rest of your program), by using the "/local" refinement:
x: 10
change-x-locally: func [y z /local x] [x: y + z]
change-x-locally 10 20 ; inside the function, x is now 30
print x ; outside the function, x is still 10
You can specify refinements to the way a function operates, simply by preceding optional operation arguments with a forward slash ("/"):
compute: func [x y /multiply /divide /subtract] [
if multiply [return x * y]
if divide [return x / y]
if subtract [return x - y]
return x + y
]
compute/multiply 10 20
compute/divide 10 20
compute/subtract 10 20
compute 10 20
6.4 Function Documentation
The "help" function provides usage information for any function, including user defined functions:
help for
help compute
You can include documentation for any user defined function by including a text string as the first item in it's argument list. This text is included in the description displayed by the help function:
doc-demo: func ["This function demonstrates doc strings"] [help doc-demo]
doc-demo
Acceptable data types for any parameter can be listed in a block, and doc strings can also be included immediately after any parameter:
concatenate-string-or-num: func [
"This function will only concatenate strings or integers."
val1 [string! integer!] "First string or integer"
val2 [string! integer!] "Second string or integer"
] [
join val1 val2
]
help concatenate-string-or-num
concatenate-string-or-num "Hello " "there." ; this works correctly
concatenate-string-or-num 10 20 ; this works correctly
concatenate-string-or-num 10.1 20.3 ; this creates an error
6.5 Doing Imported Code
You can "do" a module of code contained in any text file, as long as it contains the minimum header "REBOL [ ]" (this includes HTML files and any other files that can be read via REBOL's built-in protocols). For example, if you save the previous functions in a text file called "myfunctions.r":
REBOL [] ; THIS HEADER TEXT MUST BE INCLUDED AT THE TOP OF ANY REBOL FILE
sqr-add-var: func [num1 num2] [print square-root (num1 + num2)]
display: func [filename] [view layout [image load filename]]
cls: does [prin "^(1B)[J"]
You can import and use them in your current code, as follows:
do %myfunctions.r
; now you can use those functions just as you would any other
; native function:
sqr-add-var
display
cls
Here's an example function that plays a .wave sound file. Save this code as C:\play_sound.r:
REBOL [title: "play-sound"] ; you can add a title to the header
play-sound: func [sound-file] [
wait 0
ring: load sound-file
sound-port: open sound://
insert sound-port ring
wait sound-port
close sound-port
]
Then run the code below to import the function and play selected .wav files:
do %/c/play_sound.r
play-sound %/C/WINDOWS/Media/chimes.wav
play-sound to-file request-file/file %/C/WINDOWS/Media/tada.wav
Imported files can contain data definitions and any other executable code, including that which is contained in additional nested source files imported with the "do" function. Any code or data contained in a source file is evaluated when the file is "do"ne.
6.6 Separating Form and Function in GUIs - The Check Writer App
One common use for functions is to help keep GUI layout code clean and easy to read. Separating widget layout code from action block code helps to clarify what each widget does, and simplifies the readability of code which lays out screen design.
For example, the following buttons all perform several slightly different actions. The first button counts from 1 to 100 and then alerts the user with a "done" message, the second button counts from 101 to 200 and alerts the user when done, the third button counts from 201 to 300 and alerts the user when done (the "for" function performs a count operation):
view layout [
btn "Count 1" [
for i 1 100 1 [print i]
alert "Done"
]
btn "Count 2" [
for i 101 200 1 [print i]
alert "Done"
]
btn "Count 3" [
for i 201 300 1 [print i]
alert "Done"
]
]
The action blocks of each button above can all be reduced to a common function that counts from a start number to an end number, and then alerts the user. That function can be assigned a label, and then only that label needs to be used in the GUI widget action blocks:
count: func [start end] [
for i start end 1 [print i]
alert "Done"
]
view layout [
btn "Count 1" [count 1 100]
btn "Count 2" [count 101 200]
btn "Count 3" [count 201 300]
]
In large applications where the actions can become complex, separating function code from layout code improves clarity. You'll see this outline often:
action1: func [args] [
actions
actions
actions
]
action2: func [args] [
other actions
other actions
other actions
]
action3: func [args] [
more actions
more actions
more actions
]
view layout [
widget [action1]
widget [action2]
widget [action3]
]
Here's a simple check writing example that takes amounts and names entered into an area widget and creates blocks of text to be written to a check. One function is created to "verbalize" the entered amounts for printing on the check (it converts number values to their spoken English equivalent, i.e., 23482194 = "Twenty Three million, Four Hundred Eighty Two thousand, One Hundred Ninety Four"). Another function is created to loop through each bit of raw check data and to create a block containing the name, the numerical amount, the verbalized amount, some memo text, and the date. The GUI code consists of only 4 lines:
REBOL [title: "Check Writer"]
verbalize: func [a-number] [
if error? try [a-number: to-decimal a-number] [
return "** Error ** Input must be a decimal value"
]
if a-number = 0 [return "Zero"]
the-original-number: round/down a-number
pennies: a-number - the-original-number
the-number: the-original-number
if a-number < 1 [
return join to-integer ((round/to pennies .01) * 100) "/100"
]
small-numbers: [
"One" "Two" "Three" "Four" "Five" "Six" "Seven" "Eight"
"Nine" "Ten" "Eleven" "Twelve" "Thirteen" "Fourteen" "Fifteen"
"Sixteen" "Seventeen" "Eighteen" "Nineteen"
]
tens-block: [
{ } "Twenty" "Thirty" "Forty" "Fifty" "Sixty" "Seventy" "Eighty"
"Ninety"
]
big-numbers-block: ["Thousand" "Million" "Billion"]
digit-groups: copy []
for i 0 4 1 [
append digit-groups (round/floor (mod the-number 1000))
the-number: the-number / 1000
]
spoken: copy ""
for i 5 1 -1 [
flag: false
hundreds: (pick digit-groups i) / 100
tens-units: mod (pick digit-groups i) 100
if hundreds <> 0 [
if none <> hundreds-portion: (pick small-numbers hundreds) [
append spoken join hundreds-portion " Hundred "
]
flag: true
]
tens: tens-units / 10
units: mod tens-units 10
if tens >= 2 [
append spoken (pick tens-block tens)
if units <> 0 [
if none <> last-portion: (pick small-numbers units) [
append spoken rejoin [" " last-portion " "]
]
flag: true
]
]
if tens-units <> 0 [
if none <> tens-portion: (pick small-numbers tens-units) [
append spoken join tens-portion " "
]
flag: true
]
if flag = true [
commas: copy {}
case [
((i = 4) and (the-original-number > 999999999)) [
commas: {billion, }
]
((i = 3) and (the-original-number > 999999)) [
commas: {million, }
]
((i = 2) and (the-original-number > 999)) [
commas: {thousand, }
]
]
append spoken commas
]
]
append spoken rejoin [
"and " to-integer ((round/to pennies .01) * 100) "/100"
]
return spoken
]
write-checks: does [
checks: copy []
data: to-block a1/text
foreach [name amount] data [
check: copy []
append/only checks reduce [
rejoin ["Pay to the order of: " name " "]
rejoin ["$" amount newline]
rejoin ["Amount: " verbalize amount newline]
rejoin ["Sales for November " now/date newline newline]
]
]
a2/text: form checks
show a2
]
view layout [
text "Amounts and Names:"
a1: area {"John Smith" 582 "Jen Huck" 95 "Sue Wells" 71 "Joe Lask" 38}
btn "Create Check Data" [write-checks]
a2: area
]
One of the clear benefits of using functions is that you don't have to remember, or even understand, how they work. Instead, you only really need to know how to use them. In the example above, you don't have to understand exactly how all computations in the "verbalize" function work. You just need to know that if you type "verbalize (number)", it spits out text representing the specified dollar amount. You can use that function in any other program where it's useful, blissfully unaware of how the verbalizing magic happens. Just paste the verbalize code above, and use it like any other function built into the language. This is true of any function you create, or any function that someone else creates. Working with functions in this way helps improve code re-usability, and the power of the language, dramatically. It also encourages clear code structures that others can read more easily.
Note: The verbalize algorithm was partially derived from the article at http://www.blackwasp.co.uk/NumberToWords.aspx (C# code):
6.7 A Full Featured Group Note Sharing App
In the previous section, the "Group Reminders" and "FTP Chat" applications demonstrated how to save text messages to a web server, so they can be shared with a group of connected users. This application builds on the same idea, adding a number of useful features. The message list display can be updated manually or automatically, at any chosen interval. A repeated beep can be played as an alarm to notify users of new messages (a beep is created with the code: call "echo ^G"). Beep notifications can be turned on and off. Messages are stamped with a time and date, and numbered. Messages can be erased individually by number, or the entire note list can be removed at once. An unlimited number of private "rooms" can be created, simply by changing the URL. A new file will be automatically created at the entered URL, if it doesn't exist.
All of the features in this program are implemented as separate functions. The "update" function, in particular is called not only when the user clicks the update button, but also by other functions, any time a change is made which requires updating the display (when erasing messages, when autoupdate is turned on, etc). Notice that the GUI layout is kept clean, and each widget simply calls a function:
REBOL [title: "Group Notes"]
url: ftp://user:pass@site.com/public_html/Notes
beep: false autoupdt: false call ""
write/append %notes.txt ""
update: does [
if error? try [notes: copy read/lines url] [write url notes: ""]
if ((beep = true) and (notes <> read/lines %notes.txt)) [
loop 4 [call "echo ^G" wait 1]
]
write/lines %notes.txt notes
display: copy {}
count: 0
foreach note reverse notes [
either note = "" [
note: newline
] [
count: count + 1
note: rejoin [count ") "note]
]
append display note
]
a/text: display
if a/text = "" [a/text: copy " "]
show a
]
autoupdate: does [
either autoupdt: not autoupdt [
time: request-text/title/default "Refresh display (seconds):" "10"
b/text: join "Auto Update: " autoupdt show b
forever [
either autoupdt = true [wait to-integer time update] [break]
]
] [
b/text: join "Auto Update: " autoupdt show b
]
]
submit: does [
if f/text = "" [focus f return]
if error? try [
write/lines/append url rejoin [
"^/^/" now " (" n/text "): " f/text
]
] [alert "ERROR: Not Saved (check Internet connection)" return]
update
f/text: copy "" show f focus f
]
erase: func [arg] [
if true = request rejoin ["Really erase " arg "?"] [
write/lines to-file replace/all replace form now "/" "--" ":" "_"
notes
if arg = "all" [write url ""]
if arg = "" [
indx: (
3 * to-integer request-text/title/default "Index:" "1"
) - 2
remove/part at notes indx 3
write/lines url reverse notes
]
update
]
]
changeurl: does [
url: to-url u/text
update
focus f
]
setbeep: does [
beep: not beep
p/text: join "Beep: " beep
show p
]
insert-event-func [either event/type = 'close [quit][event]]
view center-face layout [
h3 "Current notes for:"
u: field 600 form url [changeurl]
a: area 600x260
h3 "Name:"
n: field 600
h3 "New Note:"
f: field 600 [submit]
across
btn "Update" [update]
b: btn join "Auto Update: " autoupdt [autoupdate]
p: btn join "Beep: " beep [setbeep]
btn "Erase" [erase ""]
btn "Erase All" [erase "all"]
do [update focus f]
]
Later in this text, a web based CGI version of this application will be presented, so that users can view and enter notes with either this desktop version of the program, or with a browser based interface, to share and interact with the exact same live data on any platform. Enabling different interfaces can be useful on mobile OSs or on any system where an executable program can't be installed.
7. A Few Useful Data Visualization Tools
7.1 Displaying and Sorting Data Using Spreadsheet-Like GUI Grids
As you've already seen in the "Contacts" example, it's possible to dynamically construct GUIs using foreach loops, to display data in GUI fields. We'll build on examples such as the following in a later chapter:
REBOL [title: "Display a Table of Data"]
; Build 500 rows of 3 columns of random data:
x: copy[] for i 1 500 1[append x reduce [i random "abcd" random 1-1-2012]]
; Display it:
grid: copy [across space 0]
foreach [indx text date] x [
append grid compose [
field 50 (form indx)
field 70 (form text)
field 110 (form date)
return
]
]
view center-face layout [
across
g: box 230x200 with [pane: layout/tight grid pane/offset: 0x0]
scroller [g/pane/offset/y: g/size/y - g/pane/size/y * value show g]
]
Using raw building blocks of GUI field widgets, even the simple example above requires quite a bit of thought to display a small grid filled with table data, and normal features such as column resizing, sorting, etc., require advanced coding skill. To load, display, sort, and save flat lists and blocked table data, the following compressed code is provided to simplify the required code. Just define a block of header labels, assign the variable label "x" to your grid data, and include the compressed grid code, and you've got a fully functional data grid:
REBOL [title: "Table/Grid/Listview Example"]
headers: ["Numbers" "Text" "Dates"]
x: [
[1 "1" 1-1-2012]
[11 "11" 1-2-2012]
[2 "2" 1-3-2012]
]
do decompress #{
789CC518C98EDB36F4AEAF78500FB11228B2074D0AA8B3A01F904BAF860EB245
D9CAD0A2225133720CFF7B1F77D24BE324456A60C626F9F69D1C5B4A86015E4A
3A922778B3199B7445D9FA1996F6670E8763118D21E004CB298735EBF6B02CCE
4EC9AEE3FB74E8CA3581A5B7C8615E447B8D3741B46674DCB5430E94B41BBE7D
822D292BD20F91FA36FC25F8E118F5A423258706341E2C23C04FD975A4ADC0C7
819E7C664D0BCB781B237C9C713271888BA83821CD9946F0B7A355734AEFCDB0
1F38D945949595DA22550EF5D8AE615995BC4CEB8612C87AF26524032FB4644D
0D7AC783CACD9E5C65ACA5FB4C625B8842A9C5B9309D26253ED672FA5B08E3B0
2CD8B065AF401BB9161A0FE50BF945320B56C2A6E1194C2885B45C4D4BFECBCD
56A3BDAE98CAC5303870D663C82C606682B29E929388331FC12908D265119C0B
529F610E33839EC22241D2211517C28E1E740DFEAB279835F00E3E271060845C
14B2B40826554FAA11D3CE91B2B09E8E3692FE3D68FE37775D720A29D75BE851
CAE9C47EDA78B5D51D819CD24E43119B4E8B7A72BAB29EA7CA4546DBBA21B432
0A92866F490F128CF55814726819F7D69E4062335BB35D57F618F79A5A092BA4
35C3DA5582A49CC02388E54A2F9594C50FD0B9BF484719CEF8B4889E0956E075
CF28350AAA555AEED8D85A4F0E69C7B01ACB2F0CBB00C6B85A1D3EBAF4C0EC58
6A44BB5584D0F798030666AE2B05CD842F0CB32CA01788AF7F0C14F5E8C9D07C
25E9A66F6C25EB48BF262D2F37C468213A970043DAB268672F0D79CD501942DA
B4C6369475654BB2452680E04F9194F3692E516983012BB6B345EE2FE02D383E
279077B9E598DD2131AFDF4948AE85E9D18A15AAE951F5B4F6BB4D92681369CC
126D54363D2C3EFA6C25CCD84AE564EEA314A0561D6F18561B212FD2142A032D
F76CE4E0357865CBC259B56E30D92B46861FB1A3C4F0DC03336B14A1A7AF750A
EF1709B22DAB2AA54D4B0CD3B014600B9E647F6E50890DE698A927A29567BCE1
58552A529723C5C6FE5755A9E163C082228BC46F79EC178F9D4F89BDF4E9DA45
B4E0040F22447BC2C7BE5501DA92D754910C6BFB597B083C278ABBAE478E80C2
8F639D15ED4030B7A5CB44EE4E5280C48777992B9CB363588AFF2343FD8D7439
0345348FE1E76C8407BC1F09DC3FDA0E6007AFBF150B88552B535A42FC141721
0D254A60095FFD33E52512A355FA5D7A7F123CEA9EEDBE2B38844B6E60A3C90B
BB7AC4D138B399A1206C970086CECC882E7792DBEDF929B4A625736E5161B20C
855126358089D5C66F0C3BAC5463D7511C08825E475E48FB84718B851F9BA1C8
75EC8F0C1BDFEBB6E1A2D2FA7B77BFCFDFCBBF0F1F9404122FD7E8EF5489370A
B6ACC52BC297DC28E2F31026CD4DB600991A6EEDA2DD8256BB20D4DD1C99CF3D
01E4A1EA2F6BDBBC3C1692F717116E44CCA6036D2A34F65E8C2DDA0CED062BBD
9D06B0DF12BF3B3762E701DE8C1DF635373D288238318AFB831E46826B442DF9
BB99643DAAF29CDF88E90B6070E1F1012ED5A230287CA94BCAD38ABD62540DBC
94C2E31493034E69753D104F3C214D8AF1A0653C0463573F7CC5D140F8B96E7A
8CD4966C4A8E898CB54F51C442EFD14F02648C3BE90ED36BC335621AEA01165A
7966D44E2CEEA5DD5B2860642E12ADF3299DF0EC766A9764F24FF0F7454ADBAB
525D3BB926D329BC91E8F2FE35796485D04E399DC44E288939C1E21EED2FBCAC
530112C4101C9CA78E36826FC691FC8E10AE2F86B748CC4B0187C11A6CDB6075
E11F5EB590A8854173DD9FB640F3A95828F5C905448C59A46A785064B04EF7AE
C4B093023363508A81D1543E6C0AB5AC69A26BE8CD5C6F190F19664AB59B4B58
7869D53532E873A691AAC3E0B6182AF9EDA1514EDD29CCA70FF3C88E856E86C6
A39919783FCEFD693A89BE3511FFD0A86DEE25105CF0641F066B66D1B5223DD8
B9195A154429B5151F4ABC350D628693CF5FE666211DAF59C36AE49CB5560B25
2476315861F83FAB1E0B2BECDCD1B71EBD9C2C6650B03E39887FDB23621E72C3
F888C301354F5B4396E319D48450701DF028E604317059979EB1928C0C79FBB6
2173F1DC44B9D156E916F7312CFE9850D72014A5B91FECD483D32366E2536C1F
19F797225CE5A1BE8EA9FB9A0B07FF2E2D6DBF2694828C5E0D2015B74969A1CF
FDF7B34E101F44D4AE90721C3D35507DF3702AE799C15E211C6139A5AA53F45A
7199BB71DC05971DF4C3801AF6C08E7DFAA299EBBBBE78E5A0DA7C3A2FFC7BF9
5B252884CED06E385EE3FA4CF6E26FCD2AA43A765884DC7304A40BF35A118089
F12404BC0CD761894BCF68DECDAF039F5356D0C7C87BF1B6CFC2764FBD093BD5
CC83F03FD2FC2B1641170000
}
view center-face gui: layout gui-block
Other options such as coloring rows, and specifying column data types, using the compressed table code above, are demonstrated in the following example. This is a critically useful tool for data entry, visualization, and demonstration:
REBOL [title: "Table/Grid/Listview Example With Expanded Features"]
headers: ["Numbers" "TEXT (Note Sort)" "Dates"] ; REQUIRED COLUMN LABELS
; ALL THESE ADDITIONAL SETUP PARAMETERS ARE *** OPTIONAL ***:
x: [[1 "1" 1-1-2012][11"11"1-2-2012][2"2"1-3-2012]] ; some default data
colors: [blue black red] ; specify column colors like this
empty-space: 235 ; size of blank GUI area to appear below grid
svv/vid-face/color: white ; default GUI face color
; Here's how to include GUI layout code to appear above the grid:
gui-block: {
h3 "RIGHT-CLICK/DRAG HEADERS TO RESIZE COLUMNS. RESIZE WINDOW..."
text "Click headers to sort (note that sort is DATA-TYPE SPECIFIC)."
text "Notice Arrow Keys, PgUp/PgDn Keys, Scroll Bar, and highliting"
text "Click any cell to edit data. Buttons load and save data to HD."
}
; The following line automatically fits grid to resized GUI window:
insert-event-func [either event/type = 'resize [resize-fit none] [event]]
do decompress #{
789CC518C98EDB36F4AEAF78500FB11228B2074D0AA8B3A01F904BAF860EB245
D9CAD0A2225133720CFF7B1F77D24BE324456A60C626F9F69D1C5B4A86015E4A
3A922778B3199B7445D9FA1996F6670E8763118D21E004CB298735EBF6B02CCE
4EC9AEE3FB74E8CA3581A5B7C8615E447B8D3741B46674DCB5430E94B41BBE7D
822D292BD20F91FA36FC25F8E118F5A423258706341E2C23C04FD975A4ADC0C7
819E7C664D0BCB781B237C9C713271888BA83821CD9946F0B7A355734AEFCDB0
1F38D945949595DA22550EF5D8AE615995BC4CEB8612C87AF26524032FB4644D
0D7AC783CACD9E5C65ACA5FB4C625B8842A9C5B9309D26253ED672FA5B08E3B0
2CD8B065AF401BB9161A0FE50BF945320B56C2A6E1194C2885B45C4D4BFECBCD
56A3BDAE98CAC5303870D663C82C606682B29E929388331FC12908D265119C0B
529F610E33839EC22241D2211517C28E1E740DFEAB279835F00E3E271060845C
14B2B40826554FAA11D3CE91B2B09E8E3692FE3D68FE37775D720A29D75BE851
CAE9C47EDA78B5D51D819CD24E43119B4E8B7A72BAB29EA7CA4546DBBA21B432
0A92866F490F128CF55814726819F7D69E4062335BB35D57F618F79A5A092BA4
35C3DA5582A49CC02388E54A2F9594C50FD0B9BF484719CEF8B4889E0956E075
CF28350AAA555AEED8D85A4F0E69C7B01ACB2F0CBB00C6B85A1D3EBAF4C0EC58
6A44BB5584D0F798030666AE2B05CD842F0CB32CA01788AF7F0C14F5E8C9D07C
25E9A66F6C25EB48BF262D2F37C468213A970043DAB268672F0D79CD501942DA
B4C6369475654BB2452680E04F9194F3692E516983012BB6B345EE2FE02D383E
279077B9E598DD2131AFDF4948AE85E9D18A15AAE951F5B4F6BB4D92681369CC
126D54363D2C3EFA6C25CCD84AE564EEA314A0561D6F18561B212FD2142A032D
F76CE4E0357865CBC259B56E30D92B46861FB1A3C4F0DC03336B14A1A7AF750A
EF1709B22DAB2AA54D4B0CD3B014600B9E647F6E50890DE698A927A29567BCE1
58552A529723C5C6FE5755A9E163C082228BC46F79EC178F9D4F89BDF4E9DA45
B4E0040F22447BC2C7BE5501DA92D754910C6BFB597B083C278ABBAE478E80C2
8F639D15ED4030B7A5CB44EE4E5280C48777992B9CB363588AFF2343FD8D7439
0345348FE1E76C8407BC1F09DC3FDA0E6007AFBF150B88552B535A42FC141721
0D254A60095FFD33E52512A355FA5D7A7F123CEA9EEDBE2B38844B6E60A3C90B
BB7AC4D138B399A1206C970086CECC882E7792DBEDF929B4A625736E5161B20C
855126358089D5C66F0C3BAC5463D7511C08825E475E48FB84718B851F9BA1C8
75EC8F0C1BDFEBB6E1A2D2FA7B77BFCFDFCBBF0F1F9404122FD7E8EF5489370A
B6ACC52BC297DC28E2F31026CD4DB600991A6EEDA2DD8256BB20D4DD1C99CF3D
01E4A1EA2F6BDBBC3C1692F717116E44CCA6036D2A34F65E8C2DDA0CED062BBD
9D06B0DF12BF3B3762E701DE8C1DF635373D288238318AFB831E46826B442DF9
BB99643DAAF29CDF88E90B6070E1F1012ED5A230287CA94BCAD38ABD62540DBC
94C2E31493034E69753D104F3C214D8AF1A0653C0463573F7CC5D140F8B96E7A
8CD4966C4A8E898CB54F51C442EFD14F02648C3BE90ED36BC335621AEA01165A
7966D44E2CEEA5DD5B2860642E12ADF3299DF0EC766A9764F24FF0F7454ADBAB
525D3BB926D329BC91E8F2FE35796485D04E399DC44E288939C1E21EED2FBCAC
530112C4101C9CA78E36826FC691FC8E10AE2F86B748CC4B0187C11A6CDB6075
E11F5EB590A8854173DD9FB640F3A95828F5C905448C59A46A785064B04EF7AE
C4B093023363508A81D1543E6C0AB5AC69A26BE8CD5C6F190F19664AB59B4B58
7869D53532E873A691AAC3E0B6182AF9EDA1514EDD29CCA70FF3C88E856E86C6
A39919783FCEFD693A89BE3511FFD0A86DEE25105CF0641F066B66D1B5223DD8
B9195A154429B5151F4ABC350D628693CF5FE666211DAF59C36AE49CB5560B25
2476315861F83FAB1E0B2BECDCD1B71EBD9C2C6650B03E39887FDB23621E72C3
F888C301354F5B4396E319D48450701DF028E604317059979EB1928C0C79FBB6
2173F1DC44B9D156E916F7312CFE9850D72014A5B91FECD483D32366E2536C1F
19F797225CE5A1BE8EA9FB9A0B07FF2E2D6DBF2694828C5E0D2015B74969A1CF
FDF7B34E101F44D4AE90721C3D35507DF3702AE799C15E211C6139A5AA53F45A
7199BB71DC05971DF4C3801AF6C08E7DFAA299EBBBBE78E5A0DA7C3A2FFC7BF9
5B252884CED06E385EE3FA4CF6E26FCD2AA43A765884DC7304A40BF35A118089
F12404BC0CD761894BCF68DECDAF039F5356D0C7C87BF1B6CFC2764FBD093BD5
CC83F03FD2FC2B1641170000
}
; APPEND ANY WIDGETS AND/OR GUI CODE TO APPEAR BELOW THE GRID, HERE:
append gui-block [
; REPLACE 'BTN' WITH 'KEY' TO HIDE BTNS AND STILL USE KEY SHORTCUTS.
; CHANGE/REMOVE BUTTONS AND/OR KEYBOARD SHORTCUTS AS NEEDED:
text "" return
btn "Insert (Ins)" keycode [insert] [add-line]
btn "Remove (Del)" #"^~" [remove-line]
btn "Move (CTRL+M)" #"^M" [move-line]
btn "Grow (+)" #"+" [resize-grid 1.333]
btn "Shrink (-)" #"-" [resize-grid .75]
btn "Fit (CTRL+R)" #"^R" [resize-fit]
return
btn "Load Blocked (CTRL+O)" #"^O" [load-blocked/request %blocked.txt]
btn "Save Blocked (CTRL+S)" #"^S" [save-blocked/request %blocked.txt]
btn "Load Flat (CTRL+U)" #"^U" [load-flat/request %flat.txt]
btn "Save Flat (CTRL+F)" #"^F" [save-flat/request %flat.txt]
; LOAD A DEFAULT DATA *FILE HERE (instead of specifying it in code above):
; Not that the "load-flat" and "save-flat" functions load "flat" blocks,
; which are simply long sequences of data values. The "load-blocked" and
; "save-blocked" functions load block which have rows enclosed inside
; deliniated blocks. All of those functions provide an optional
; "/request" refinement that allows the user to select a file:
; do [load-blocked %blocked.txt]
]
view/options center-face gui: layout gui-block [resize]
Details about how to build GUI grids from native REBOL code, including the compressed code above, will be examined fully in later sections of this tutorial. For now, the general solution above is immediately functional, and can be applied to the majority of situations where a visual grid tool is required to enter, display, sort, load, save, and otherwise manipulate table data.
7.2 Creating Graphs, Plots, and Charts with "Q-Plot"
REBOL's complete graphics toolkit will be fully explored in detail, in future sections of this tutorial. For the moment, the "q-plot" dialect by Matt Licholai is a simple solution for creating bar, line, pie and other charts from block data. You can download and run the q-plot dialect like this:
REBOL []
if not exists? %q-plot.r [write %q-plot.r read http://re-bol.com/q-plot.r]
do %q-plot.r
Once q-plot is downloaded, graphing a block of values is as simple as this:
view quick-plot [
600x400 ; set the program window size
bars [5 3 8 2 10 3 4 9 5 7] ; set graph type and data to plot
]
Using the code above, here's a complete bar graph example:
REBOL [title: "Minimal Bar Graph Example"]
if not exists? %q-plot.r [write %q-plot.r read http://re-bol.com/q-plot.r]
do %q-plot.r
view quick-plot [
600x400
bars [5 3 8 2 10 3 4 9 5 7]
]
Notice that the q-plot dialect syntax looks very much like REBOL's build in VID dialect for building GUIs. The primary difference is that "view layout" is replaced with "view quick-plot". Here's an example that demonstrates how to adjust colors, add text labels, and attach scales for each XxY data axis:
REBOL [title: "Another Bar Graph Example"]
if not exists? %q-plot.r [write %q-plot.r read http://re-bol.com/q-plot.r]
do %q-plot.r
view center-face quick-plot [
600x300
y-min 0 ; minimum value to display on y axis
fill-pen blue ; set fill color
pen green ; set outline and text color
bar-width 80 ; set bar width
bars [5 3 8 2 10 3 4 9 5 7]
pen black ; set outline and text color
label "Fat Bars" ; optionally add labels
y-axis 11
x-axis 10
]
Pie charts are just as simple to create. Here's a pie chart example with 2 specified sections of the pie "exploded" out for emphasis (the "explode [3 5]" code is optional):
REBOL [title: "Exploded Pie Chart"]
if not exists? %q-plot.r [write %q-plot.r read http://re-bol.com/q-plot.r]
do %q-plot.r
view center-face quick-plot [
400x400
pie [10 3 6 1 8] labels [A B C D E] explode [3 5]
title "Exploded Sections C and E" style vh2
]
Line graphs are also simple to create:
REBOL [title: "Simple Line Graph Example"]
if not exists? %q-plot.r [write %q-plot.r read http://re-bol.com/q-plot.r]
do %q-plot.r
view quick-plot [
400x400
line [1 2 4 8 16 32 64 128 256]
]
Here's a more complex line graph example that demonstrates how to plot a predefined block of data, how to add a title, how to add text with a font and defined position (up and over a percentage of the window), and how to attach a scaled grid to the display:
REBOL [title: "Another Line Graph Example"]
if not exists? %q-plot.r [write %q-plot.r read http://re-bol.com/q-plot.r]
do %q-plot.r
my-block: copy [2]
loop 10 [append my-block (2 * last my-block)]
option-font: make face/font [
size: 30
style: [italic bold underline]
name: font-serif
]
view center-face quick-plot [
500x500
scale linear
line [(data: copy my-block)]
title style vh1 "Linear scale"
x-axis 7 border
y-axis 7 border
x-grid 7
y-grid 7
text font option-font "Formatted Text" color red up 50 over 40
]
Here's an extension of the "Paypal Reports" program you've seen earlier in this text. It plots all the gross transaction numbers using a bar graph, and makes a pie chart of transactions with Saoud Gorn:
REBOL [title: "Paypal Reports Charts"]
transactions: copy []
saoud: copy []
dates: copy []
foreach line at read/lines http://re-bol.com/Download.csv 2 [
row: parse/all line ","
append transactions to-integer row/8
if find row/4 "Saoud" [
append saoud to-integer row/8
append dates replace row/1 "/2012" ""
]
]
if not exists? %q-plot.r [write %q-plot.r read http://re-bol.com/q-plot.r]
do %q-plot.r
view quick-plot [
594x400
bars [(data: copy transactions)]
label "All Paypal Transactions"
]
view center-face quick-plot [
495x530
pen blue
pie [(data: copy saoud)] labels [(data: copy dates)] explode [1 2 3]
title "Saoud" style vh2
]
This example demonstrates how to include several plots in one window. The default alignment for multi-plots is vertical. The "multi-plot/across" refinement lays them out horizontally:
REBOL [title: "Multi-Plots"]
if not exists? %q-plot.r [write %q-plot.r read http://re-bol.com/q-plot.r]
do %q-plot.r
m-plots: multi-plot/across 594x200 [
[
title "2 to a Power"
pen green
line [0 2 4 8 16 32 64 128]
]
[
title "Rizing"
pen white
line [0 5 10 15 20 25]
]
[
title "Falling"
pen blue
line [25 20 15 10 5 0]
]
]
view m-plots
This example demonstrates how to place plots in sub-panels for easy switching:
REBOL [title: "Switching Plots"]
if not exists? %q-plot.r [write %q-plot.r read http://re-bol.com/q-plot.r]
do %q-plot.r
window: layout [
vh2 "Switching Plots"
guide
pad 20
button "Sine Wave" [graph/pane: plot1 show graph]
button "Parabola" [graph/pane: plot2 show graph]
button "Cubic Curve" [graph/pane: plot3 show graph]
return
box 2x204 blue
return
graph: box 354x204 coal
]
data1: copy []
data2: copy []
data3: copy []
for i -400 400 .5 [
append data1 sine i
append data2 (i * i)
append data3 (i ** 3)
]
graph-size: 350x200
plot1: quick-plot [
(graph-size)
line [(data1)]
title "Sine Wave"
]
plot2: quick-plot [
(graph-size)
line [(data2)]
title "Parabola"
]
plot3: quick-plot [
(graph-size)
line [(data3)]
title "Cubic Function"
]
plot1/offset: 2x2
plot2/offset: 2x2
plot3/offset: 2x2
graph/pane: plot1
view window
Be aware that instead of downloading and running the q-plot dialect from an external file, you can include the compressed q-plot code directly in your program:
REBOL [title: "Q-Plot Compressed"]
do decompress #{
789CED7DFB93E3B871F0EFFB57E0A6F2D5483BC779DDAD1F5ADF6DD93EA77229
3B71EE6C5752AA498A92A0197A299126A91DCA17FFEFE9071E0D02943433BB4E
3E27AABB5989041A40BFD16800DFFDEA17FFFC6B357FA5E0F3BBA22BF54C9DFD
CBAE58BE57BF2DAB4E7D53E4A55E7667F4FE9BBC83D7376FB2BFD78BECF6FAFA
969EFE41376D516D67EAFAF2E6F29A1EFD7D8170FEDF9FB21A605C36F4ECE7BB
EEA16A00FA6FF2AE53BF2E960F5599170CF8BBE2FEA16BE1DD64395500F8E673
84AE7E733928F7FB36BF07C03FFCBED52A6F55AE1665053D5D6BBD2AB6F7EA3B
1ACB1FBEFD46ADB8DB974A7DAFB5D27F361D51EBAA512BDDE545D9FE8520FE76
D7D4558B307FDB541F8A15C0557FA2E1E7DB95D279BB575DA576D09E01492010
5A872D165B6E9481FDB2DA6CF4B60360BFABB26F2AF5D557F4587EFE0146AAB2
2C7AFE1BBDC2C7FF9EFD7B261F5FA8DFD72BC0FAD5AFFA9A3AE446B2AA963B6C
2CEF00F711B85F578F004E25E0FD7CB5526D972FDF43838BBC51CB87BCE9DAA8
E06BF59D7EAC9AF7AA576575AFDA655EC278A91D1EEB3F146D5735FB999A5F5F
02E1D5FCE63AFB462F912D6ED4D9B7DBA2037C39A42DF216DAABB66AB327E4A9
F56EBBC49EB767778AE021945B80F2A580F28BBC2D9640E81A40B40ABB037DB8
9435BE801A3FF6357EF8E543BEBD879680668DEE76CD16C859E6FB6AD7A96AF1
4700F3B9DAE408057AD6D6F006495CE8E6F22F02E8976AFE26FBC77C4B2CAECE
7E59EA7C0B3077B55A56C82040872553BAFD5CE5AB15BC03E4E8AC06C6D64D38
A437D04101EC875F564D03DD802AB9AA9B6A51EA8D7A2CBA0755ADD7AD66A6EA
1EB48A286A3F3D52AFE9545D15DB8EFAB205742EA133BAC1CA40D3560EE647D0
811F253B10345FE60B5DB604EF43DE14D5AE558BDDBD5A17BD0EC0FD18C0FD54
20E7E734FCCDAEEC8A2C20AC44C24FD4FCF64654FA4DB105215A225E33C02A36
DAE84DF50118A65AABFBB25AC03720F72A40E54F018A68FA87EF090F4C712478
BE75342E3630B80F5AD5800CCD4AE2F3518C02051D8F6B46C112FEC0F70EF500
B039FC2531F18800454784B58A10F1BAA94B8D785D528F5AEC12B5FF3981643E
591047D7858E01DE10A104C090EDAE2CCB0968BA0794E317071068F5EA8E40FE
6A034A0E74F026CBDA2C2B8D1ACDB2426B9D6555C3B2EC9052168B262771F68F
F4070D10E605B2D606342CE8A13B5F03A4A7035DB8811279598A17DDBE067D3A
37A27FE79EAF2AE8129889F97D93D70FC5B2B5DAA105990416BCDF15BE7007D8
D7AB6CB75D69B019DB6A2B44A2DDD575D574C3C73046BD45553E280DE3CDCBB6
025C38ED492F1153AF48D7D3D3CCF466861A421B56FA0CF0C1AAFD0FDF67DFAE
40B3ABBFC37FAC65FBFC83BAB9FCE20BB459B757F0DFCD4FC13CCEBEB89EDDFC
0407D529D0DBEAEF4065BAEED097B7A09C871FF3E26CA59765DEE83316005006
7907225A966AA1D10EADD0E6A086E00E9A5A60BB1F8A56C17F5BB085AC00EB06
08083A82E104B5D4BAA936AEEA62B758A0764756837A46FE88FC4A4D48090385
F754BFCD37647ACFA12B024007D4D5EDF6BC8376BAA65AED966848B7FAD18001
B12E8094C65C5D4E4DD541A7573B1664543DEF6C8F1F4149B3615F361A581014
1380B2C6089B478B8C83841A2D5A70675B48B515562DB497A6C6F7BA43C8A019
DEEB7D123DD00964A2515A196A81C2CEF6D90699DA1537CFF27EF0AC4F94EB07
E53AF6BFC2DF19A8C5563C244D9D6D1BF108747EF658ACBA07F16C9F81DF908B
07FDF0419DB5C59F6573EB5D590E9F35BA05DD2E1E0086C0D0351BD9A5955EE7
502A5B57DB402AEDF3655556617F41E6860F1D10E075F17857C725ABC76D0C13
C8A86334F419BF0830133DEA333000191980A060EA2913A5EDF6C1D3D57E9B6F
8A65541A9E67F5D222F000438D7E4C1DE35101758CBD0496754F1E1FF4D608D1
739A606A7978331221610AE6CBBC5B3E78ED7C0516352FDD4FE26D45DCAC481E
D4DE7C5F15EB35FC435869EC9755F667DD54501E547C4DCC4B5F3ABDA9B345F9
9E95B36FBC58AB89E3BAA99A1BC7CE3DBA83E1B6EF8B9A04D80D82140D3B6C00
E0519F833B9097A041567B6020E0D0A20B75F233F036201243F827DD83CE8549
C7B6DD0195487F234A906A8414FAD6839B55EED0DD8032A00B040014CDA6AA75
032AD7F9CD383634ACA849518EB185DC330114BD2F40DB5F8670BEA9C83B0457
A8510D4F6E406F376441CB42AFB813971F71FC1A342E366035A39A1B05E99EDC
09BE1235585F0DDEB11452FD75D1B484C862B3DB64D5DA5688CA03B551D6DE19
4E8C01E2C7F21AF40B3C0960937A6F00268B3F3EC08456CD05DCE128467BEC5B
DA2263044C3EFCDC454FC32777A76307EC5EB362DF353DACBF6534BD8ABF8D30
28C8E3DC586BF7E4190C8AF50D09F2FE290C8AED7F0ACCC7A318EDF1A767D024
765ABDACB62773E8DF1A9E4EE45036ACF3DEABD0FE800AEDC738B41F5112FD11
C4F74F540D23F0F013203E3988D10E7F6206356DDD3C9740C89ABD5721FD0115
72884049213981404F928C2710E8B064F49F4A32F0738C62C714B2E860A9B7F7
DDC3BB319590009EA8BCAE764DF77048558D4BB7F4C7806CE8A4B9B0356A410A
2ED54DB1D4EDE714E1E4701CBA7BCBB26A752BDD60F4E5ECA8EDFC2EEA9C6813
AA04F39234574E26F9A2354A3633EEC095F9FD5ADD5C5F4FD5D7761E334DDA3F
314DEA9A9D3E6E12448535CC6EF571F4F15402E057D94A2F8B4D5E7A3670D0D2
E3CBB77B3537E3F94A5D9B11C2B7148323AC9B6B3509D0313D9525A9A90937F0
F55718B59C5A503FE39FD0A6AD9364BDB7C15C013DFC4505E47DC83F681F7F69
8BFB38E48F1F24E46462C6E0A83A8571D871998730A6E9A9D2107669D9546DAB
7002972C6C1B370D5FA8B03300E874E109BB135024CD223CCD9CA989E0128E72
5CEDA7C4D3C84452263996BEAC36F5CE85DE8359EA50E91FE536C75D18210E40
E102D975521C82426EB6FC3A41B41154E1A7589BB67F063C7EB495ADBECFBBB1
B1862001EB5FAB78301307024B5C593EBFB0A47FED517F3285C306042606A3BF
53C1A4BDC030A96E50B3E2A2424331C3AE52ED43F508862DEFCE5BB5D0B4EA83
337F2CFA6EC836B655DB691A88448E8876F05ACDCC9B750E28A9B90C4281A45F
DFC95A18E448F2668FBC299F4F42B385F27BA3A612167246BEFAA3E8C2185B4A
B843B0D0EAF5E5ED1B94D2F0CD1885ECFA2E93D17DB5219C678D4E4DC5C84474
B1D114459E9FBB8812FC3DB78125FC7BEEE24EF8867D21FA1BF4F97C187E6ADC
2313880A7F9D9BB894F9E7DCC5A7DC3043F0961D1C5FE023431F4FA914DA8681
AC57FC1EFF9EB05EE02283E17AAA9AE06A3886E12BB56AF247BBC8323D04E815
0B02AE62B647837FD6032127E6B33B75F60BEBCDEC6D500B571FA05767AECE20
60B8CF6AC4172EF8C1FF30FAA2E1E5B70C676FF89810C283173C685843443E61
44F78072FD41377B8CF5DFA313BEAB3FB7EE9209F93908A61554B3CB5DD3EC19
E455C802C1B306CD0434B1E300A37966F11DCF0E68583303C3F184353508620D
1AAAD4EB4EC5958B6DABB1545E9426F4AECE912A52F68BEDB2D19C6710889B69
10399444CE3E301CE8857A5D014E960F4C85A417FD1615E579C7AC854E0DC5F4
0D7127F9BA030CB86E20D2FBD03291E100E0B119B2E4264F8EE86EE51CC7824B
8DF7A4CEA01CB84E9E27EA6E7A64A230823EDBE001CB69283609502B7EA0291B
E0125426B36F629E98E8C45CE607DCC9DE1B71777FF6D97D53AC8E0B212B5735
C7A17DE61F6F1B16625EB505548A77910C7A39736CEA110EA37FBEF8BD3A1919
C45918C137E1F247CD5A6B592163ED300F8106F4EAD34B308D392553D6F374BF
C0DC9084396C837D9E0A09B5FE41A8035E85A89F4978523356ADF04200B2F457
093738086C57E5AC4F6086D0551B45515F4D323B44FF985AA1C106521889D4C4
767FAAA484523FA777D343848E804FAC4B40E27310AC1FF40E18A18459D3C055
240C320F5F30E90605088F8151090A84FAEAC5687A0AAA3E01BA46474625704A
10786EFEAFFFD3FFD5340F6B4E52B02FB5F347ACEE8508EBF509F1B6F8CDD46D
2CD0F060A0F64F576B42A3459A2CA293E1F0133992636ED4DA5D8267622B7AFD
1CD64B18633F9D0BE105E67336C419184F46BDB3997165985F4A5E8F89383DC2
BB928BF3BE783613BB0C88147B6FF2E6FD69EC6DE60FC8DFFBEC416372AB637A
14C7DA7C47F4B000141FC1D4BE550F94B6C646AE061E874696796BD203AA6D69
03C03861A69FB7AAE8F4A6152070CEDC740888D26A6C4A22CFB168E13A986081
A39797A89D0D608C3E99D99D7932555F8170DD0D15B8488961864E84A6458A8C
592D4A144AE5A3784A0C525EFC0BE412CB059EEA8ECA09B7EEF499BC8332A245
3C201298438B03036C22340B7D6C8E6EC061D6F03BD7747A1DC74D11A47FD373
C473388ED07F4EC04A4C4522B0EA18DC4378B7A234F3042307E9F6CD9815908E
556F1CAB50B78CCEC0C6156FA7FBEEA8E245B31CA3C88EE088DF0490DA8E9279
5303198E88554A622033E54D7ED2898212456022050A8FAD58D5981A6BDE36D5
6EBBCAAED5A40065CFBAFE18B7489EB85002756C45004E7128D6190CFDC24BA4
51BDF17CED39AEDF617AA73DC08809D244170DA7091F08319B8E39FD7337676C
DFF2D3BBF1813172C0B27A69416934389A0E4D6A50BF588711458E54F9826F29
693A07146D31AC881989642ED0F2A0D053A400A6EF404A4CB4C2459EB2C51477
5ED8C24C6555E1081F0BB1F8F31611F840B649A389420C71AA2A556BC88E6A30
03958023D666B03E06F87583937653BB622CB691C4BD9CE203CC3E41F1DACF9B
37C957230B31F8F92930F7E4269419B34C50B3E45AFE104A385645D4CAC7D050
2946152DDFCD6BCFA649AF6DFF2287ED096ED99E750347369C01E01924CF92F6
DE217BF984E47F68BC2261788541B1818B1BD1C85342153FFA78F634E6CDE707
223CBB86C2B14F1BD6FFDE88C373CCCECB020F317A8E74FEF4A002FDE1DC84EA
A15C1E15F3C1CAC2BCAA61E6F2807BE6CAEA913322EE1405AF8B56E65170687A
74BD01C1B404A74540ADC9AD008EEFD88FC1F158CAD990A825F0C75103D4053B
D9096631D42D37C5095E615F6726973378C1FD77D91FC13B21CF38BA99301593
60950054C2974A08CE1A198910F2339CB8CC6DFD9B3B0132E5455F98A686331B
D9D88B02B811D70F65759C574D6EB8DBB1891A95DD13182FB826B8FB04FD8DC9
AAA96ADA5A407B60C4563C4A6AD2252D05B4D13A0713D4F0532C9E2264BBCAF7
E7CC84D802F273ABEF116854E9C4E0CF912594E41C043F6E5985A111FF255657
12CD9FD0E2103AB2F03458AF4F9269802B927B831EC496F7FF8CEF87FB6833DC
E54AAB631F0B81479DFA09AB8ACC30FC341A2E89F92743E618F443A824D61CE2
92961F3F292A9FC78B2C472FC7DFC44F0D994ECF6EFCC00C0BD774756D512AD5
046E27232BF5412F3B988100A639C72FA86F4C0255A4EFC15B6316E82D7D0FDE
B265A097F83578678D03BD1D6411468388A6D2EC54C6515C13C43D840EDCB449
79375A6CBAA1B566DC91F8A0E5E48DE390DB21080556BDD1359098370B020083
C31F08457F09CA233BBC1BE0EE090BB1D265A1BF30ABFD0F74BCAC9FD2F566F3
0CB8E2BBB6A10D6EC6B75080AFA29329849173202DABAB3D4FF40AF3CF5048E7
546262CB82DA4CCCCDA9DD78F95C2AA9F158FA6B1ECE15A57586A88CEBEF71AE
892541DFED6A53494D0F22D80D05D10890B0B3E895F6DD89F518057247E1344D
278A3A5842199AF4661E17BB73BC6B12E5F5903BE71D2FB7C9D27E0B02651EDA
2C405C94B875A56EDF60229F01123A5B020C785CB730479B4BC0B791DB25E7FC
BEEAEBD0A91BD27CE0DF99A0905CFA7A2382B4864FA6C3445F8F90305CE82891
90BF97387A83D914851B4DAA8A18FAA159666A3197987228398269EE0C7060D7
E1740635529721ABCC68AFB3C04F6A0942D4A428D113AB13FA12BB86C5F360E7
B07D3EDC8D2C9E07E587F3344C6DA36DF947676867E458E414A000558E478850
4531F90A26715ED8D2119C68F10C8901262D2317655767ECF6719486C3912E23
EF53258905228305DC06EBA158889DD75EA4AE128AE00233380FC65C5F4998AC
015836A2B4C3F4240C0689C7D4B8E410E063F0E9C01ABBDC71BDBA27E4B5EA0C
03B2188FF5447B3B0236F363446D76798DC1DFB2AD68BB3182A7D3124C38D69C
C471EEAAC4A3DBDBC4FF672805F7C566B0215724D3D768CE47A6CEFB1A07A67B
3ED52BE1AA16145E48EE15602E3D96CBC69E3C27B13207CBC89C7D769DF07A2D
FB275A50F1ACE442F0E9D4A54F63E79F903B77D4DF5F54FD5D2CA0077CE68187
39C2D4A7BB6D0A4F08C8C0E7B37EC12B11653227B7FCD5024DF678028F4F7F3A
817FF6F2701456618C5359A31557F99E9BFABF40D5C70B5445DD7B891FE319E5
7F78A02A1542D078F00FDA1252EFC4D276950F384F4D30FB638B53035C64C30E
C1D378A6ED9874E6779CC970066E8D9A04337020A995ABBBB997A75889E575AD
E9308CC11CC22808C0956D7BAA06BF4FD08887A22A7F03C1BC97C4F212A8B9E7
5375800ACD5637AD9DE783B9F0137ACBA9ACA5E19D70BAECC769BAD8E8A511F0
31C27342A91EB6E61F2FB0849F431C2670E43D19246F569E16AB4B4D80901A49
5372C07E0FB5D4FFBE98D728A0016AFE3F8B7F914A0CCE688A5C2B1A24BB574B
AA143956380EDCEDED9DA91F6868EC31F52003E065804776450FDA2B942C318A
F621AF41C4F0102EA8AA7EE0DFD6CAB4FBCDA22AED7622516BBF31B349B7EC0F
55F181C1EE58C568B6490714F5C3738978C5BDE7FD31F0A7EEA259A6CD2DA47D
F5F33B39C1095E785569272D752790165A694130935B55775737A365F6AECCED
A199E578984BCCFE9E77DC8F4B54E8ED11A1D85ABE41CFA17D31F47E74D3F7F8
7EC760D3B74C44C4ADDF322BE7C806F003A991A1AE3CB2195C36E9B6844BE0FF
3D1BC36576D0607BB81CEBA7DD241E7422D82A2EB1167761C4580DBA364A4121
2B82D50E6C1EA7B4DEFEA4CDE3812A71654FE5DA017FE216F200E0D816F24121
A7C15E8F12F680BD07CB25FB91DE4E3E68D1EC054F8F3E05D86E2A4FF2C700B8
DD692E2B5F85427411F2D26B4FB9E772CF481F046A93E864EF783075EC91243C
754C9B9A174D350F4E31D17CD9C97CE210945EBC36C9F583EA26831EDDAA647D
F13E05A04EF9D3D1ACDCA0892D6EBCEF3304194EFADAC7A203A36AFC88D8B12F
9A25481B7CE674C8ABF90DCD581FE204AF1CBD66FE3010FCFD3408AB22DF60D0
C442B0BF9F0685D5AA180CFD7E1A8C7FA514400FC3FC7E1A103E7C5A74C461F5
664CBC2238ECBB0E784A0670FCDE8DD8B97AD0F96A5857BAACF8E7ADF5025D48
C56D92A7F7A2E7D6ABC52DCBBC3EA186EEE5C0731CBA83662FBF3BB440F09943
A9313796859EDEE8AEE6791B465F9B6ED805F376868D6604CABDE20AF4E6227C
33EC37B276D84CDC7766999775DF058F3FEA680860AC70B8C2951BD9D5FE2E00
968E36D8C2BD6950D61AA28D565C42BC99ADA3E1606364920C3F0B9735B704FF
341DFD8056E0DF0422534AB8C6814DE0EFDEA0771AE2E4489D8BA80EB79ED2F6
1D2D3353799CAB48341EAA7291AE3282798F0D8377EE0F3711AE14480DFC7F98
7F36E6EBAADCDF575B83108309468B453EBD1A20BF70DA76B008E3E7ABC14AB1
7FDEE00964923267F81B66FCB8C24E5DF6EB327C9F41189838F3E903620547F7
F5A018872EDC396CDBDD66A1299E45D3F716CF99C7C882397D7F34E9B88129D1
8A2D65BBDBA81A0FBAC70ED3DE4F3E2BE55635B743AEC1104C3B1BB832F755B9
0AADA71E24685765F1218C082E1AB9D2C1B8DF860F1EC1A50D936BEB5D539721
9C0F4555EA3040F8FE217F5F0C1AAB1EB7830EAD9A7C113C2A75BE0E1E2CC103
D4616B5D1E82D9E4F718090B9E6DF30F61DCB86A70E939F412FFB40B2B61F8A5
ED06B09BAA4A4E072DE5ECF6065CFA1607F7CC68B66B0B81C4B8AF89240220BE
DC8A7764ED9A4A23BF5CA8610EBEE81F32930F319D08DA38CEC48872968BC5AF
B0CD83F90947D75AFCA9EE42398DD432EED1C4E20D7C7E402ACEAA58C839D74C
0E38A1AC04FE83FC354F0C507ECDD1BEF0090FA2273CE7B0B596D50EF9C01390
F681E0DC5CF92040DEC16476A5EF1BAD5BD554E69A03195FFB37323B2DAF93B9
3D6D5D55ABB65205DE81516C776D444A4406116C3E101CEA14FD136DE1C47936
544BCFAF815E48243BE9236D131502D9E5ED2EC882D15B545C182E5F623EE1CD
4F1015D304BB3A5C99925FFC88825FE9823D53BF2DF89CA1E8FD9EDF2FABD112
497396E20A3A4E631A2E1C052C333CB00E3FA90E4F1A28FC662ABA7D9BEAB72F
E67B1F176439FAA86318E933B0CBF5299DB6E50EF59AD3E5FECAA87F921C1F85
F6F62D1EB7BFD46DCB71544D5344BFE86E0EDF7777ED94459B58E4C5F5F5EDCA
39132C98E9E856F37A66926A85AE43CD972CDD43E9E6F54172E1676F8B1DA216
7E003FAF0F660F87C47A3DAA58F7AF131C4608C59C3A73F1414F21E13DDF72D0
D12D36F64603663073E3D44867C7D92BECD56BEEECD00ABCE68E8EF4F3A84133
AA32C14407AA5BAF1819F1F54855B3BC0506DA285A32C16F0E9DB72CD423D51C
2D192ACAC3655F8ED831C82172EA5860F0730A5EB1B1A85882A02F20E658D5B2
AA5654AF4C9F3E67924775D354CD3BD5357BDA2F600DAB9F808CAC988C34CB79
F396F17943A7D99D4949F4C97E702CCDCC1C28164BDF79FCAD0997D1F7B8BAEF
A85D5B770F06DE67F8EDCEDE66A368CF3ECF0E3157CE46D95803E089D7AC59DD
A5261CCBB6A7455A28F4A1E3116DFD026F0873DBF9D7F6CA935D8BC8C8FD9D6D
04C104CCA3F5E933B7A91ADFE39D2734B1E3A70868596DC187DB76624A1886DB
E7C64F1649CB0EE6E09D20F5D1F382CD749ABBBB4F3009A6B9ED7119C8DF7913
6CEE157D8CA96A2AD8842B40F6D70428C98A32BF251C7B169CC26BCF763E7D59
701CF2450AB23D5238D5C018331EC2AADDAB43076744200F0F9B4E1DE60E4ED5
D87A5918DBC06333FD622C6A7977CDCF84CE5B12F734E1F59C26A78281F7630C
DC2719B83FC4C0FD3106EE4F66E0F135CB00D5FD0803F7A30CDC3F8D81FB5319
B81FB259BC1ADA3F8F81FB218FC490ED2AE14762E014560F8F9592964EE55AFC
6B58D7ECFC8FB88F32DF3902E658C4837CCBF5D40294FA43B1A645161B5540C7
AE6B005ADED9CBFAF063088A70E39565936AFF3305B8C44F8A6F5096D1C76429
82C9331E194AA7D562961C58EDC7143E831CBE093662AE36C8602AFF663A2527
1C26B26364313DFB5A71C7467BF6A4B62FB86D15371E10C8D3E7FA24FA88A8C3
C7254F72C88CC123EC3CA842031F192EFEA153B4FDD59C6A38EA1FBEC5CB5629
EB30BAAED9ADF6D9BB87BB4AF923B1FF50E8C7AB6F9AFCD1E7840DB7062C37AB
564483BFDDD6BBCE5E348D773FF364115A188DFCF262B44B52B3D95E31798EE4
567DA73391C1EF2FAB94D7537249652E0544C4E1D61ECA30E4CCBE636958EEF5
D83E343FAC4FB421CDBD90F75D0E639BEE8BDD1D3B009ABA03133FFDF8652307
EE21F1D7460EABA49F377CC9E34F7C9410BC68F6A9870331218203238CAEEEE4
1E251E46B7737249EACBB58037781032EA8CCFCE6FBCBB6F3977A6BE90E3311B
40AF3DC1C28B3F6927D83A5FEA2BDEE91C28028677F365F010C3A7748FE67C01
33E150796EC187C27D23DB2E6BF36D9BB5BA29FCEA41026D83DB4617652E963F
C2DB45EF9B7C2F38C7DF46DAE885DB4E831F7FFB685B941FC45A89BC83F4CB37
97F49FD8C2175C46FAC59B67F3D5F3B877701969F492F9016CCE40F20C31CE1F
8233A74FC902FD46AFED3E11BE73BBD9953A3857F47774418739DC0DDF9232A5
ED48AAAA79BEC79136A3B74FEA81284486226C96D388409FCF40CB9F1B1FF61C
E78D93E8D2A204AAA7EA3FE3702156A7438273F2BF8675E2DB9000CA0810EC47
00E1C4BAA9C6636E48D6359C811857733416861B945F549D0CB887FB117A926A
E2F7338BC8082DACF3D9AA8CF5F9B93969D53C84665B3528BF0FCAF32F732A9C
2D2FC4C13D46927292EFFC9C8FFA9B0C8408C9776E6E6611EF783C7729DA2214
77469AC58892A730EC0F76A03FD081FE651D0871466C0EE8E21CE3188CE5FB04
0B0C683456627FA8046E541B9D5A4899B45B4342B1A43D082C953681838FE2F3
1B073E630E25058B45EF4862E9686EB3A3DA378996DF40C30426DCDFCC7871B5
01D514C6E3AF515B77B87669F6BFBB9CFA6C906561B6975A5D42F7B611287FF3
DA68D7FD0168A2B4806D36AD3AE8E6B719863ADFD526CDDEECD298D3968ACF60
2C66DF444AD2719D9124DCEF47E56D16508DEB230B0A1C25B9311E5D3C559F04
5B6F6515D763DF0989520E7F58D1373FCDA8CF4DF21E7522D8240242456E35BD
496C048907728011DC4E0CC70AB61BEE4DE8CD4FA5FE2AB4ED3C7C2578F84862
8BC7C221552A60FC42FB3E6561CC521315B70B4AAEB81F7B3350DC01A00976C8
75C62431700DDB03037A2AA3423468A7BAA92E5F6D24C54594BF6F568E6BF902
01ABC358589D0E9B98EB056C576C89A9D4AD12DAFE20B4FD516847CDD0E4D821
E0D348ED8FDBA8C93E009600C11E961D1DFD729C8E6E178D54B861CACAAA7BAE
7819E0B3D01EC1F44AE00CD3525830DC05DC2638404C91B4DA34159330F23E80
013F8FC2E8837EF4613FFAD3FAD107FDE8C37EF4A7F563C13EB7A5943FBA02E1
F85FF161B13CBDB18DF389C6465CF93C188B7D357147DE48EAEABEF3268DD670
DA6A43CB816DD159D5837319FE7A4CEDCA06EFB8493EBB86D2EC518A6952E726
C3522B6D8A76E9FA82C0F0C150F7C3171B3502F08325B7762986861B521D6268
77AA836994D2E1FAF0D4BBE1F373F3D31C9E1C10D1EB55DE9FE874ABD9AE3868
C73C9EB214B94E792591EA96C093A301B6C166CA636B68612DB010E6392E2D0E
C66B0D0C42C5EF4F07EA162D0D648F17CB4C440BDA14CCDA591A02B411C661D8
8716C277D13022732B4DD6C9BFB0679471F7C2E248A7B8CA563F8A2AA303C212
B6A81CD0113FD63A8A0921314E5F4A7CACA3967204BCA395AAE9FD91945B5C8C
F5C59BCDE44B63D512EFF607DE1D74D10F7AE7CE26A4DFB18E4DB6385EAF3F50
4FE8DCC45B67F712EF9CD24DD5331A35F14A68943415ED3BB34CC00BD4B817D8
AE40DFE3325D53EDEE1F5CB414A3BE112CABFB12CD582D4BFBBE113EB644AD2C
340CACED78996E5BD11159EBAA2CAB478C4A17712EC4C8540A3F2045726A15D5
382D3AF3DB46D7B8726ED622298A0DD3245A46C7845017D17E125CAFBBCDE90F
1695A6195C93C2549F5D97EAB74D78C00A8508B13399281D0B9582061BB5C285
9EEF7EF58B7FFEB5A88FF47BE411EC3907A3B5012453DF152ED63649033A7C85
E5CFE137676C20D967B633572BAD6BB5C0442E5A04104B10C3ADC2D43C823D14
91743EA54D0A21A8302DEEBB265FF2711972D77A70759BEC3E1D4286693AEF3C
CCE15A28F78862B034DACFD41937CA87C8982624AA373B78BBD0AE69EECB84D2
EEA7678706C658E3B316A4DC48DEA0C074B409FEF91BCDCDC6F76AD7B8950D5E
D2A1B381D9D2D0B85EDCD630330786E0D3BBE8141919699E2A71DC8C88321F48
82960047CFBB247C3E51D039F62A64FAC901555F8699C7F106996A35D1EBB536
9C8BE7B934FCD62F34E3111833D30FE2761FAF0A3944B4B4ADD8A7A4B62C1BE2
204895B7F61E11780D92CEEDA3AA894F3AAAADB059199132E46F69B67B8367C1
319832662FCF2A5DBBE0BCABB99F893352A39A839111AA9271B00857E16BC9FB
6B73C1F5638E6A91051CD59D55E3D682E197AD51056223C55B65AA1871D9B562
29C3E4B1D1E125F38416F9FD365F94B4E3C574B5DA6C803AED59423190987698
6D06FA86D77E3959CB29775A8AA56944B34BABEBB7CE90D035DEC64E97F91E2C
09F2022EFDFA83A8702889BB2CDE9A84313E1F81A6CF3481C27B6B0A770C7678
57E6DA141CE8D5038B92A6BB350CA12F3679A7C1B75802A1CD693D0C6E509A77
11E1DD356002F0686122E0C30DB9D974DADC0207FAE3CB37E072F6183602C6D2
974A7D4FA90EB4903F046986B329567864508982FFA5ADDD151B9380E73732B9
D6DBA877787C0D6D69401A59D6A2815C26D0323C9676224FC1C4136D3335F912
D363CC7198546B3A1D6E050E6085E9A8F695BA1E9C52101F72E429E5D56BD514
F705A660583853E6838908B5C0AC907EC57D1AACAC26181E4895558B3FE2A1AE
B6CF8133313C9872D9E8BC33A7743247436DD46840DA479C2DBA84895850F163
2A2562FE64BB8577109530A8B8EEAFA357B8EB57EEDF090019959BCE8F22499E
4FD8B425926413D1624FA6275CFE66EF4A6774BFE252F40FE58A6CA0F5227337
A19F7C91903A93FE1752AED4401E0FCEBB41BC7D2771EBB9C853F15152791749
CEFBF4CF0CEDACE26981662B4DC93945B08D9042F789E27CE61F2DB1DECBF20D
EE3612A8C29F6DB49F911472C9474AE1A05B7FCAD26E410D8CE6B36C9B8C7B80
131DC21F4E20B69AF75702F3304E0D1C2E556C4075545D5E66D41F6FF4F0A561
454C0532954CDC14277DFC529E5E2D72666C57664EA5D0CFA4D7CC7899B37DCB
F49F7679F9CE55335872434B24DAC5A6F03B8228BC67322F6890B7389BA0156D
DAB515704EC8D2412FA91D8F2599A4811FB9A1CF9235EEA7AC2F7E247710C6DF
1C216703CFD4CC06860B61F6742366EAF40E43C71BE75C2C2864F92556F6B7C2
ADA2DD47A35992714374DCF189EDC803F2718FD36D0A2F82516781D204A2A083
92621BC3CC76BA17B2267EC2C93D63D2B06982FFACB08564BD32D09921E2E808
3D36B3B34499544AA968C90AC4A9DC7B881D044A462891BCB5E04AEA994C7D39
55EE92B1D89CC42AE339AD401303FF2281A6C3437180D3572944837ADE50AE4F
807EF006138C264CBA7DADDF193632A3C27B7A79E68FA9AD9BEA83D3E7617DAB
DCD9EDC3DDD9D2004E68A78971CAAC3D90783B70136724D3B113E74572AAB8ED
09E92CDBA9E921352BE5595AA18B31B32351463324DA654372452292CA467FD2
20F8DE7176BE8CB29A42054C703E45FC126E92F1195CF36C3ED55BEFD0BCBA7B
F55FAB2C8BF4F3A50000
}
view quick-plot [
400x400
line [1 2 4 8 16 32 64 128 256]
]
A complete tutorial about all of q-plot's features is available by downloading and running the "ez-plot" tutorial from rebol.org:
write %ez-plot.r read http://www.rebol.org/library/scripts/ez-plot.r
do %ez-plot.r
7.3 Drawing Charts Using Raw GUI Code
It should be noted that creating bar charts using native REBOL GUI elements is as simple as drawing box widgets, each sized to the numerical value of items in a list:
REBOL [title: "Simplest Bar Chart Maker"]
data: [12 3 9 38 1 23 18]
gui: copy [backdrop white]
foreach val data [append gui compose [box blue (as-pair (val * 10) 40)]]
view layout gui
The code below adds a number of features such as text labels, randomly colored bars, and a 3D look using buttons instead of box widgets:
REBOL [title: "Simple Bar Chart Maker"]
data: [12 3 9 38 1 23 18]
labels: [Jan Feb Mar Apr May Jun Jul]
gui: copy [backdrop white across]
repeat i length? data [
append gui compose [
text bold 30 (form labels/:i)
button random white (as-pair (data/:i * 12) 40) (mold data/:i)
return
]
]
view layout gui
This example adds variables for auto scaling and sizing, a gradient and colored grid background pattern, and vertical bar layout:
REBOL [title: "Simple Bar Chart Maker"]
data: [12 3 9 38 1 23 18]
labels: ["Jan" "Feb" "Mar" "Apr" "May" "Jun" "Jul"]
height: 11
width: 50
gui: copy [
backdrop effect [
gradient 1x1 180.255.255 255.255.100 grid 10x10 220.220.189
]
across
]
foreach val reverse data [
append gui compose [
button random white (as-pair width (val * height))
]
]
chart: to-image layout gui
gui2: [
backdrop white
style txt text bold (width)
tabs 20
across image (chart) effect [rotate 180] return tab
]
foreach label labels [append gui2 compose [txt (label)]]
view center-face layout gui2
7.4 Creating 3D Graphs With r3D
A tutorial about the 3D library "r3D" by Andrew Hoadley is covered at http://re-bol.com/rebol.html#section-9.6. A nice business use example of r3D is available by running this code:
do http://www.rebol.net/demos/BF02D682713522AA/histogram.r
The following example demonstrates how to edit code in histogram.r to create 3D graphs using a block of your own revelent data. This can provide a nice flourish for displaying data graphs in presentation settings. You can use the "asdfghqwerty" keys to adjust the camera position of the 3d view. Simply load your data into the "graph-data" block:
REBOL [title: "3D Graph"]
; Here is where you put your block of data to be graphed.
; The first column contains labels for each value.
; The second column contains the actual values to be graphed.
graph-data: [
["Jan" 11.0]
["Feb" 22.0]
["Mar" 25.0]
["Apr" 55.0]
["May" 35.0]
["Jun" 75.0]
["Jul" 20.0]
["Aug" 33.0]
["Sep" 21.0]
["Oct" 55.0]
["Nov" 65.0]
["Dec" 45.0]
]
; This compressed code is the r3D module. You never need to touch it:
do to-string decompress 64#{
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
Rf8G28Udt8glAAA=
}
; This compressed code is some initial preparation to enable plotting.
; You never need to touch it either:
do decompress #{
789CED564B6F9B4010BEF7574C4F6912110763471187F6901E7388A2283D581C
D6B0B669318B96C5C1F9F59D7DC00EC458E9BDC896601EDFCC7C33B3000090B2
3D97EC45B2B26E6308A2DB9BDB2F30941F8DFC84E23D867039943F0AF187298D
343FA540A853620DD407C8F2BA2AD8F191AD791183920DA7E25756349C885351
8846F2CC98D7560146F3266491C568501D619518D196ABE0C138C4B069CA1456
D6D4E033C58283468755C6D37CCF8AAF49AF9D152265056468606409AC7A5DBE
A1CEDFE10E2B4164C955234B98234391FE4132E9B11C7A20154BFDFBB44768CC
6F90C3332E8BA18B0D323FEB120D5C969FF0980F3C4CDD37E1D94AC281073E8C
CA208AC80E88D53515C2F0AE8FB41FA73A6FAE5A31696673D10F9BB97899A174
242C9BBD491167AAE0E556ED7EC056B26A17E8ECBD599B36B216384F169C286A
C5AB18BE1974089C1E2E61064A046EC27C14EFB866B2C659E3DAD780C01556BF
44887B64EAD21B6E84E42CDD6100258F2439478537D497E73CB60EB3F947230C
1DEC45C60BC7A08CB260BF68F70B73A7F4CE17487A57B46E8AF96BADC9B94FDE
DF90665F414419EE228AF56F9E2A8CC6B326C5ED83B459739BC72823B2C01437
1962B2AA42CEED1478501F696CDFB7B02BEBDAF1EECD88873DB72C3795A87395
8BB203065283313394B5F4E1481FDEC7A94C5F236077CEBAA7C21EAE83A77FC0
5EF58D0CF19F90BA9FA4D0856189B14942D7CC655D69D9819BB38D34F41969E7
F245E6ACDC167A71A411B84ED8E408A4DEB836BA230079797820F4E22306C351
5838676F69E8A3035A79587473B108F2064AA1466F0B2CDCCD8A1C668E8A0B94
C3DB2EC7694F289393FB3EBD8DA32D2B74EC1DCFB73B455FB824D3874F66D9CF
844996EC46B7E0C9680610FB2779BBD233B3E395B7EA976ED7931B6DB29674EB
491127E6AC4E25E72542F41D3A44B665268099876E77269C51DDE2D702BE6DDA
F0FE165792D541C572E9D5B390DCCFE1038E23ED798A34B8D0B9D0708EB7104E
9434ECDADDB86B533CBFDA0F8AFF3C0F78C61760AD645E6EFDAB283943E7D401
7DF27CB64BA2BA3CC817C387E3A9FB40F80B7F5D1FA3080B0000
}
; The user definable GUI layout goes here. If you want to use other
; key controls, or widgets such as sliders to control the view angle
; and size of the 3D view, put that code here. The "cameraTrans_" and
; "cameraLookat_" variables make all the adjustments:
out: layout [
origin 1x5
at 0x0 scrn: box 400x360 black effect [draw RenderTriangles]
across
text "" #"a" [cameraTransx: (cameraTransx + 10) update show scrn]
text "" #"s" [cameraTransx: (cameraTransx - 10) update show scrn]
text "" #"d" [cameraTransy: (cameraTransy + 10) update show scrn]
text "" #"f" [cameraTransy: (cameraTransy - 10) update show scrn]
text "" #"g" [cameraTransz: (cameraTransz + 10) update show scrn]
text "" #"h" [cameraTransz: (cameraTransz - 10) update show scrn]
text "" #"q" [cameraLookatx: (cameraLookatx + 10) update show scrn]
text "" #"w" [cameraLookatx: (cameraLookatx - 10) update show scrn]
text "" #"e" [cameraLookaty: (cameraLookaty + 10) update show scrn]
text "" #"r" [cameraLookaty: (cameraLookaty - 10) update show scrn]
text "" #"t" [cameraLookatz: (cameraLookatz + 10) update show scrn]
text "" #"y" [cameraLookatz: (cameraLookatz - 10) update show scrn]
]
; Try changing the variables below to affect whether the lables and values
; are shown at the bottom of each 3D bar in the graph, and whether or not
; the data is shown in color or black and white:
DisplayLabel: true
DisplayValue: true
ColouredLabels: false
; The following 2 lines update and show the display. Don't change them:
update
view out
7.5 Using the Google Chart API
Google.com provides a powerful and nice looking chart generator which is freely accessible by anyone, at http://https://developers.google.com/chart/. The syntax for preparing chart data and displaying results is dramatically simplified using the REBOL script at http://reb4.me/r/google-charts.r/, by Chris Ross Gill. To use it, just include the line "do http://reb4.me/r/google-charts.r" in your script, or paste the following code:
REBOL[title: "Google Chart API"]
#{
789C8D56EB6FDB3610FF2CFD1517015BBAAEB6E2B4EB5A0D4390BED26ED99A65
6DF710848092684B0D256A2495D85DF7BFEFEE28594ED002FD6253C77BFCEECD
F3E74F5E9F421A066F6AA76402D189D62B25E169258C83E3B3575118BC93C6D6
BA4DE060BE982FC2E0B8779536C8FBB432B575BAABA481736DEDECA4560A055E
EA065555CE75491C1BBA58E1C5BCD04DDC89958CBD8D0BB6612F445B5E9C138C
3078261C0A3E9E1DF7ABD9E1C1C1A33078BEEE3432259016C49D85C18B9A707E
B5621D33A6DAB90983B3DE74DAE2D5BF27B29506355910F0F6FC149C065114D2
5A7095845D072D79F85F189CD6B91166935020825379255502FBB95CD52D6A42
D299126EA94D8354A11412DE6C3AB4942EFBB670181A6874D9A352A7B582B216
4A16043578A61B5163E0D281663928A0CA02AE654E1CA775215B42BD5F14B37C
33B3220CB2300B43762C81DE4A0265B476E0D0A8851FC04AE7EA7665C3A0111D
7158902D62D61D5E16DA481871210BC19ED9BAE9101E9F0BADB4F14785D99B4E
D61F4BE104EA21635049D561F2C36C80B04D2AA39B8BAEB6739F084E2E538FC2
3060A43E9AAA6E2544AA88804EF17A831FEB4D04B613E672B8B45833412E0C44
F9958D004F311658FD41B74E282456CCD0D5C8DBDD8F000FF11233825F44A720
442EA2B88501452361FF21ED8CC6D4A7B9D2C5E55E861665B1FD228F02664830
64AD936B07FC89642F2DCCCA8EECA0F3F7983E1443E686CA2CD6ADDAB0830131
26D0884B097758C5374024BA2A35E4755BC21DB2BD25671E2AE21E915A696ACC
622ADACD6C842B7C65316DCCE780DBB313F06E13975276E02978755D617B40DA
52B9885A1D0D372C36C955A2C536ECA8C507D38335FF152F60E171069514E556
3BA11E2A6D447E85096288947282276B47E360EBC911300BFE64A991655F48FF
9191B29DE21C4ADDE9D9C3053A560E98F984A3E6F8C9D367CF5F9CBC7CF5D3CF
A7BFFCFAFAECB7F3DFDFBC7DF7C79F7FFD2DF2A294CB5555BFBF544DABBB7F8C
75FDD5F57AF3E1607178FFC1770FBF7FF438425B01ABBE81BBED9B5C9A3D6875
2B87D006F5923F07D846BADEB4105D44148C006994E93534750B0B6639207A57
17971E3452BF05A37B4C3AFA7197583892DBB24A0BE18A2A036EB3A936252643
386CCC1BC124DD46BED7688DAA9C6512F6632AA3A9A947DF7C87A7AEC7B0A2EE
186D2050DD3B5688FFBE7220A2EE31B293D84AE48A92EDCA554730C8736D779D
44575006BB127DE41F2A2C1C75B34AAE3D6F9C8C6E62BFEB6B690A81B944A11D
8434614680ECF5279DA709B207D6199C6F43008C6CF495849D3078F16D1E7D98
F876D264AF6B0C3336C752F4CA8FCEA3F85A9BD2E795248252167523D41EA699
1216E3B0E08CCE179CECC00710A7FC3436C91CDF71C9403AF3AC1E4270CBE864
2918421AE5AB28031E93F851E019D7099E842F2FD4A0555D22C1E2D5CA88B296
2D712A12A3A8D0024869607AFE2C1D0165B70A8207FA186F9AAE5F1872DC2454
C7D36ADB23CD313A5681EDF3CFCA85C117248BCF9F4CD760D533EFDAB9E51675
C0E8D5CD0E9AAA9CE31EF344F76B221ED65FAC442E7DB9F4ADA2F7001AC142F5
4AA761EA155B7CC4C0A09D217BB67186D15736B636D7BB1FB1DE54CAFE453689
76F72B2B8EEE4590EC0C3E4E1FE34C27561FF05BFCBCAE239F7A766547827971
6E471FFD3DBB3FA0703750ECAAFE7853F5BD41D6F4F273603E29914DBB97A370
254DCE4FB1A550145F99F7AB64B761872531F0F1A6CE253797DF0D7E4CDD4DFC
88924DBD1D1DFBB843615826E368BA8B2DF17544DB15A21FBDAB30AA71FE594B
ED4A8BB3FE801FF70F0FD6870F68703B7EC6EDD333241C7337A00EC784A75938
F46FE2A71DB615471FFF8591C881EF153B9AF04B4B497CCC10B2013DB6BCA395
489B9EDE81C3538EDF580C974ED9C46C81904EDF250C111BCB6BDB0C636D7339
EE9039FF638D5279C63C6938BD141248475C0886089C7986E867D50D8FB391BB
D03B8FC4A115C6B727B34EC569B726D424E35B702BE219BD690CE55662B96385
2E9883C2BCE5C8AB1D16BA19EAD00F167E2E73D9414B81F0A9C077F5FFE11722
94EF0C0000
}
That code enables a simple REBOL dialect to create and display charts:
REBOL [title: "Google Chart Introduction"]
do http://reb4.me/r/google-charts.r
probe chart [
title: "Chart Example"
type: 'line
size: 350x150
labels: ["Red" "Green" "Blue"]
data: [50 40 10]
colors: reduce [red green blue]
area: [color solid 244.244.240]
]
halt
The block of chart properties is easy to understand. Edit it to include your ownd data values, title, chart type, size, labels, colors, etc. To create the chart above using the native Google API, you'd need to learn how write the following code (paste this into any web browser, all as 1 line, and you'll see the chart appear):
http://chart.apis.google.com/chart?cht=p3&chs=350x150&
chd=t:50,40,10&chtt=Chart%20Example&chco=ff0000,00ff00,
0000ff&chl=Red|Green|Blue&chf=bg,s,f4f4f0
You can view the results of the REBOL "chart" function using the "browse" function:
REBOL [title: "Google Chart Introduction"]
do http://reb4.me/r/google-charts.r
browse chart [
title: "Chart Example"
type: 'pie
size: 350x150
labels: ["Red" "Green" "Blue"]
data: [50 40 10]
colors: reduce [red green blue]
area: [color solid 244.244.240]
]
Or view it directly in a GUI by loading the results into an "image" widget:
REBOL [title: "Google Chart Introduction"]
do http://reb4.me/r/google-charts.r
my-chart: chart [
title: "Chart Example"
type: 'line
size: 350x150
labels: ["Red" "Green" "Blue"]
data: [50 40 10]
colors: reduce [red green blue]
area: [color solid 244.244.240]
]
view layout [image load my-chart]
Changing the properties is easy:
REBOL [title: "Another Google Chart"]
do http://reb4.me/r/google-charts.r
my-chart: chart [
title: "Chart 2"
type: 'pie
size: 500x400
labels: ["John" "Paul" "Sue"]
data: [35 55 10]
colors: reduce [orange purple pink]
area: [color solid 225.225.245]
]
view layout [image load my-chart]
You can include REBOL code directly in the chart block to perform calculations, format data, etc:
REBOL [title: "Example by Chris Ross Gill"]
do http://reb4.me/r/google-charts.r
clipdata: {Adsense Revenue^-300
Sponsors^-500
Gifts^-50
Others^-58}
browse chart [
title: "Revenue"
size: 650x300
type: 'pie
; extract raw data
data: parse/all clipdata "^/^-"
labels: extract data 2
data: extract/index data 2 2
; format data and labels
sum: 0
forall data [sum: sum + data/1: to-integer data/1]
forall data [change data round 100 * data/1 / sum]
forall labels [
labels/1: rejoin [labels/1 " " data/(index? labels) "%"]
]
]
More information about the REBOL Google Chart API is available at http://www.ross-gill.com/page/Google_Charts_and_REBOL.
7.5.1 Working with Other Web Site APIs
Chris has created a number of other web APIs that make easy work of interacting with popular sites such as Twitter, Facebook, and Etsy. A tutorial about using REBOL with the Etsy API is available here. You can read more about using REBOL with the Twitter API, and simplifying the use of Rest, OAuth, and other protocols at http://www.ross-gill.com/page/REBOL
7.6 Using the "Nano-Sheets" Spreadsheet App
In the introductory demos section of this text, you saw a small spreadsheet app titled "Rebocalc". This application idea was extended in an article by Steve Irvin and Steve Shireman at http://www.devx.com/opensource/Article/27454. The resulting "Nano-Sheets" app is actually useful in production situations and can be improved using simple REBOL code. Saving, loading, printing, and other features are already available in the Nano-Sheets app. You can create your own functions that use any of the features of the REBOL language to process cell data (math, graphics, parse, native dialogs and GUI interfaces, Internet connectivity, file and network protocols to connect with data sources, etc.). This program and the free REBOL interpreter are so small that they can both be sent easily, even by email, to others who may need to use it. No large or expensive office software installations are required to run spreadsheets created with this tool.
Here's a slightly modified version, with scroll bars, for grids of any size. Column sum and row sum functions are also added the included abilities:
REBOL [Title: "Nano-Sheets Spreadsheet"]
cell-size: 125x20
sheet-size: 10x50
window-size: 800x450
scalar-types: [
integer! | decimal! | money! | time! | date! | tuple! | pair!
]
protect 'scalar-types
sheet: lay: current-file: sheet-code: none
cells: copy []
sheet-buttons: copy []
use [
buttons== cell== sheet-code==
id val text action face style
] [
id: val: text: action: face: none
style: 'btn
buttons==: [
'buttons into [
any [
set val word! (id: val) 2 [
set val string! (text: val)
| set val block! (action: val)
] (
repend sheet-buttons [id text action]
append lay/pane face: make-face/size/offset style
cell-size
cells/:id/offset
if 'button = style [face/edge/size: 1x1]
if not none? face [
face/text: text
face/action: action
face/style: style
]
)
]
]
]
cell==: [
set id word! into [
opt 'formula set val [block! | path!](cells/:id/formula: :val)
| opt 'value set val [string! | scalar-types] (
set cells/:id/var cells/:id/text: val
)
]
]
sheet-code==: ['do set sheet-code block! (do sheet-code)]
sheet==: [
(sheet-code: none clear sheet-buttons)
opt sheet-code==
any [buttons== | cell==]
]
]
if link? [
hilight-all: func [face] [
either empty? face/text [unlight-text] [
highlight-start: head face/text
highlight-end: tail face/text
]
]
]
clear-cell: func [cell] [set cell/var cell/text: cell/formula: none]
clear-sheet: does [
foreach [id cell] cells [clear-cell cell]
clear next find lay/pane last cells
show lay
]
compute: does [
unfocus
foreach [id cell] cells [
if cell/formula [
if error? try [cell/text: do cell/formula] [
cell/text: "ERROR!"
]
set cell/var cell/text
show cell
]
]
]
cur-cell: does [either in-cell? [system/view/focal-face] [none]]
empty-cell?: func [cell] [
all [
none? cell/formula
any [
none? cell/text
all [string? cell/text empty? cell/text]
]
]
]
enter: func [face /local data] [
if empty? face/text [exit]
set face/var face/text
data: either #"=" = face/text/1 [next face/text][face/text]
if error? try [data: load data] [exit]
if scalar? :data [face/formula: none set face/var data exit]
face/formula: either formula? face/text [compose [(:data)]] [none]
]
event-func: func [face event /local f] [
if all ['key = event/type in-cell?] [
switch event/key [
F2 [if in-cell? [show-formula system/view/focal-face]]
up [move up]
down [move down]
]
]
event
]
formula?: func [text] [#"=" = text/1]
in-cell?: has [f] [all [f: system/view/focal-face 'cell = f/style]]
load-sheet: func [file [file! url!]] [
clear-sheet
parse load/all file sheet==
current-file: file
show lay
compute
]
move: func ['way /local pos] [
pos: find cells cur-cell
cell: pick switch way [
up [enter cur-cell skip pos negate sheet-size/x * 2]
down [enter cur-cell skip pos sheet-size/x * 2]
] 1
if not object? cell [cell: none] ; if 'A1 = cell [cell: cells/A1]
if cell [focus cell]
]
new-sheet: does [
clear-sheet
current-file: none
show lay
focus second cells
]
use [not-vals] [
not-vals: reduce [none ""]
no-val?: func [cell-val] [find not-vals cell-val]
]
open-sheet: func [/with file [file! url!]] [
if not file [
if %none = file: to-file request-file [exit]
]
load-sheet file
focus second cells
]
save-sheet: func [/as /local file buffer] [
if any [not file: current-file as] [
if %none = file: to-file request-file/save [exit]
]
if all [file <> current-file exists? file] [
if not confirm join file { already exists.
Do you want to write over it?} [exit]
]
buffer: copy []
if sheet-code [repend buffer ['do sheet-code]]
if not empty? sheet-buttons [repend buffer ['buttons sheet-buttons]]
foreach [id cell] cells [
if not empty-cell? cell [
repend buffer [
cell/var reduce [any [cell/formula get cell/var]]
]
]
]
save file buffer
current-file: file
]
scalar?: func [val] [find scalar-types type?/word :val]
set-cell: func [id val /local cell] [
cell: select cells id
cell/text: form val enter cell show cell
compute
]
show-formula: func [face] [
if face/formula [
face/text: join "=" mold/only face/formula
focus face
]
]
ctx-html-export: context [
out-buff: make string! 10'000
html-template: {
<html>
<!--Page generated by REBOCalc-->
<head>
<title>$title</title>
<style type="text/css">
html, body, p, td, li {font-family: arial, sans-serif,
helvetica; font-size: 10pt;}
table, tr {border-collapse: collapse;}
th, td {font-size: 12px; border: 1px solid #C0C0C0;
padding: 0.5em 0.5em 0;}
th {background: #8E806E; font-weight: bold;
text-align: center; color: #404040;}
</style>
</head>
<body bgcolor="white">
$table
</body></html>
}
emit: func [data] [repend out-buff [reduce data newline]]
set 'emit-html func [/to file /local val] [
clear out-buff
emit <table>
repeat row 1 + sheet-size/y [
emit [<tr><th> either 1 = row [""] [form row - 1]</th>]
repeat col sheet-size/x [
emit either 1 = row [[<th> col-lbl col </th>]] [
[
<td width="110"> any [
get/any mk-var col row - 1 ""
]
</td>
]
]
]
emit </tr>
]
emit </table>
out-buff: replace copy html-template "$table" out-buff
write %rebolcalc-out.html out-buff browse %rebolcalc-out.html
]
]
avg: average: func ["Arithmetitc mean" block [any-block!]] [
remove-each val block [no-val? val]
either empty? block [0] [divide sum block length? block]
]
gcd: func ["Greatest common denominator" m [integer!] n [integer!]] [
either (m // n) = 0 [n] [gcd n (m // n)] ; Euclid's algorithm
]
geo-mean: func ["Geometric mean" block [any-block!]] [
either empty? block [0] [(product block) ** (1 / length? block)]
]
median: func [
"Returns the number in the middle of a set of numbers sorted by value"
block [any-block!] /local len mid
] [
block: sort copy block
len: length? block
mid: to integer! len / 2
either odd? len [
pick block add 1 mid
][
(block/:mid) + (pick block add 1 mid) / 2
]
]
mode: func [
"Returns the most frequently occurring value in the block"
block [any-block!]
/local last-item result high-count count
][
block: sort copy block
result: last-item: first block
count: high-count: 1
foreach item next block [
either item = last-item [count: count + 1] [
if count > high-count [
high-count: count
result: last-item
]
last-item: item
count: 1
]
]
if count > high-count [result: last-item]
result
]
product: func [
"Multiplies all the values in the block"
block [any-block!] /local result
][
remove-each val block [no-val? val]
result: 1
foreach value block [result: result * value]
result
]
sum: func [
"Adds all the values in the block"
block [any-block!] /local result
][
remove-each val block [no-val? val]
result: 0
foreach value block [result: result + value]
result
]
sum-rows: func [
"Adds a range of row values"
row start end
][
total: 0
for i start end 1 [
do rejoin ["total: total + " row i]
]
total
]
; =sum-rows "b" 1 4
sum-cols: func [
"Adds a range of column values"
col start end
][
total: 0
for i start end 1 [
do rejoin ["total: total + " i col]
]
total
]
; =sum-cols 2 #"b" #"e"
col-lbl: func [col] [form to char! 64 + col]
cell-name: func [col row] [join col-lbl col row]
mk-cell-size: func [col] [
either any [none? col-widths col > length? col-widths] [cell-size] [
as-pair col-widths/:col cell-size/y
]
]
mk-var: func [col row] [to lit-word! cell-name col row]
sheet: [
origin 5x5 space 1x1 across
style cell field cell-size edge none with [formula: none] [
enter face compute face/para/scroll: 0x0
]
style label text cell-size white rebolor bold center
style menu button 60x20 silver edge [size: 0x0] shadow off with [
font/colors: [0.0.0 0.0.128]
]
menu "New" #"^n" [new-sheet] menu "Open" #"^o" [open-sheet]
menu "Save" #"^s" [save-sheet] menu "Save As" #"^a" [save-sheet/as]
menu "HTML" #"^t" [emit-html]
text 10 "" text "(Press [F2] to edit cell formulas)"
return
]
repeat row 1 + sheet-size/y [ ; +1 accounts for header row
repend sheet ['label (as-pair 30 cell-size/y) either 1 = row [
""
][
form row - 1
]
]
repeat col sheet-size/x [
append sheet compose/deep either 1 = row [[label (col-lbl col)]] [
[cell with [var: (mk-var col row - 1)]]
]
]
append sheet 'return
]
lay: layout sheet
foreach face lay/pane [
if 'cell = face/style [
repend cells [face/var face]
set face/var none
]
]
focus second cells
insert-event-func :event-func
gui: [
across
g: box window-size with [pane: lay pane/offset: 0x0]
scroller as-pair 16 window-size/y [
g/pane/offset/y: g/size/y - g/pane/size/y * value show g
]
return
scroller as-pair window-size/x 16 [
g/pane/offset/x: g/size/x - g/pane/size/x * value show g
]
]
view layout gui
Try saving the text below to a text file, then load it as a spreadsheet in Nano-Sheets to see how some useful features are implemented. This is the "native" internal format that Nano-Sheets uses to save data. You can also use the modified version above to save and load flat and blocked tables of REBOL data, as well as standard CSV files that are compatible with traditional spreadsheet applications:
do [
right-now: does [now/time/precise]
circle-area: func [diameter] [diameter / 2 ** 2 * pi]
]
buttons [
A10 "Random-test" [
set-cell 'a11 circle-area random 10 1E-2
]
C10 "test-2" [set-cell 'c11 right-now]
A5 "Check" [print "check"]
F16 "Done" [quit]
]
A1 ["test"]
C1 [[now/date]]
D1 [3]
E1 [$200.00]
F1 [1x2]
E2 [[d1 * e1]]
A11 [19.63]
C11 [0:46:00.171]
C12 [[c11 + 1]]
As you learn more about REBOL coding, you'll be able to easily modify and extend the capabilities of the Nano-Sheets program in ways that would be very difficult or impossible to accomplish using other spreadsheet applications.
8. Using REBOL to Create Presentations
8.1 REBOL as Presentation Software
REBOL GUI features enable simple methods for laying out images and text with a variety of fonts, styles, colors, gradients, and the ability to create animations, add sounds, and make use of other elements that are useful in creating captivating audio/visual presentations. REBOL's graphic capabilities provide advanced methods for creating images from code, which are difficult to design even with complex graphic manipulation software (a topic for much later in this text). In most simple cases, applications such as PowerPoint and Photoshop can be easily replaced by straightforward REBOL code.
8.2 Some Basic Layout Ideas and a Simple Code Framework for Presentations
Slide show presentations are often shown in full screen mode. In REBOL, the size of the screen is stored in the value "system/view/screen-face/size", so the following code creates a full screen white box that closes the GUI when clicked:
REBOL [title: "Empty White Screen"]
view center-face layout [
at 0x0 box system/view/screen-face/size white [unview]
]
Here are a couple of GUI layouts that serve as examples of slides with text and images. Clicking anywhere on the screen advances from one slide to the next:
REBOL [title: "Slide 1"]
view center-face layout [
at 0x0 box system/view/screen-face/size white [unview]
at 20x20 h1 blue "Slide 1"
box black 2000x2
text "This slide takes up the full screen."
text "Adding images is easy:"
image logo.gif
image stop.gif
image info.gif
image exclamation.gif
text "Click anywhere on the screen to close..."
box black 2000x2
]
REBOL [title: "Slide 2"]
view center-face layout [
at 0x0 box system/view/screen-face/size effect [
gradient 1x1 tan brown
] [unview]
at 20x20 h1 blue "Slide 2"
box black 2000x2
text "Gradients and color effects are easy in REBOL:"
box effect [gradient 123.23.56 254.0.12]
box effect [gradient blue gold/2]
text "Click anywhere on the screen to close..."
box black 2000x2
]
REBOL [title: "Slide 3"]
view/options center-face layout [
at 0x0 box 600x400 [unview]
at 20x20
text "This slide is smaller, and as simple as can be"
text "Click anywhere on the screen to close..."
] 'no-title
This program uses q-plot to create graph images of monthly sales and expense numbers, and then displays them in a full screen presentation that can be shown to a group:
REBOL []
do %q-plot.r
save/png %sales.png to-image quick-plot [
600x400
bars [5 3 8 2 10 3 4 9 5 7]
]
save/png %expenses.png to-image quick-plot [
600x400
line [9 2 4 1 7 3 8 14 10 5 6]
]
view center-face layout [
at 0x0 box system/view/screen-face/size white [unview]
at 20x20 h1 blue "February Sales:"
image load %sales.png
]
view center-face layout [
at 0x0 box system/view/screen-face/size white [unview]
at 20x20 h1 blue "February Expenses:"
image load %expenses.png
]
Making a slideshow framework to display consecutive layouts is as simple as creating a block of nested blocks containing the GUI code, and then using a foreach loop to display each layout:
REBOL [title: "Simple Presenter"]
slides: [
[
at 0x0 box system/view/screen-face/size white [unview]
at 20x20 h1 blue "Slide 1"
box black 2000x2
text "This slide takes up the full screen."
text "Adding images is easy:"
image logo.gif
image stop.gif
image info.gif
image exclamation.gif
text "Click anywhere on the screen for next slide..."
box black 2000x2
]
[
at 0x0 box system/view/screen-face/size effect [
gradient 1x1 tan brown
] [unview]
at 20x20 h1 blue "Slide 2"
box black 2000x2
text "Gradients and color effects are easy in REBOL:"
box effect [gradient 123.23.56 254.0.12]
box effect [gradient blue gold/2]
text "Click anywhere on the screen to close..."
box black 2000x2
]
[
at 0x0 box 600x400 [unview]
at 20x20
text "This screen is smaller, and as simple as can be"
text "Click anywhere on the screen to close..."
]
]
foreach slide slides [
view/options center-face layout slide 'no-title
]
You can simplify coding tediously repetitive layout elements, by only putting unique GUI elements in each slide. Widgets and layout code to appear in all slides can be inserted in a "forever" loop. This example separates out the title, the text background box color/effect, and layout code to appear in each unique slide, and adds forward and back controls, bar lines (black boxes), and colors to appear in every slide. In this example, the "compose" function is used to insert data contained within parentheses. To create your own slides, all you need to do is edit the unique code to appear in each individual slide layout (title string and unique GUI code for each slide):
REBOL [title: "Presenter"]
slides: [
"Slide 1 - A Few Basics"
[
text "By default these slides are white and full screen."
text bold "Adding images is easy:"
image logo.gif
image stop.gif
image info.gif
image exclamation.gif
text {
Press the space bar, right arrow key, or left click screen
for the next slide. Press the left arrow key, or right
click screen to go back to previous slide. Press the 'X'
key to quit...
}
]
"Slide 2 - Colors and Gradients"
[
at 0x90 box as-pair system/view/screen-face/size/1 220 effect [
gradient 1x1 tan brown
]
at 20x70 text "Colors and gradient effects are easy in REBOL:"
box effect [gradient 123.23.56 254.0.12]
box effect [gradient blue gold/2]
text {
Left arrow key or right click screen to go back, 'X' key to
Quit...
}
]
"Slide 3 - A Simple Window"
[
text "This slide is as simple as can be."
]
"Slide 4 - Lots of Stylized Text"
[
across
text "Normal"
text "Bold" bold
text "Italic" italic
text "Underline" underline
text "Bold italic underline" bold italic underline
text "Serif style text" font-name font-serif
text "Spaced text" font [space: 5x0]
return
h1 "Heading 1"
h2 "Heading 2"
h3 "Heading 3"
h4 "Heading 4"
tt "Typewriter text"
code "Code text"
below
text "Big" font-size 32
title "Centered title" 200
across
vtext "Normal"
vtext "Bold" bold
vtext "Italic" italic
vtext "Underline" underline
vtext "Bold italic underline" bold italic underline
vtext "Serif style text" font-name font-serif
vtext "Spaced text" font [space: 5x0]
return
vh1 "Video Heading 1"
vh2 "Video Heading 2"
vh3 "Video Heading 3"
vh4 "Video Heading 3"
label "Label"
below
vtext "Big" font-size 32
banner "Banner" 200
]
"Slide 5 - Live Code"
[
h3 "Remember, These Slides Are Live, Fully Functional GUIs!"
box red 500x2
bar: progress
slider 200x16 [bar/data: value show bar]
area "Type here"
drop-down 200 data reduce [now now - 5 now - 10]
across
toggle "Click" "Here" [alert form value]
rotary "Click" "Again" "And Again" [alert form value]
choice "Choose" "Item 1" "Item 2" "Item 3" [alert form value]
radio radio radio
led
arrow
return
]
]
indx: 1
forever [
slide: compose [
size system/view/screen-face/size
backdrop white [
if indx < ((length? slides) / 2) [indx: indx + 1 unview]
] [
if indx > 1 [indx: indx - 1 unview]
]
at 20x20 h1 blue (pick slides (indx * 2 - 1))
box black as-pair (system/view/screen-face/size/1 - 40) 2
(pick slides (indx * 2))
box black as-pair (system/view/screen-face/size/1 - 40) 2
key #"x" [quit]
key #" " [
if indx < ((length? slides) / 2) [indx: indx + 1 unview]
]
key keycode [right] [
if indx < ((length? slides) / 2) [indx: indx + 1 unview]
]
key keycode [left] [
if indx > 1 [indx: indx - 1 unview]
]
]
slide: layout slide
view/options center-face slide 'no-title
]
It's important to realize that because these slides consist of live, running REBOL code, they can contain much more than just static text and images. Any sort of fully functional application of deep complexity can be included. You could, for example, include charts, tables, or even a working spreadsheet that performs live calculations on instantly updated data. Using the few simple tools you've seen so far, you can easily create presentations that are all but impossible using traditional presentation software.
8.3 Using Tab Panels and Menus to Present Information
Though not a traditional slide format, a simple and effective way to present multiple screen layouts is by using a tab panel widget. The following code contains a tab panel, and also a useful menu widget, created by Richard Smolak. The same 5 example slides shown in the previous section are demonstrated here. Menus can be used to pop up small text alerts, requestors, and choices that alter the state of screens in the presentation. Simply copy the compressed tab panel code (and menu code if needed), plus the "insert-event-func" code, then put your slide code inside the "tab-panel data" block. The syntax required to use optional menus should be intuitively understandable by examining the example code provided here. Beyond being a nice way to display pages of presentation data, these two GUI tools are useful for building all types of applications which require significant screen real estate:
REBOL [title: "Tab Panel and Menu Presentation"]
; Tab Panel Widget:
do load decompress #{
789CBD586973E2B816FDCEAF50577FE86428C7989824506F2695349DADB37496
4E4FA09C2A63CBE0C6D8C4368DC9BCF9EFEF5E495E65B2CCAB9AA4005BBAD2DD
8E8EAEF45714AF3CF799AA33338A6948868D47253647CADCF4A9D7238E6951B2
74E309EBE05D6648CD1E999953CABB79CFA312384E44E31E6925ED9668A2F698
0A517CCC441F1577666297179836890365E4FA66B82236B582D93CA4514476F4
8F7F35E8D973D8FF76F8DC69FEB8D56767E387ABBED63D39383B884FC79713EB
E4B8D38F2EEF9ED4A3969BCCFBBF76AE7E9E2E4757ABB079D3D28FADC99F8DDB
6F0FA77BB3FBF1D3D199F7703C0906B3BEDEBCE99EFE6A1F8EBFF6EFC70F27EE
FDF2E2707C39681F5EDF256A337A1ADCEB874FB7CF897DF2E5E2A9D171EFBA4D
4775FC78767D6D1E4D6EA6D6F5D765D4D793E3D9E0AC3B4816E777E7CFA74F17
9FA7AD9B2FC9F27C104C8F0E7FBA17DD4B6B7BFAE3B6D1FC71393BD8BB9AF447
30CF73D3695D7C3B595EDEDE3C58BB5F4EA3CF5DB77564FF79D78CC0BC643038
39F8BAFB5DBB3EBF98B9FDD1FDFC9BAD37BE3E07CBF3D3D5C21B3C75CD8BE6F7
B3B63538BE9C3E9CF8FABDF7BDD554E943AB75A21FDF6996EA6DDF5F7DFEDE3A
38D8D54E0F9AD303F8FBBDF17716F40812DD237AA293ACC90ABC20EC113FF0A9
6833C4AF4311003CCDF058C85DA3F4F3A884D40ECD258065E15B64E8109350A3
200E30000001B61C750ED8F1630561A38E82D0A6E17E4910B4AA0814953A0EB5
004B08872002E0D024A6BECD6CDF7054748428446B6D12E600BE6E94A767ED91
AA6D1A85F98D3A6525EF8B1110225553C6A169BBA00780AE9176A7BDC53F3A69
EBFA565BDFDE6A6FEFBDC72CA31478235D67754B8C27B0038ABB6996023F4E25
E1B1104D9159ADDDDA129F0A0E34AD92F1F5AB5546493A0998F266DC507FCC16
7D012744F502CBF448E43965C844403AD60444CA19B383A55F690259CFE955C0
55789685D594C55464B91E89A8071926A227620F4E602D229E06D7B769B24F1C
F84D65945190B0C1A016463B151DAE039E0D51D6B46237F071587956E335ABD4
BA900BD949B064B3A15CA9CFA805B1F1D2425D9300173B7F279F98AE72BCDF11
6D474DF78499EBE3982012DFCDCA0C76E0C495A12200E868BA684A1282544A61
0593DF902D09409C50D4558FB4DB521777013B5B521F2EB9D4D091675A531908
68433DD5F129EAE84EB5299D9321268B0CE7D4271BC5286C82739EA7D4B4839F
B820BB408C2DD88535A30AB35A3E0B8305C44AEBEC6D693B5DF874C876755CF9
DD581FBF8C97EAE227C7568A1F0063A71AE6F706B166C914FDAEF39720A7831D
8CD2351DD8927F8023E1977F5E89C98B4BAF48ED9C373323855B3D12870BDE52
A01EA4A31EB1CDD864CF0864781D99D31E31C33058463D22A257D9C83826C0D9
8F436C3320313B5B6D0DF6A64E879BB104BD28202C441D64E8D3A5CAB5396E18
C5C4876D17348DA3FC2975C90F84E97C54E686637A00AA5C927FBBBECB20375F
91D94ABCA5AA33C7E4FD2E2F2AF524470F9205EE3C850D2DA6F348CC3FCCC33F
33936C9BCA85EB2386FF7C3B28CF62E4560A0B916786D2BE6B14048B2572FA0A
254C757B2D0DC92320DE7213380ACA66C1A2F08398E4CC372CC0464BA5B84340
FB0B0C6A668AD050988BA79F238B29C2868A2E16373214E15B4EDC98A6120BE4
11C80AF9657A0B1223563C73455C3B210EB32E9ECD59260AD49136959323529B
271BE60087F28255184367F378B52FEC2EAC3DD854C016A9192012CCA8C41DA6
0F21057C71BBE1812D8B0F063E7237A23874FDF107D6802E0D47B05F4E3F90FF
9260F413F8049FC4986C32EEF2279610836C485C042EE0907D3E217CF5C8984F
2FF335C8328D4561F80A166BE4A151CD0F62552A65F3A1AF741FECE3B891444A
0C04E1AF1158F8E8279F41EAAE752255CA025DA733CB0278B05E234ABC4D2182
841D13EA94E5D8038AE0894B9B306A061AC10F194DA2ED25DBDB6FD109168680
1AD3F5186F67CB0E57417D6E11DAD80B4A5E9B2EAF61320E2A170B355E426F8F
540B2B116DDCAF7161B5B014635C8F4D0AC37C8187418F5AA0BAF41578888843
04AB343A6CB1F4C806FE6CCA9E3E2AC5FE9A6E991BDF1F711622B613088F7051
41DDE999515C0EE127B45D9A8E510F9258136F24E6A61B6629D4B228BD3C2791
AA844DE08768EACEDF502CA4A501E7119C2D63CD824CAA34DFDBCA19CE02C1FA
36D83CBF112D81A3729328DA0E948505695828305C4DC81F8443A202A4628899
9B69D9C139282DEEE0A83B86A8BC8848D30A8328AA348A81DB499B4473445C95
B1983EA2ED26DA2ED910956E7B937814F2149A31253B35D067DB51E54099FE3B
FC52EB8DA74511232493093581B2F30305475A9D0AAC364B5250DC626D5B697D
6524AF72CAEF2F42B36A9A844501323CD265B23522AF2FBDEAFB9A24416E27FF
284BFF384788D37D5EABBE3751B5E156FEFF704B50A8B3EEDF499451A60385E8
AD44694B65025FE26B2E205E3EDBD4DFFD08C190724EE2B70FC4618CD324A5D3
A438D463C8F7457FF5CA2E3D6AA64F901026A816CB06E9A28FAB6682EBC89785
5B722F33BE7A7512A45727B083BFE9EA846354A14F0BD34322C16A1B7FE45A75
E2DA9438B51E48875B474D2959EAB20332C26B104715672EDCB91CB0AC7A6355
719D252271E33581023FC4A583949BF42AA2974AD4F5BF78DFCA6A222CCA3476
67E4517F1C4F8AB42BEE71B212B5519C3FBB421348CB2F8AA1469124D3E9544D
45B4AAFC7EB2A24BED6185F6895D3C56EC6681E0D19722F1E2222A0808FD42AE
DCDCCE9B5352AD9F242DF8D39BF1F202AF8C29121C96803E1D2343D7788D3775
6AB266B86033AC6DD8D628F1617D8E44D4AA550C141EFF917995973065A7A088
D1BB50C4D45CE2554CABB36AED18F94E614D8C15A87813FC7A43452753A4D130
FEFE1F50628086731B0000
}
; Menu Widget:
do load decompress #{
789CB51B6B73DAB8F67B7E85BA3B770237430CE4D12EDD6EC6109A92266D499A
B629E3CE1810C6606C6A9BE074BBFFFD9E23C9B66CCB84A477C384C8B2747474
5E3A0FE5EF5118D516D45DB5C8C29C53E20D6774143E23839D6F357B615A3468
119F8E5723CABABED55EE260329C5B646D875342C71665FD8E678EC9F1E1EF7F
DB9FDAEFAFD6F5B76796A7C3CFBBEB9B69F7C68256BB87CFD71DFD161F3EFFF0
8EBE6047FB6CDCFE78D3D5F58BB3F7930FE1D5CE7C0DBD9DF6ECFAF5F93B787D
7CDED775AB77A9EB1F5C0D5EE8C7F07AFC11BEDE2F11ECB103E35F37F7BE7CFF
EADE20C09DE6DCE9F63F5D1DBA67172F34ED85A6FFB8D1FB7A27B27A33FFD38B
BDABAFE7DF17E1B43B5B4DA7B06AD7BABE3A773AEFDAD7FE7BC07074D5DDB1E7
776BC4D25B2EDB00BBFBEE1656F7ACC08AACAF67572BBDD9A6BDC6E7B6ADEB6F
BAD6FCC5E91B5B3F8F2EBFF6FBD142BFA6D6E71D58EDEAF8C7CDF2EB25ECA0D3
D1DFBEE95E754EEFDF74FA7AB7FBEEA67376DFD1BF9FDDF6FBFA69FBEEF6D68F
AE7A17BADEEB0DDB9DE69DFEFEEDCE17EB6BEFB6F7767D7EEE0125EF69AF1FE8
EDB61E5993FAE5349AF5828B4B86E17A31E97FBC9E863F7AFACDFBFACCBA38EF
996DA0FBCE6D70F5E58D7DE67EE94CFBFDD7A3D3C6C58B8B2BFDB2DB7D7D7C7A
AD03B5FBB7FAC5DC72BF1EDE6B07EDBECE5873F3E9FDD5DBA3CE6DAFF7EA1F89
D9A3291DCD413CFCF9969CD6D9578F73FAB47BFAFCF8E2DFE1F44D3DE6F4F97C
4FDFEBAE7AB0F41A609DBE99CED7FDB6A5B7EDFECAB4AE0F7B20017A74DBB7EC
B707CF3F009C7EFFEDF4F38ED75D74901A97D7AF7FD43B9FDAC8ACB3CBCE9ED5
EDB65FB70F6F75ABDFEF358EEE74EB3540EEEA7BF3E0CB878F079ED6EE3292ED
E46966C06F10DE3BF60FAA2DCC20A4BED01BAE6313131489290EEFFD561B798E
E7B788EBB954F44C3C3714DA88CD64643276E898A379D2E94D26018509F5A89E
F4055373ECAD33500DF117155640C7A6043D009C5BA41135F2EB359E37F71BC7
2FF61B4707DF6A39706BCF1F838948A18CCDD0240397AE356CC18E6D3F08894B
A39098BE15A42D2399623ACBA92981F856C3E9ACB7305F1A04066A42827BA0F1
421BAE6C678C2B52F22769366AE7A65B6BD6EB8719A839C8CDA323522324E9C9
8C34A4A70C62793C8C1C41BC498617410838B5083029E5376C8A711824A236C9
304960164F9E50EAC492004D69333EE5DC9AACDC11190019E069CF90374B41C8
40F896A6ED9F90898603C84034B4A895B4C81E9F0C2D79CFF1D078603C4C1E13
4CBD35991428912388EDDA612B2BC42D62BAF764C0C48B6D3F850A2C75BD90F0
57032180CD83E3FDE6C1C17EB3719C8E5C9A2E6037F2960029ED659445B5497B
3C9F9A2350389B0C0DC2C553DA84ED06D40F4968DA0E419082DCE688E6642704
D6B7889DE9E3CA243130A337857E3835F93BE9503DFC63BF513FDE6FFCF13C47
098EBB1B1666ACA73688393302C5D1B1B8A0DD301DDB725B64D7A193303B7269
FAA618894D80E99BCB13344F4E003E806F5B364C6C46F5EC342E1087D1214883
45C35AC84408D7B271C5CCD8D82C813E64FAB9551812FCC9625F2AEC02E01DF5
5381378967E44730E9311D986A82D4C2C628500F39A9312DCCF571F55300E1B6
5A0315A321CC4184156354C0346F19DA9E1B2858BF6152B85872ABA10629E858
59DBAE202AE873153850A9470DF25FA19F553065F5E870DB353927033AF2DC31
0940EE4FD4AB732563D695D9AB7887822E894CEE7285CDC2803EB28BD231D1D8
41B6CB0D7D7610EB33B6C55B9C29654CE1164931B130B6B8E244136ABBB4C1DB
9968A9B292E2527C47B91992B62AA6E4ACA51A0DEA5AA6456531A70A090DC083
008B661674842998B776952FF2241547139A5BC50B0500159756CB92B5401555
3AA81E0C16C99BE715945963DB1DD3088413FEAA5E8324536722AC066F0F5468
E2671BAD8E1905F82C574B0D740EC8A9996BF35E2956ADD2D342FE240AFECBBA
2C84447DEAC89FB2132823289B7549880C3B53F2C63D4356738426A1852702B0
2B4B27D079F6B67476A981798C7979BC71119C2E5A41C4A7743CC8346C50B89C
77365D935D90135CA9A63CB232847CE078933F78368E1C2FA0357A077B41FF9A
3D96EF84CF015FC61ED393C47C2003180483B75FF1270D1F1E82B5F4BD110D82
9AB70A1128C74405F90138F8B3721D00C59578B080839C84F68212148D3B5471
D198D37BC2AC5730F23DC7A93936F84302E5F07E097230454C986A3E843EFEB0
990F8C7B900CDEB236A6211D852DE0E153362F1C71C11B92CE7E5004721018F9
64B128210DF723C980A3B8C51AC6D698288542DAD2368BFD0243CADF95BD3118
D56ADC8E174EDFF8B336EDB0FCDC509E9ACC53564E508129F6E57BB2CFF293DC
16A74888274623AAC3895129F1C6C12456AB85D0EC5BEDE543364F0C38AED7A3
669A504093A5F1A093B0539687E5B17DE6FD6C621232C47F172B27B485E5636D
49DA26B643333665E8CC334A0516973957D04F1A393165ECE087184B49719460
A4927859B40C96AA81AF8470311AEC7510FAECAF961E7C136F38C3497C804FC3
95EFB22DB3C050E107A003844A9E49E70C672067312A22E152879F887D8B7E1E
69C638705459582D7248299ECC0B49CEEFF8EC640F041E44EF24CE56404BF42D
CD702A3A97A2C55D23DE74BC91E9B0E65D92E0F55760C4590BED329F0381E842
38D7E355DC0A5643DE630727AC319ACE79C35E58ECEFD0F44547B8607FC1F2B3
BF3402591CF377AE07E6920F172C8D43C9241309188340C95CC1E76C5200D8C0
3616B305741942DC847004B605C76BE0C1793440C0102F7B2EA960536BE0C304
1CB49F04BD4614C2219066FE8C543818EC010055783FB79706093D42DD315F98
E076EC30E16192DB40DA275282BB0CB2181B29E15BE4203A48C99BA65114C1F0
D843E72476C2B82E9818976C769D779937B28B001F31CBC41457925254052DBF
860E07F92884A84C3D2E982AF7BCE03C6F4CC0629E2A3172D4013700F52A753B
8DE4D478446E67DBAC4E2E7768A4CA9718F0ACDDFFEDF237C277AE2529B08CD0
81AEC7B2561766881D29A9962BD4834AF95DAE278951455A0C7641D1CDA14349
0535BE45427F45AB06A3D380E5F976E1B4421B205E8136EDD6E21E768E560D31
1CB50C8C0419B0D2D33318892704FE45FC9FC5C376C180900A5A91643536335C
A0D5B45DEB191FF73BEB46B75274C7F3C144910A33300900AEFB0916C21211BE
6134093F992D90314DED81412AD2A1C3625926B43CAB08946F908A8864719DAA
34384E59A1809C24CBB22E84CF90348C814853E26C8E471604235AE674147C67
843C89896A89A3125A598783BD0A3D5EF293B5466887E553EA92547738DC1683
4B2713748F4129C08B4A8A08E09B3051355E368E23396F9BF568004D9040ED1E
629383FC0EA4CC2CB7EC04F896D169F2127A588D0A9F732E15FAC580557C1CC9
9B15B86379536BBC64E2CEAB214390217349FE634D2216E2EF2F5D2B373BDEEE
C85B2CC117073F3B0AC1E813166B8283432A65468347A8CDA323A3CA13DCE835
958E96B2E3D5BCB7581EDE97E606D4E97289090119A439DB20DCCE218599A886
1B3807E4D7F27975C2EB8A63EAD80BB057BE9A710058BD6DA91EB5EDEE13574B
AA6E091396E6A9C18F06A7B774DF19F73B9F9FE1C6281F156448554A146EB213
B2E06306B6A047B19F5724C2450E4975E2DDF73CF42A73DA90398B44A089669C
0C061299659283319226955720BC7C098208F649B3B5E653EA12982B83238599
1730331008E51444442251B31819C11191A90E31A9111A1D47EA92C92C0820D3
796D4CE9920CC6BEB9164715BA4360E2175695D4F741C3F7EB464E81F2A0C462
780A2AC2EF0C6C0CF184B16A9680572C80BE65410FCAB51A04741C0903B14C82
03C1D6C79666088F20C88C848B25FE7AF815E0976D902C150B68C3A0B2F89A1F
6A35FA7D653A2719416AC822AACC673C2ABFFFA40C7F31CA57E6FCE532154FBE
A36A663CDB3809FA0A5E025BD4099AF2792D3EAD300742928A0A12F4CFD2AA73
EE4D5C3F75B96CB08045890FC12F803303ADCB5BD9FCCE2B0E78F8E1F484235A
257FBE2249975B65647193DD6FCCAC7307AE74048421C9415D0107074C638DF9
C2E2C89D79109E0C7621F0406292DD4CEE1A3D6003F1C97B5CD98F4F594ED30C
057166A52319756AA5D4512D414A324A55456745D127EC4CD6A1243CBC050AB3
9D95E4FF4AD3829BC98E32A3F1B43F999448221B97C4C9998D0D54BD6011D5DD
6A188AF594F42A2D5DA1D92AABA115B44E594C2D8CDA504A2D8C55153F846F28
46F3334B7889E8472A0AADEA19B19F5A26CF42602A251B65374614EF44910B5E
436CC7E3F624F89732B0E08F6385AB4AFE22F50D69676081642294B44410F994
60414C3DB55993463063A95C406B8AE23BDBF231D90CC756C249A8B261726C60
6D7ECBA9049B8D3B4DB8868800B2B68CB91DF3ED2FF2105F1E5824DE285BA526
AF524B567900403AA7C5FD8274261279E3EC8DE51625F03DEE76D41E02BCF12D
1792ADF6C7252E4563C3E027D432CAB7AF7205124A3C412CCB5040B5E47BE492
C4BF379A3C38DB376A7A2AFD4F117C99E319A1378781E7AC42CAE4E0D1A4FE05
8A72992B5FF297CE8F47B24B5C87C983528EDED2C9D85CB7FA3F5E6099C49536
F022CA2E9EB03B26E25E49DEE530480A41EDBCB15B1D83CDA3B62FD32519371E
3DE7E6C57724A452D79DE9AC681C28452C32F5E154264A874DE5A2257EADCAB3
D9E0EA32D7B6D4DB85D80FFDDDE28D8D2D02CE38B0F35CE75E89166C92DF81D6
3077A118C0D3CF6C448953C4CA354A09CEE5A6B39F48DC39DDDD63E96783D1BB
126189870B2344A31313738CD047067B301206D57082610CA04FE970E3E72789
5AB9B21082C0B250E90CAC16A9B550949054CAA85415520119C92F14D7DAD574
5CC48A9248E12F887D2E1988B9F6C7E602C14AF9E1681516135F49EA2BAE0FA6
9FF20CE813EFB666925752112549812B2EDC6E4838F2DC5C116F6EEB9B8AD414
922E5FBC493FE2F0B96FF174791EF55C82262DC6AA39086B15B0CE7172B3FD79
98ABA6EFC3E153CE52F67E6BA696D239CEDD0DF87AF5FDE7F00B1FE27BEC5CF8
A3C0E64285E25FA474194DE3320FC6DFAEA1222DB7A18CBE3CA9A04CCCB14A13
D697E25889D79AF68863825F961FAD28A8F0407412B00B0149448A28629A9B9D
1808B25E128C4A858A5D9E5E4DF5261B84A605906DEEB6F0DB9095B20CAEB44C
55B67D80774148784D32D7C9CA5DB93E5E8FCC750A4615FA45ED30D39B629231
F0F11E25C3CEBBC41D92F412C30EBBE4C13204995B1EE9058FF45E867C3323BD
1921DFCDE0D1C84E725067FE312577BBE0A1A23262A03552BB943AED99FFA7B0
852B5B6902F7D81278B735E3D8DAB21F2B7BEF7238920BA3EEF3C3EFE597B082
ACA8316A33B194FC0F4EDCEB9C891B73123B8BC7D62C1358A4898F38807E8040
79F1CE5F8A5253AAD26477BA18373653EE49BB44B5891369E93EE37F084A7659
93B0D222BC8C7CA8DC1378B5CCE66E41B81A99A54D01B8042696F69F80E28B52
24C4202067732BD6487628BE646727D7B20587528E714B85025B2DAAB6505CE9
F69674D5077ACCF13873A72BBDFD1697FB714A72A52D003F4943CBA2C141A2A1
B30D5E5D6C99F9D04301C9244363202209FC77431B040804940CD96F930C307B
8BB782C12732E21B663C8F5D82914872B36B38325AB011E325D6BFBD0989FF97
78E79FFF0120649CE6573C0000
}
insert-event-func [
either event/type = 'resize [
mn/size/1: system/view/screen-face/pane/1/size/1
my-tabs/size: system/view/screen-face/pane/1/size - 15x30
show [mn my-tabs] none
] [event]
]
view/options center-face layout [
across space 0x0 origin 0x0
mn: menu with [
size: 470x20
data: compose/deep [
" File " [
"Open" # "Ctrl+O" [request-file]
"Save" # "Ctrl+S" [request-file/save]
bar
"Exit" [quit]
]
" Options " [
"Preferences" sub [
"Colors" [alert form request-color]
"Settings" [request-text/title "Enter new setting:"]
]
"About" [alert "Menu Widget by Cyphre"]
]
]
]
below
at 10x25 my-tabs: tab-panel data [
"1" [
h1 "Slide 1 - A Few Basics"
text "By default these slides are white and full screen."
text bold "Adding images is easy:"
image logo.gif
image stop.gif
image info.gif
image exclamation.gif
text {
Press the space bar, right arrow key, or left click screen
for the next slide. Press the left arrow key, or right
click screen to go back to previous slide. Press the 'X'
key to quit...
}
]
"2" [
h1 "Slide 2 - Colors and Gradients"
at 0x90 box as-pair system/view/screen-face/size/1 220 effect[
gradient 1x1 tan brown
]
at 20x70 text "Colors and gradient effects are easy in REBOL:"
box effect [gradient 123.23.56 254.0.12]
box effect [gradient blue gold/2]
text {
Left arrow key or right click screen to go back, 'X' key
to quit...
}
]
"3" [
h1 "Slide 3 - A Simple Window"
text "This slide is smaller, and as simple as can be."
]
"4" [
h1 "Slide 4 - Lots of Stylized Text"
across
text "Normal"
text "Bold" bold
text "Italic" italic
text "Underline" underline
text "Bold italic underline" bold italic underline
text "Serif style text" font-name font-serif
text "Spaced text" font [space: 5x0]
return
h1 "Heading 1"
h2 "Heading 2"
h3 "Heading 3"
h4 "Heading 4"
tt "Typewriter text"
code "Code text"
below
text "Big" font-size 32
title "Centered title" 200
across
vtext "Normal"
vtext "Bold" bold
vtext "Italic" italic
vtext "Underline" underline
vtext "Bold italic underline" bold italic underline
vtext "Serif style text" font-name font-serif
vtext "Spaced text" font [space: 5x0]
return
vh1 "Video Heading 1"
vh2 "Video Heading 2"
vh3 "Video Heading 3"
vh4 "Video Heading 3"
label "Label"
below
vtext "Big" font-size 32
banner "Banner" 200
]
"5" [
h1 "Slide 5 - Live Code"
h3 "Remember, These Slides Are Live, Fully Functional GUIs!"
box red 500x2
bar: progress
slider 200x16 [bar/data: value show bar]
area "Type here"
drop-down 200 data reduce [now now - 5 now - 10]
across
toggle "Click" "Here" [alert form value]
rotary "Click" "Again" "And Again" [alert form value]
choice "Choose" "Item 1" "Item 2" "Item 3" [alert form value]
radio radio radio
led
arrow
return
]
]
] [resize]
8.4 Show.r - A Useful Line-By-Line Presentation System
Carl Sassenrath (the creator of REBOL) released a simple and productive presentation tool at http://www.rebol.com/notes/devcon07-carl.zip. Here's the code (you only need to edit the title and background image - copy and paste the rest):
REBOL [
Title: "Slideshow Presenter"
Author: "Carl Sassenrath"
Version: 3.0.3
]
file: system/script/args
if not file? file [file: request-file/only]
title-line: "Presentation Title" ; EDIT THIS
diags: %diags.r
save/png %logo.png svv/image-stock/2 ; demo image
back-image: %logo.png ; AND EDIT THIS
author-mode: off
time-need: 1:00
;-- Configuration --------------------------------------------------------
page-size: system/view/screen-face/size
;page-size: 800x600
;page-size: 1024x800
;page-size: 740x480
big-size: page-size/x > 1000
sect-size: page-size/x / 4 - 40
text-size: (round/to page-size/y / 32 4) - 1 ;pick [28 20] big-size
margin: round page-size/x / 10
h2-size: as-pair page-size/x - margin - 30 text-size * 2
h3-size: h2-size - (text-size / 2)
origin: page-size / 11
def-in: round page-size/x / 7
spacing: page-size / 100x200
;-- Styles ---------------------------------------------------------------
back-image: load back-image
stylize/master [
vh2: vh2 h2-size left top font [
size: text-size + 6
name: "arial black"
style: [underline]
color: 240.220.60
shadow: 3x3
]
vh3: vh2 h3-size left middle font [
color: white
style: [bold] ; underline]
size: text-size
name: "arial"
]
para [origin: 10x2]
effect [merge gradmul 96.96.96 128.128.128]
txt: vtext page-size/x - margin - 50 font [
style: 'bold
size: text-size
shadow: 2x2
]
txtb: txt page-size/x - margin - 50 font [
color: sky + 30
]
txti: txt italic white effect [merge colorize red]
code: tt page-size/x - (margin * 2) - 50
black snow edge [size: 2x2 color: gold]
;snow coal edge [size: 2x2 color: gray]
font [
size: round (text-size * .8)
style: 'bold
colors: [0.0.0 0.0.80]
] as-is para [origin: margin: 12x8]
]
bullet: to-image make face [
size: text-size / 2 - 1 * 1x1
color: 160.0.0
edge: make edge [color: black size: 0x0]
effect: [oval gradmul 1x1 255.255.255 0.0.0 oval]
]
shift-bullet: text-size - bullet/size/y * 0x1
;bold: make face/font [name: "Arial Black" size: 480]
;scale: page-size/x / 800 / 17
backdrop: layout [
size page-size
across
at 0x15 box as-pair page-size/x text-size * 2
edge [size: 0x2 color: gold]
effect [merge grid navy gradmul 200.120.100 128.128.128]
origin 10x20
banner font-size text-size + 4 italic title-line white
;font-color silver
return
]
backdrop/image: back-image
backdrop/effect: [gradmul 1x-1 50.50.90 100.120.140 fit]
backdrop: to-image backdrop
end-mark: make face [
offset: page-size * 0x1 + 40x-20 size: 140x3 ; -140x80
effect: [gradient maroon green]
]
pan-mark: make face [
offset: page-size * 0x1 + 40x-20 size: 140x3
effect: [gradient maroon purple]
]
;-- Scanner --------------------------------------------------------------
*scanner*: context [
;-- Variables:
text: none
part: none
code: none
title: none
out: [] ; holds output block
;-- Emitters:
emit: func ['word data] [
if block? word [word: do word]
if string? data [trim/tail data]
repend out [word data]
]
emit-section: func [num] [
emit [to-word join "sect" num] text
title: true
]
;--- Text Format Language:
rules: [
[to "^/=start" skip to newline (author-mode: true) | none]
some parts
]
parts: [
;here: (print here)
newline |
spaces
;--Document sections:
"===" text-line (emit-section 1) |
"---" text-line (emit-section 2) |
"###" to end (emit end none) |
;--Special common notations:
"***" para opt newline (emit bullet3 part) |
"**" para opt newline (emit bullet2 part) |
"*" para opt newline (emit bullet1 part) |
":" define opt newline (emit define reduce [text part]) |
"#" para opt newline (emit enum part) |
"!" para (emit txti part)|
"[" example (emit code detab code) | ; trim/auto
";" thru newline | ; comment
";===" to "===" |
"==" output (emit output head insert code " ") |
"=image" file (emit image text) |
"=all" (emit all true) |
"=intro" (emit intro true) |
"=diagram" some-chars (emit diagram text) |
"=pad" num (emit pad num-n) |
"=skip" num (clear back back tail out) num-n [to "===" thru newline] |
"=" some-chars | ; ignore unknown options
;--Defaults:
para (emit para part) |
skip
]
space: charset " ^-"
nochar: charset " ^-^/"
chars: complement nochar
spaces: [any space]
some-chars: [some space copy text some chars]
text-line: [copy text thru newline]
;par: [copy part some chars newline]
para: [copy part some [chars thru newline]]
;example: [copy code some indented] ; | some newline indented]]
example: [thru newline copy code to "^/]" skip thru newline]
indented: [some space chars thru newline]
output: [
copy code indented any [
"==" copy text indented (append code head insert text " ")
]
] ; compensate for ==
define: [
copy text to " -" 2 skip any space para
]
file: [
spaces copy text some chars thru newline (text: to-file trim text)
]
num: [
spaces copy text any chars (num-n: either text [to-integer text][1])
]
num-n: 0
;-- Export function to scan doc. Returns format block.
set 'scan-doc func [str] [
clear out
parse/all detab str rules
copy out
]
]
;-- Load it up -----------------------------------------------------------
doc-text: read file
if find doc-text "=author" [author-mode: on]
doc: scan-doc doc-text
;?? doc halt
;do diags ;;; NASTY!
;-- Globals --------------------------------------------------------------
this-page: doc ; points to current page position
title: select doc 'title
options: select doc 'options
time-left: 1.0
time-start: now/time
back-flag: false
;-- Helpers -------------------------------------------------------------
title-of: :second
next-page: does [this-page: any [find next this-page 'sect1 this-page]]
back-page: does [
this-page: any [find/reverse back this-page 'sect1 this-page]
]
limg: :load-image
load-image: func [file][
either exists? file [
limg file
][
make image! reduce [page-size / 3 200.0.0]
]
]
;-- Page Builder ---------------------------------------------------------
build-page: has [page out emit bull count] [
at-once: author-mode
intro: false
in-sect: false
count: 0
; Slide title line and indentation:
out: compose [
across space spacing
at (origin)
; vh2 (title-of this-page) return
; indent margin guide
]
emit: func [blk] [append out compose blk]
bull: func [depth] [
emit [pad (20x0 * 2 * depth + shift-bullet)]
emit [image bullet effect [key 0.0.0]]
emit [pad (-8x0 - shift-bullet)]
]
foreach [type data] this-page [
switch type [
sect1 [
if in-sect [break]
in-sect: true
emit [
vh2 (data) return
indent (margin) guide
]
]
sect2 [emit [pad 0x4 * spacing vh3 (data) return]]
para [emit [txt (data) return]]
code [
emit [
pad to-integer margin / 3 code (trim/auto data) return
]
]
bullet1 [bull 1 emit [txtb (data) return]]
bullet2 [bull 2 emit [txtb (data) return]]
enum [
emit [
txtb (join count: count + 1 [". " data]) return
]
]
pad [emit [pad (data * text-size * 0x1)]]
txti [emit [txti (data) return]]
define [
emit [
pad 30 txt def-in no-wrap (data/1) txtb (data/2)
return
]
]
diagram [
type: layout/tight blk: get to-word data
emit [
pad (as-pair margin 30)
;panel (type/size) [(blk)]
return
]
]
image [
data: load-image data
emit [
pad (
as-pair
page-size/x - data/size/x / 2 - margin
text-size
)
image (data)
return
]
]
intro [
intro: true
at-once: true
]
all [at-once: true]
]
]
out: layout/tight out
offs: 100x100
if intro [
foreach face out/pane [
if face/style = 'txt [offs: 110x120]
face/offset: face/offset + offs
]
]
out/pane
]
;-- Show page:
show-page: func [out] [
; Do we step through items one at a time?
either any [at-once back-flag] [
items: []
][
items: copy next out
clear next out
]
screen/pane: out
if at-once [show-end-mark]
show screen
back-flag: false
]
pan: []
show-end-mark: does [
time-used: now/time - time-start
either time-need - time-used <= 0:00 [
end-mark/effect/3: red
end-mark/size/x: 140
][
end-mark/size/x: (
to-decimal (time-need - time-used) / to-integer (time-need)
) * 140
]
append screen/pane end-mark
]
;-- Traverse pages:
next-item: does [
if not empty? pan [
append panel pan/1
remove pan
if empty? pan [
append screen/pane pan-mark
]
show screen
exit
]
either not empty? items [
if items/1/style = 'panel [
panel: items/1/pane
pan: copy panel
clear panel
append screen/pane items/1
remove items
next-item
exit
]
if items/1/style = 'image [
append screen/pane items/1
remove items
]
append screen/pane items/1
remove items
if empty? items [show-end-mark]
show screen
][
next-page
show-page build-page
]
]
back-item: does [
pan: []
back-page
back-flag: true
show-page build-page
]
;-- Handle Keystrokes:
do-key: func [key] [
if key = escape [quit]
switch key [
#" " [next-item]
#"^(back)" [back-item]
down [next-item]
up [back-item]
page-down [next-page show-page build-page]
page-up [back-item]
]
]
count-slides: has [n d] [
n: 0
d: doc-text
while [d: find/tail d "^/==="] [n: n + 1]
n
]
this-page: doc ;next-page halt
;-- Build screen and event handler:
screen: [
size page-size
across
]
if not author-mode [
append screen [
at (as-pair page-size/x - 150 20) t1: txt form now/time
at (origin) guide
v1: vh2 "Show07.r Information and Setup" return
vh3 200 "Version:"
vh3 gold form system/script/header/version return
pad 0x30
vh3 200 "File: " vh3 gold form file return
vh3 200 "File size:" vh3 gold reform [
round (511 + size? system/options/script) / 1024 "K"
] return
vh3 200 "Slides: " vh3 gold form count-slides return
pad 0x30
vh3 200 "Page size: " vh3 gold form page-size return
vh3 200 "Font size: " vh3 gold form text-size return
vh3 200 "Head font: " vh3 gold form v1/font/name return
vh3 200 "Body font: " vh3 gold form t1/font/name return
vh3 200 "Origin: " vh3 gold form (origin) return
vh3 200 "Margin: " vh3 gold form margin return
vh3 200 "Spacing: " vh3 gold form spacing return
]
]
screen: layout/tight screen
screen/image: backdrop
screen/color: navy
view/new screen
insert-event-func func [face event][
switch event/type [
key [do-key event/key]
down [next-item]
alt-down [back-item]
close [quit]
]
event
]
items: []
if author-mode [show-page build-page]
do-events
Using the program above is easy. Save it as %show.r. Edit the "title-line" and "back-image" variables. Upon running the program, a file is requested. Save the following text file as show-example.txt, and load it into show.r when requested (you can also examine the text files included in the download at http://www.rebol.com/notes/devcon07-carl.zip - it contains three actual presentations created by Carl). The space bar, mouse, and cursor keys control progress of the slide contents:
===Presentation Title
=intro
---Nick Antonaccio
Operating Manager
Merchants' Village, LLC
Pittston, PA 2013
===A Main Slide Header
---A Sub Header
(Some sub text)
=pad
#Numbered item 1
#Numbered item 2
#Numbered item 3
#Numbered item 4
===Another Main Slide
---Another Sub Header
*Bullet Item 1:
**Sub Bullet Item 1a
**Sub Bullet Item 1b
**Sub Bullet Item 1c
*Bullet Item 2:
**Sub Bullet Item 2a
**Sub Bullet Item 2b
**Sub Bullet Item 2c
*Bullet Item 3
*Bullet Item 4
===A Third Main Slide
---Subheader:
=image rebol.gif
=pad
=image info.gif
===Slide 4
---Subheader 4:
Topic 1 - idea 1
Topic 2 - idea 2
Topic 3 - idea 3
Topic 4 - idea 4
[
A preformatted text block:
Idea 1: some thoughts
Idea 2: some thoughts
Idea 3: some thoughts
Idea 4: some thoughts
]
===Slide 5
Some Text
---Definitions:
:Idea 1 - definition of Idea 1
:Idea 2 - definition of Idea 2
:Idea 3 - definition of Idea 3
:Idea 4 - definition of Idea 4
=all
---Last Idea 1
---Last Idea 2
---Last Idea 3
###
Notes: nothing below the 3 pound characters appears in the presentation.
Because this program is entirely REBOL code, you can make changes and add functionality to it as needed. And just as with all other REBOL software tools, you can transfer, install, and quickly send by email the entire program, together with the REBOL interpreter, even to users with low powered computers and slow bandwidth Internet connections. The program will run on any operating system supported by REBOL, and the entire setup is free for anyone to copy, use, alter, etc.
8.5 Creating "Screen Shot" Images of GUIs
When building presentations, it can be helpful to have screen shots of programs and/or to build graphic layouts using the REBOL graphic dialect. You can use the "to-image" function to create images from GUI layouts, and the "save/png" function to save image data to a .png file:
REBOL [title: "Create Images From GUI Code"]
save/png %image1.png to-image layout [
size 600x400
backdrop white
h1 blue "Slide 1"
text "Here's a red box:"
box red "I'm red!"
]
browse %image1.png
Here's an advanced graphic example by John Niclasen (the "draw" dialect used here will be covered in more depth later in this tutorial):
REBOL [title: "Shiny Black Button"]
sz: 200x400
img: make image! sz
img/alpha: 255
draw img compose [
pen none
fill-pen linear (as-pair 0 sz/y / 2) -50.5 70.5 90.0 1.0 1.0
45.45.47 9.9.11 108.113.117
circle (as-pair sz/x - 115 sz/y / 2) 70.5
fill-pen 1.0.5
circle (as-pair sz/x - 115 sz/y / 2) 68.5
reset-matrix
fill-pen linear (as-pair sz/x - 115 sz/y / 2) -60.5 30.5 45.0 1.0 1.0
161.164.169 161.164.169 89.94.100
box
(as-pair sz/x - 115 - 26 sz/y / 2 - 26)
(as-pair sz/x - 115 + 26 sz/y / 2 + 26) 10.0
fill-pen 1.0.5
box
(as-pair sz/x - 115 - 22 sz/y / 2 - 22)
(as-pair sz/x - 115 + 22 sz/y / 2 + 22) 6.0
reset-matrix
fill-pen linear (as-pair 0 sz/y / 2 - 26) -50.5 100.5 90.0 1.0 1.0
1.0.5.255 200.214.226.224 200.214.226.128
shape [
move 154x200
arc 16x200 68.0 68.0
arc 154x200 -149.0 68.0
]
]
save/png %black-btn.png to-image layout [
backdrop 1.0.5
image img
]
browse %black-btn.png
8.6 Embedding Binary Resources (images, sounds, files etc.) in Code
The following program can be used to encode external files (images, sounds, DLLs, .exe files, etc.) so that they can be included within the text of your program code:
REBOL [Title: "Simple Binary Embedder"]
system/options/binary-base: 64
file: to-file request-file/only
data: read/binary file
editor data
Use "load (data)" to make use of any text data created by the above program. This example uses a text representation of the image at http://musiclessonz.com/test.png, encoded with the program above:
picture: load 64#{
iVBORw0KGgoAAAANSUhEUgAAAFUAAABkCAIAAAB4sesFAAAAE3RFWHRTb2Z0d2Fy
ZQBSRUJPTC9WaWV3j9kWeAAAAU1JREFUeJztlzEOgzAQBHkaT7s2ryZUUZoYRz4t
e9xsSzTjEXIktqP3trsPcPPo7z36e4/+3qO/9y76t/qjn3766V/oj4jBb86nUyZP
lM7kidKZPFE6kydq/Pjxq/nSElGv3qv50vj/o59++hNQM6Z93+P3zqefAw12Fyqh
v/ToX+4Pt0ubiNKZPFE6Ux5q/O/436lkh6affvrpp38ZRT/99Ov6+f4tPPqX+8Ps
/meidCZPlM7kidKZPFE6kydKZ/JE6UyeKJ3JE6UzeaJ0Jk+UzuSJ0pk8UTMmvn8L
j/7l/nC7tIkonekLdXm9dafSmeinn376D/rpp5/+vv1GqBkT37+FR/9yf7hd2kSU
zuSJ0pk8UTqTJ0pn8kTpTJ4onckTpTN5onQmT5TO5InSmTxROpMnasbE92/h0b/Q
//jR33v09x79vUd/73XvfwNmVzlr+eOLmgAAAABJRU5ErkJggg==
}
view layout [image picture]
The program below allows you to compress and embed files in your code. This compressing BINARY RESOURCE EMBEDDER program will be referred to many times throughout the tutorial. Save it to a .r file so that it can be run later:
REBOL [Title: "Binary Resource Embedder *** SAVE THIS PROGRAM ***"]
system/options/binary-base: 64
editor picture: compress to-string read/binary to-file request-file/only
To use the compressed version of data created by the program above, use the following code:
to-binary decompress {compressed data}
For example:
REBOL []
image-compressed: load to-binary decompress 64#{
eJzrDPBz5+WS4mJgYOD19HAJAtL/GRgYdTiYgKzm7Z9WACnhEteIkuD8tJLyxKJU
hiBXJ38f/bDM1PL+m2IVDAzsFz1dHEMq5ry9u3GijKcAy0Fh3kVzn/0XmRW5WXGV
sUF25EOmKwrSjrrF9v89o//u+cs/IS75763Tv7ZO/5qt//p63LX1e9fEV0fu/7ap
7m0qZRIJf+2DmGZoVER5MQiz+ntzJix6kKnJ6CNio6va0Nm0fCmLQeCHLVMY1Ljm
TRM64HLwMpGK/334Hf4n+vkn+1pr9md7jAVsYv+X8Z3Z+M/yscIX/j32H7sl/0j3
KK+of/CX8/X63sV1w51WqNj1763MjOS/xcccX8hzzFtXDwyXL9f/P19/f0vxz4f2
OucaHfmZDwID+P7Hso/5snw8m+qevH1030pG4kr8fhNC4f/34Z89ov+vHe4vAeut
SsdqX8T/OYUCv9iblr++f67R8pp9ukzLv8YHL39tL07o+3pekn1h/dDVBgzLU/d3
9te/Lki4cNgBmA6/lO+J/RPdzty8Rr5y94/tfOxsX6/r8xJK0/UW9vlH93/9oAzR
e09yKIUBVbT9/br/U/m7x6CU98VAAJS2ZPPF/197eEDhtfs9vX9rDzc6/v3qzUyo
nJA/dz76Y77tHw+w3gXlbEMpDKihza/+7/o/c3+DU54tDwsobR2/fXR/qYXBiV8T
t3eDEmpA/d9LDASK0y/tnz+H/Ynmt78E1vti7lAKA6pouxz/X7v+uR045ZFdRE6x
1q21pG7NiSzx1f5R40pvvdNn+oB1P4Onq5/LOqeEJgCemFy1KQgAAA==
}
view layout [image image-compressed]
You will use the binary resource embedder regularly. It's a good idea to save it now to your desktop or some location on your hard drive that's easily accesible.
8.7 Playing Sounds
Playing .wav sound files is easy in REBOL:
insert s: open sound:// load %/c/windows/media/tada.wav wait s close s
Just as with images, you can use the "Binary Resource Embedder" program to save sound files as code in your programs. This example loads and plays an embedded sound:
REBOL []
my-sound: load to-binary decompress 64#{
eJxtlHtMU1ccx6vGR3kWLMWpm5pskc2pi3MkqFNYJsh0oigV5KEULK2lpdDb9vbe
9r56W1pogSKltBQqSGGAlYeimAlDkU1H1KgssrhBmFNhjAECHcZl2bVo1LjfyTm/
nO/vdf45n9joyMjphTTaoQj2Tq4QWB1Io9HmUSt8Fc2z59GWUEp6KpB6k8pC3zJs
7nzTvXbB/sdhmMdjcx5744q9SkHf1l4f/LLhq+K33jZnOI6/3gZBVAjq8Qj6Mkzl
ek5KwBEpL11saGyrANhx4kKHzUBgmFKhUKKIAgBAlNRqNaSayiO1pEqeLeTzxZjJ
nA+KAK2ByBaidle1XoGVnrLjx3kSlULEOSaBIBmkMZkL9Lo8owYUZYqyRLwMMVJo
NqgkoL7MQoqOiXUVzpPl1a7OiyXS6K2RBxJiE1KgitrTteYyq0ktStgbE3MwTUha
7BXFmDxHjpGITE5Unu++dLaupc2UFTLfd5PEZi8xObp7W/Ts8N3Jx3npXMjqarQA
8XFHpJBUeAwscNRZ8wwF1a7auvIzVWRCwCL/XY6B4c5CWGYol+8LWfk52X63t1Ka
mnw0Izk+XkCcbDpVKueKUdvpRofVfulms3TlIh8mODQ7fjpx2359ERTFZB3u+Gt2
MH+zr8+mgwJMZ2/vajYeDt2aYr72U7eNUDf+es+yYQFzQcQvs+MTmQH0zaTTsued
j/HHT2b6vloc6L0+71Z/hw2RykVHvt6bVtTZd+ecraCq3sT28fNnFD4bdQ/sWhwQ
xHG14HH7iHtPpi6ELgn0CnFNTQybdwSviYLs39TYyqwN565cq+evoQctDvv+6cjT
ns/ogQGH6r4tVwh0PWMjzq1egb5rqqfHJhq3+Pt9kOW61l2jh3TOG0Pt/BV01sL9
A+7H/5xi+TGWpl/pq0cScxruP2qI8g70W2Zxj7ovb/cJYCS0D49dMXC5up4H13OC
vFkLEodnhv8t8WUE+AsHn9w27totPj94XbXCZ6l31uj45MOkJUz65tbJiZGahJ1p
rv6fcZYXa/7eodlHtLplXqyFkXenJ7qSwz4VNlxuTlrK8NvYNDPqrl9LDcq4Pzk5
aIyNFpdftITSmfQP256NuH9jL2L6MhVDk2Mdiojt8WBZniB8bWha7f2JOyf2BLO2
4b1jk301pAxAeFtWegfvbJgan36ofc/Hj7G/cuCPvlaYx04SZPCT4lKExvqLbRXg
wS+jBZbv+vuuNhWDieuCAoN3EP0zU5PnOcu9vEO41Tdu9zp1AOc4N4PDPpSYlV/T
dtaBcWJi+YbWH+/e7HAoD6zyZyyP0t9z//1nq2Cjj/8nKaXtP1ytOyHn8TkZ3DRu
ugjWWU9WFqMivhCznunq6W42Z4Yt8wrYwGt++NR9x5K0/t33I0TFrZ1tTj3ATU7l
iUFQli2SwFrTiaJ8jdZgrmpou+CqkOz+yD94XZK568Hvt5rwo3u+iEmR6IttZUYS
yBRkyhFCDcnEmdkArMkvsTlqapx1VeWGnCNRYWHhidKi+pbW+tJ8pVwuQ3G1BoVz
eOkcbg6caywoyMWVIKTEqf9ozNPrcrUYLMuRABBO6nUkppACAIzpqLQ8LYmpIBAE
FRAEwbASwQmCwFSwgpIgJarW6inTUfUakiTVOAJTMkJoNCSuAmVSQCqHUdwTec4X
tRrHVEpYqaKMIg9OqNVqAvcgh9RoczUkgSKeGMUlRKWkBqpQah6Gzkko/oJjOE7g
L2FFAc2DM8zTjHoZDKsw4vlEDwyRF5xUKZVUj7lagsCxOfYR/wHwfuhW/AUAAA==
}
insert port: open sound:// my-sound wait port close port
8.8 Launching Code in Separate Processes
For background sounds and other elements of a presentation, it can be helpful to run code sections as separate programs which don't block or interfere with the operation of your main program. You can accomplish this easily by writing the separate code to a text file, and then using the "launch" function to run it. The "launch" function opens a new instance of the REBOL interpreter, to run the loaded file. The launched code executes as a totally separate and independent program. Be sure to include a REBOL header in the saved program:
REBOL []
write %playsound.r {
REBOL []
insert s: open sound:// load %/c/windows/media/tada.wav wait s close s
}
launch %playsound.r
8.9 Running Command Line Applications
The "call" function executes commands in your computer's operating system (i.e., DOS and Unix commands). This can be really useful when creating presentations of all kinds. The example below opens Windows' Notepad to edit the "C:\YOURNAME.txt" text file created earlier (leaving out the /show option runs the program in a hidden window):
call/show "notepad.exe c:\YOURNAME.txt"
This next example opens Windows' Paint program to edit an image we downloaded earlier in the tutorial:
call/show "mspaint.exe c:\bay.jpg"
Here's an example that embeds an executable program into the code, decompresses, and writes the program to the hard drive, and then runs it with the call function:
program: load to-binary decompress 64#{
eJztF11sU2X03K4VqJsrkZJp6OzchhFJsx8qDB9od1fHdIO6ds7AgJX2jttyey/p
vWUjJuNnmNhMibzwaCSLi+EBE1ziGIkBGh0BSYTwwAMme9Dk4kgkgSiKcj3nu7es
QrKFhMUQOcn5+c7fd875+vXe27FJAg4AbIiGAQwWIwZMEbqTcmODN5xRdmRi6aoy
Z83YogngLlaNtV+s6kV7q9KelHeu9LYqQTXt7e/v97UqLcLuqKJIvriShnAIoJ0r
gXvPn+StlDAF5dyzHLwAdlw4TZ1Mm7oQvWDu7jKLslsxBc4KQ30bb9bMHF3F/D5j
MFAHEIbHD+cwb88s9riSEIjvK7EKogZs//bxAvQmYlqM5JsOUwHPWFgEAYDTvqTp
eYdy1Fn5Sh/O96h9nLrrDcD4IpQm7UOkWL/nt6MlqMvxrkl+GVWS7xqWalzDzqGz
9rbyD5ehpmnl+ezt3M/RSPe7Q9/ajeh5+9Ztm3vKh9xoM7SaimLUR18C2JKf+Kg2
APoJwzDOuiAF+hHU/pHXryObdLyP+y2kEhx7UaLfo0gq/RJa60/n88Ndrpz7FmqG
u5bk3L8zwdWXc0+jdOYXkn4lnYfW++/qOPLyDz7BfH3jTXVnplx949inhPvnSgw/
8RSIHM7P8PdSUYtxlxSkONE+o/u7EkNElMbpcuRKUhTjmLH/iHbDQQ7DHqL77zbh
oQxeRa9duBQHkRj+HnIdr7y/e178AvmmnHt5VQAmaNo59/EZ8QSJAY7EURJvMu2x
KipYj2CaEToYve2eYYiwl4rWY6jN8RWF5XtsuWSyhO7aJG8XXQFkNdWYIqIHK8nH
8FOSFJMoteEfZfQEo1SNCPCW2/BTjWK1uXkp9dDDegjrDqpkAUtiJhNp4ma3qUrx
MG6dqkyFMQ2ExQmaxgU2c/07D2ZJsCz3Q68Xh76Cvac2pZwi8jCO8rIZd4jielmc
uHxmsEMe1vMBZJf0YY8Pda95yH5p+tWrI86XMZbTE5a1gVlXFKyryeowp0Cy4Wf+
hdSrWGp26N008hW4XnS6/OBS7MnUVHoK0osoTV+22qF56c95qKdtZBzB66J/imSc
/Rmsg/KDdHFbA9O3RrZWByD/qPf1KTCwze3y2KCbn9vnP4ExoItiwr11zvncqq6+
oXGV//XVa5qCzXxL6M3ZfBfMZyFPBvywgD3FGDjLnGVl83o4T+HJAZ/PFxWTqrcj
GxerHljRqyL9sWXxqU2/nkHki1H4HDkvJeM7vZooeLdnNU2R10K34G1XdgveTmE7
vmv7fNDcFY1u3ABpNa5J6rZd9MouqGpjw6z1GLXn6vDxV/s9o1cYvcroNUanGP2J
UZ3RG4zeZPQ2o3cY/YtRqCdqZ3Qho6WMuhitYHQZ0pr6mRr21Zvv03VFuuMoX0Gd
VqT7BlupKFoXw8eo/8yynUR+HvEa4g3EPxEXYuwSxOWIaxADiGHEBKKGeADxCOIx
a1wXkE81zH/ut0OdG0LtjQ2+hCSBzLUKWoeSyErC+pickIQgfAmhgaSG319xPEvo
ioQ6Ld9D0CL04ddZQuknaxA4W1hRtXeySa0DXWM7BHjDFhHkhLUKYs2cJTcrA0H4
mmtXYgk+m1GVTBBOsVVbXJGDsNTWKexIqpqQ4aWYqgbps4LPCDFNMPcLYXQpldrC
g0bcVHcKcQ220DqyB4PTHYKWScZVgCGsw/LBEgHWsjYLZR2zRTMxWZUwfaFwOAot
SXVXTIuLM9V/ZeuSMw/UxW/s4KOF6W2GNjmp8Uo6rci8ImsZRVLxG+1hZWhgrlv6
/4F/ABcSIgQAEAAA
}
write/binary %program.exe program
call/show %program.exe
The "call" function has many options that allow you to monitor, control, and make use of output from external command line applications. Type "help call" into the REBOL interpreter for an introduction. For more information, see http://rebol.com/docs/shell.html.
8.10 Creating Simple Animations
You can place your widgets at specified coordinate positions (XxY coordinates indicate X pixels over and Y pixels down):
REBOL []
view layout [
size 600x400
at 200x250 btn "button 1"
at 300x350 btn "button 2"
]
Change a labeled widget's position using the "/offset" refinement, followed by a colon:
REBOL []
view layout [
size 600x400
at 20x20 btn1: btn "button 1"
at 100x20 btn "change button 1's position" [
btn1/offset: 300x250
show btn1
]
]
You can create a coordinate position from two separate numbers, using the "as-pair" function (paste/F5):
REBOL []
x-size: 600
y-size: 400
x-position: 20
y-position: 20
view layout [
size (as-pair x-size y-size)
at (as-pair x-position y-position) button1: btn "button 1"
]
To create a repeating loop, just copy the line below that starts with the word "box", and the closing 3 square brackets, into your GUI code. Anything inside those brackets will be repeated continuously. This is a simple way to create continuous motion:
REBOL []
view layout [
size 600x440
btn1: btn red
box 0x0 rate 0 feel [engage: func [f a e] [if a = 'time [
btn1/offset: btn1/offset + 2x2
show btn1
]]]
]
To control movement using keyboard controls, you need to check for user keystrokes:
REBOL []
view center-face layout [
size 600x440
text "Press an arrow key"
key keycode [left] [alert "You pressed the LEFT arrow key"]
key keycode [right] [alert "You pressed the RIGHT arrow key"]
]
Put the motions to be performed inside square brackets following an if test:
REBOL []
direction: "down"
view layout [
size 600x440
btn1: btn red
box 0x0 rate 0 feel [engage: func [f a e] [if a = 'time [
if btn1/offset/2 > 420 [direction: "up"]
if btn1/offset/2 < 1 [direction: "down"]
if direction = "down" [btn1/offset: btn1/offset + 0x5]
if direction = "up" [btn1/offset: btn1/offset - 0x5]
show btn1
]]]
]
Use REBOL's "within?" function to test for graphic collisions (i.e., when graphics touch, or share coordinate locations):
REBOL []
direction: "down"
view layout [
size 600x440
btn1: btn red
at 20x350 btn2: btn green
box 0x0 rate 0 feel [engage: func [f a e] [if a = 'time [
if btn1/offset/2 > 420 [direction: "up"]
if btn1/offset/2 < 1 [direction: "down"]
if direction = "down" [btn1/offset: btn1/offset + 0x5]
if direction = "up" [btn1/offset: btn1/offset - 0x5]
show btn1
if (within? btn1/offset btn2/offset 1x1) [alert "Collision!"]
]]]
]
This simple program demonstrates the some of most important animation techniques discussed here. Catch the falling pieces:
REBOL [title: "Catch"]
alert "Arrow keys move left/right, up goes faster, down goes slower"
random/seed now/time
speed: 11 score: 0
view center-face layout [
size 600x440 backdrop white across
at 270x0 text "Score:" t: text bold 100 (form score)
at 280x20 y: btn 50x20 orange
at 280x420 z: btn 50x20 blue
key keycode [left] [z/offset: z/offset - 10x0 show z]
key keycode [right] [z/offset: z/offset + 10x0 show z]
key keycode [up] [speed: speed + 1]
key keycode [down] [if speed > 1 [speed: speed - 1]]
box 0x0 rate 0 feel [engage: func [f a e] [if a = 'time [
y/offset: y/offset + (as-pair 0 speed) show y
if y/offset/2 > 440 [
y/offset: as-pair (random 550) 20 show y
score: score - 1
]
if within? z/offset (y/offset - 50x0) 100x20 [
y/offset: as-pair (random 550) 20 show y
score: score + 1
]
t/text: (form score) show t
]]]
]
Your ability to create interesting animations is limited only by creative application of movement.
8.11 A Simple Animation Framework for Presentations
A simple animation framework, specifically designed to make easy work of moving and resizing GUI elements in presentations, created by Jeff Kreis, is available at http://www.cs.unm.edu/~whip/presentation.r (be sure to download the demo file at http://www.cs.unm.edu/~whip/test-prez.r). A short discussion of this tool is available on the REBOL mailing list at http://www.rebol.org/ml-display-thread.r?m=rmlBYZS
8.12 Using Animated GIF Images
Another easy way to work with animations in REBOL is with the "anim" style in GUIs. Anim takes a series of still image frames, and plays them in order as an animation with a given rate. The basic format is:
view layout [
speed: 10
anim rate (speed) [%image1.gif %image2.gif etc...]
]
The following script will convert an animated .gif into a folder filled with individual frame images:
REBOL []
gif-anim: load to-file request-file
make-dir %./frames/
count: 1
for count 1 length? gif-anim 1 [
save/png rejoin [
%./frames/ "your_file_name-" count ".png"
] pick gif-anim count
]
This next script will convert a directory of images (such as above, or any other series of images) into an embeddable block of REBOL code. It looks for all the images named [%your_file_name-1.* your_file_name-2.* etc...]:
REBOL []
system/options/binary-base: 64
file-list: read %./frames/
anim-frames-block: copy []
foreach file file-list [
; Unique portion of file names for your image frames go here.
; Leave out this check if you instead want to convert all
; files in the directory:
if find to-string file "your_file_name-" [
print file
uncompressed: read/binary file
compressed: compress to-string uncompressed
append anim-frames-block compressed
]
]
editor anim-frames-block
Here's some sample output:
anim-frames-block: [64#{
eJxz93SzsEwMZwhn+M4AAg1g3ACmGsCsBjDnPxj/B1P/waz/YM4oGAXDBij+ZAHT
OiAClCfYOf4zCHLIeGxYcLCZQ1gr5sSGhYfbBZS95nhsXHS0W8I4686JjYuP9ys4
d8l4blpycrJG8KqYk5uWnp5ukHxqjufmZWdnWxS/OsPJcODoPLtKprUcW9TPLXJT
V7LdFZIZuMx/Ll/rrJCkC3NZ1ztd6SpVCG+L363EsXpCTvhmtovzVCWurr7R6jG7
rzZarKFpd8XTS77Z1/Xu7Qn+vunr6+/v725rqv6nm/Oj4Or2Ll7jvDUOa8+e6FX3
3uYjbPz0fN/RKjbeWcU+Z5do2qfN2lWaelnXfbveKwkz7ytLqu0qBK6Xed1cyfhG
TC58xeujhyuF422FXxQeOPybbR1nzbbP18+khtXvu/H95Ns7Gzdv5ZtfaVX64fjZ
crf/d6xPvV7XmJ7PZ1/x/ueXm/nXrOfVZKyZ+DL8nt85zhWzqu8LPosvPyYZEdW8
QrJjvjdj3TOFJuXQFVEVEl0iC9L49pVJJvZcnR7XLn/w+ux64XUpizrvbF0R1PFx
4QvB3s29OxLylB9tW9Cj9+vEol5NLk+5ia7vLB74GvxbETxZRklSqI+HyWNpR7ri
VbkJtreOp05nF1O/EeGW9C01/RqjmVrF3l7PZxnfPStv12qxsjBYAwBolvDW2AQA
AA==
} 64#{
eJxz93SzsEwMZwhn+M4AAg1g3ACmGsCsBjDnPxj/B1P/waz/YM4oGAXDBij+ZAHT
OiAClCfYOf4zMHLIeGxYcLCZQ1gr5sSGhYfbBZS95nhsXHS0W8I4686JjYuP9ys4
d8l4blpycrJG8KqYk5uWnp5ukHxqjufmZWdnW6hqBUwQfnxuvkPltxaJLSsuLOTt
ZWPdIPzSaal3vZUth6nWhZUsq7NsrUqzQ9f47K17qyWmdW1T2txFsreLdW/Pydu6
rXe2mHrsYuf3j86uLn95Z1/Qf6ZnWeUGD2e38V/3WVOh9viYkfzh3Fvmb1Iap+oq
P7OUKH64ocH2tsisGfkvTy7nXi6nG/n11dGZzLv3RQt8On3c19zY7e8stbyDCxtf
h0rLZBZuKjyYFrv6jsLdZ8xr99lGi3wueRLuGN6+zqSq7MW1700y/hHle4o/PhP8
5Xt+397f3z88Pj3ff/++v79/vGdnYbAGAJfEqNM/BAAA
} 64#{
eJxz93SzsEwMZwhn+M4AAg1g3ACmGsCsBjDnPxj/B1P/waz/YM4oGAXDBij+ZAHT
OiAClCfYOf4zMHLIeGxYcLCZQ1gr5sSGhYfbBZS95nhsXHS0W8I4686JjYuP9ys4
d8l4blri2cIVNC+GU2Hp6elcEX0tnsbLfPpNs++9mTE57fRcyepfJZxfFgUsdNWU
s51l8ihoma+8XatU6cOQVaHCca6zQh+GrYvlrWOVnvbgxrzUo/POzrz2JmpuLuu+
VuntT+9ML316T3VWuf79HXX/t/GuKTJIPBj5UW7bzB0fko75frwVGzP1ffIRa934
tpiQp88O9Zq3q84pL3qwq593uZ621dus61NCJ097K/714b7l3tf1bAv03jfNmv/v
264t3wu2Hn0r9973y6uiy2aql235hJeef35hovexONmK8jc3rzapXLeL03r+6cXl
1fHn9+39/f3D49Pz/ffv+/v7x+fX98/v3////1NWFgZrALxatNdHBAAA
} 64#{
eJxz93SzsEwMZwhn+M4AAg1g3ACmGsCsBjDnPxj/B1P/waz/YM4oGAXDBij+ZAHT
OiAClCfYOf4zMHLIeGxYcLCZQ1gr5sSGhYfbBZS95nhsXHS0W8I4686JjYuP9ys4
d8l4blpycrJG8KqYk5uWnp5ukHxKZMWCZWdnSuW+urOSId11nkP+rx6JLS8C2l0n
y6XO2PLyUovvXDtTCdNXV5pCl8YtnRn68tq6qOVNX6tKdW4uT+ud5sv9RTt6Xt79
Vz3a4Stu7Cq7+OitZ/i7i3tza5n4tCo+3JzWdniTz5oI1cfHNOVXt2pWqp87VaPv
LZf1413C3s7pdmKys0rSL88PZGbbe+vzva1rY3+/PV32+sCubRtnnd0rkJdwj/0h
0wyemh2p644UC7fl7H778NGh3vO6fKbGX1/f2Jx9/9ze3d/fPzjczSvvv2/Pz88v
Lq+Oj7dTYLAGANdbpyswBAAA
} 64#{
eJxz93SzsEwMZwhn+M4AAg1g3ACmGsCsBjDnPxj/B1P/waz/YM4oGAXDBij+ZAHT
OiAClCfYOf4zMHLIeGxYcLCZQ1gr5sSGhYfbBZS95nhsXHS0W8I4686JjYuP9ys4
d8l4blpycrJG8KqYk5uWnp5ukHxqjufmZWdnWxS/unNy8/Lz8x2auWR/BTVeXOwi
Khe7y2Sl47KAiVamXApZV5b4rnWSXbVVO3RB3OF/PN7X1G9usjnfdXdl2dpz2/IK
D339VZZ3fVfZ2kdnd5uqx++t+/9tqvaMlWfXh3IrT7sZ/jHxaHim0zWtSqOnM6a9
FDtbU26cfkDPvrlNc1dm6kVTb22Lv5alaYfm5C+qu3OrNPfa+tzj13Ijv+XemZzI
zv9n+oq7Kye6f9+js2Fz5IFZx4PK+MR+JSy/sTn7/rm9u7+/f3C4m/m7pACDNQAX
yZ/iJgQAAA==
} 64#{
eJxz93SzsEwMZwhn+M4AAg1g3ACmGsCsBjDnPxj/B1P/waz/YM4oGAXDBij+ZAHT
OiAClCfYOf4zMHLIeGxYcLCZQ1gr5sSGhYfbBZS95nhsXHS0W8I4686JjYuP9ys4
d8l4blpycrJG8KqYk5uWendyiezhkdy8zHemsfm9O5LG6m7zHGqjWKRCMo7MY+h4
Z/IrYGXwMp65dq2rAl6FrGJbG3fUKuB12DrPvVqs2gFvwlelHZ/ku3qadvSilMP7
9kqW653fWvay6ezq67rxS6r/P1qjPWPDg4Nu/N+/rvyh9/iYt7zzNs0So6enpi2M
cuuRNLp3qJH/d6hNlEnY+eXS09l6w0qzLq+PPP7s98yy3N2Fp5+dvTtVN78lqf77
u5XTi3wfHpYVj5lTnX3xfsHkeDe98qrS11catc/PK7D+/u74fnNpHv19e35+fnF5
dfz5fXt/f//w+PR8//37/v5mYGJisAYARqapGj4EAAA=
} 64#{
eJxz93SzsEwMZwhn+M4AAg1g3ACmGsCsBjDnPxj/B1P/waz/YM4oGAXDBij+ZAHT
OiAClCfYOf4zMHLIeGxYcLCZQ1gr5sSGhYfbBZS95nhsXHS0W8I4686JjYuP9ys4
d8loBjYyMaj5ToqJNHjqOV0zsq/l56RnnjPlcq9t6Zy8+Nx8w+okFq8vywK6XDvl
ZGdNeR7Uyb9oUY7X55dH2INX7trCZbr62oIYSa+vv65mRDRHs05rrRR7GLU09+K+
v5LmD++sKuW/d3R2+YO4fbUn//G+MV+bsKpF9JzvnSKDx/vbhJ3DTkbo3j5coB2v
F72z4MzWubrBbLJWL25fWuZv7/d6y4q0bdMNj6udub7mzYnGuVV+v6qK8k/sl/We
l7Nb/+Ojyv5ytX0yFq/2LnRdfW3P79ef515b73/9nFRGSVPJ00c2fXwSf9685y1d
7B9ft/fu53ei/f3/5xnVtie8f33//P79wEKATeNBA4tYxoNGDrUVD5p4zF48aBZw
00h0ZGRksAYAd264o18EAAA=
} 64#{
eJxz93SzsEwMZwhn+M4AAg1g3ACmGsCsBjDnPxj/B1P/waz/YM4oGAXDBij+ZAHT
OiAClCfYOf4zMHLIeGxYcLCZQ1gr5sSGhYfbBZS95nhsXHS0W8I4686JjYuP9ys4
d8l4blpycrJG8KqYk5tUvVi5Yia1eG5edqbPtPjSnpWBy/0YDCvDvvwsXh7Q6TL5
kI1UYGbQMv65Wq2nAl6FrApd++vIrA8HmRc4smbxni59cH294d46Vu2tOQc3OzDO
cc2+ujZiZ9zjc6mvr+hFNGV+/rT31bUX9xuTTybFWllsTFzXI5uv6xO2yXe3m669
nrfIxrAzDaLqx9bc2Jx8aVZ90bWcWYZXr6xj39+W++NT4K1VuZ9LeqPfpM2cWHj8
ytmQHx/u79b9zSf3e9un5iOth/QkYnd9fHVy/fSydbWl5e8PBbYHLreJ+1Oyv1d1
cX5tVe2Li+94t/X7y9b9Wf5y4mx3u5919d/Orr1+s8jyovr9ZFYpjol1XGYvHjQL
uGk8bBEJy3jYKpG24mGbTNmLh+0KbRqPOoTYWBisAbfrxM90BAAA
} 64#{
eJxz93SzsEwMZwhn+M4AAg1g3ACmGsCsBjDnPxj/B1P/waz/YM4oGAXDBij+ZAHT
OiAClCfYOf4zMHLIeGxYcLCZQ1gr5sSGhYfbBZS95nhsXHS0W8I4686JjYuP9ys4
d8l4blpycrJG8KqYk5uWnp5ukHxqjufmZWdnWxRHhRwIfu46z6Hx1xSJLSsuLOTt
1XLdFfDy0mIfTqu5t4xfOayKWMt04NRVretrAvc3yWqVrTm/LnqlUuusba9Ct6aL
ctQ4mL+9syt3+jHWgO+Nd/fVPXxm88p8Q8y+Gl7/q5Il667sZjp7S0drqm7UHP/T
UrJ7LNc/2zFFOXudlNWyG9uzvs6yO1NgEj29V3RXH2/1tzfTthVv9lt52+zdvcXZ
zPZ/rb99OKfvLF+vu+d50Xaju3b3bSutnj+fsTx4/sra6pK3N9fed2Op/2uR/OZ5
+/pQf7GKiJ37tlb905I3LVw7s//St1W7NgW8f/l1+41qZr6O+MxvjuH3m3jMXjxo
FnDTeNgiEpbxsFUibUViGyMjgzUAhlm/D2kEAAA=
} 64#{
eJxz93SzsEwMZwhn+M4AAg1g3ACmGsCsBjDnPxj/B1P/waz/YM4oGAXDBij+ZAHT
OiAClCfYOf4zMHLIeGxYcLCZQ1gr5sSGhYfbBZS95nhsXHS0W8I4686JjYuP9ys4
d8l4blpycrJGcFnIAgdVr2kGybtEJDernZmpnfsqp9P48bn5tvr/ZKSuPApY4Koo
Fzvry8OgZb6Sdq1Sog9DZjJlh/l6mLz2ZeDfU3c3SuClwzQm+RWsC6bqOC7JOrwo
Vnv72uht1gfbeK0n6MWtKW/8pbrj2/uI7QU/F9Vmf14XMbfnolxpjWlR3GGbyXZb
a3ZufLY619b5H8+vnNRL8z7K6ciWbnG80B7Y3SZrrZF7bVN+ee6q6uKr9/ZFM8/X
qfnx7s6xYPGrs+7oPXrWzex83qes6svaa+v/n9OrtUp9fX9ve7j/ux8fP3x61rjY
vLZ6b+iNdzsPre/9l5a86itjv21cXGXk5p+Wx+fVM3K9CK15v7MtwZlL74RCAp+b
xsMWkbCMh60SaSsetsmUvXjYrtCm8ahDZVrGo06NPFEBBmsAOJHArHoEAAA=
} 64#{
eJxz93SzsEwMZwhn+M4AAg1g3ACmGsCsBjDnPxj/B1P/waz/YM4oGAXDBij+ZAHT
OiAClCfYOf4zMHLIeGxYcLCZQ1gr5sSGhYfbBZS95nhsXHS0W8I4a8uKBYvd+6Wd
i/54bFp8YjKf9yqTzk2ph6ZqxZ4S4dj87Mw00+J7IjM3Pz/Xa1v674jElecXJrom
yq3NKFbwWC4/PSiE68FB5llMay/1aJkuClobLhqyV2pa9vUp8SeZBLjL1t7czDM7
S9ViukrMlpCNYj2V5YlB03x/7/uzu3RpQqsjL5tdjYFhyIF8yfehWT82Rmz3VxXf
9rvi0+VJs8zdv8lsLYo/NK2b699pqS93r20wLu/lrTbNvbYt3/rcWmv9x5f2prb7
1VZbvHxwrPO1n94u8+IzB/XV+/VsTEpfXl5pn+9Xbf3l6b2J1cHP+6psKhc/43zk
d99Cs/qrXW17eW3Nl7Jfp1aff17zb2/Rjz8/v8uWMf1aGt/IobbiQROP2YsHzQJu
Gg9bRMIyHrZKpK142CZT9uJhu0KbxqMOlWk7Eh0YrAGyBMCKdgQAAA==
} 64#{
eJxz93SzsEwMZwhn+M4AAg1g3ACmGsCsBjDnPxj/B1P/waz/YM4oGAXDBij+ZAHT
OiAClCfYOf4zMHLIeGxYcLCZQ1gr5sSGhYfbBZS95nhsXHS0W8I4686JjYuP9ys4
d8l4blpycrJG8KqYk5uWnp5ukHxqjufmZb79XEWvrlROfnRuvn21F4tXSOOFNptu
JttVBisuzfURtJsrdfXBleWhnHFLZ5VqX18V18lnImW6JmwT/yamD1ofHG9tZbi0
TLV6ytrbOwqeHkrNCtePaiypntX7u+z9rTml7OIxWiZrbhy2kbbm45IsTDrevTDu
GM/PgptrkzWj360qefhi9nLH+b09VUa3Z62zPN+zNkLt7fVt+eK21tHf8w40Jv7S
Oxv148Pxg73y1898t4h4Pnvh9rh5c9S+XjZbH/5+757K7y/22bc716+Lzn168ln4
db/1917kfwvbOH+6/zzLD8ez7p/X9/u1/d+fiEq2+Joe3owHjRxqKx408Zi9eNAs
4KbxsEUkLONhq0SaqACDNQAYMLy/ZgQAAA==
}]
And here's an example of how to write the files in that block back to the hard drive and display them in a GUI:
; Write files:
count: 1
make-dir %./frames/
for count 1 length? anim-frames-block 1 [
write/binary rejoin [
%./frames/ "frame-" count ".gif"
] to-binary decompress pick anim-frames-block count
]
; Create file list, with frames in numerical order:
file-list: read %./frames/
animation-frames: copy []
for count 1 length? file-list 1 [
append animation-frames rejoin [
%./frames/ "frame-" count ".gif"
]
]
; Display that file list as an animation:
view layout [
anim: anim rate 10 frames animation-frames
]
Here's an example that combines the above animated GIF files with normal GUI animation (/offset values adjusted using "for" loops):
view center-face layout [
size 625x415
backcolor black
anim: anim rate 10 frames load animation-frames
btn "Run Animation" [
for counter 0 31 1 [
anim/offset: anim/offset + (as-pair counter 0)
show anim wait .05
]
for counter 0 24 1 [
anim/offset: anim/offset + (as-pair 0 counter)
show anim wait .05
]
for counter 0 31 1 [
anim/offset: anim/offset + (as-pair (negate counter) 0)
show anim wait .05
]
for counter 0 24 1 [
anim/offset: anim/offset + (as-pair 0 (negate counter))
show anim wait .05
]
]
]
8.13 And That's Just the Beginning
As you learn more about general REBOL coding throughout this text and beyond, you'll improve your ability to design GUI screens that exactly match your creative vision. If you want to create more complex visual layouts, GUI and graphic programming should be the focus of your study. For now, understanding the basic framework and concept of switching between slides, adding graphics, text, animation, sound, etc., along with specific pre-made tools like Carl's show.r script, are more than enough to put together effective presentations.
Other tools such as the REBOL Flash .swf creator, covered later in this tutorial, provide additional powerful capability for building and distributing presentations.
9. Makedoc And Other Useful REBOL Productivity Tools
9.1 Makedoc.r - HTML Document Builder
The show.r presentation script demonstrated earlier is really just a variation of another useful REBOL script called "Makedoc". Makedoc is used to create HTML (web) pages quickly and easily using a simple text markup format. Using minimal character patterns, Makedoc allows users to layout headers, subheaders, numbered lists, images, and other common page elements. Makedoc also automatically generates a hyperlinked table of contents for all topics and subtopics. Here's a short sample of the syntax:
Page Title
By: Author Name
Some descriptive info about the page.
===Main Section Header
Main section text.
---Sub Header
Sub section text.
# Automatically numbered item 1
# Automatically numbered item 2
# Automatically numbered item 3
Here's how to include an image:
=image %image.png
Save the text above as a file named example.txt, then download and run the makedoc.r script from http://www.rebol.org/library/scripts/makedoc2.r. When prompted, select the example.txt file, and Makedoc will convert it into a nicely formatted HTML page.
This entire tutorial is actually written in Makedoc format. The source text is available at http://www.re-bol.com/business_programming.txt.
More information and instructions about how to use Makedoc are available at http://www.rebol.net/docs/makedoc.html.
Makedoc has been integrated into the Sitebuilder CGI script, so you can create and edit pages on a web site using only a browser, with simple Makedoc syntax. Much more about creating and using web site CGI programs will be covered in depth in later sections of this tutorial.
9.2 An Improved Text Editor
The text "editor" function built into REBOL is very simple. It doesn't even enable undo/redo for when typing mistakes occur. The following version allows for several such useful features. After running the code below, just use the "editor" function is the normal way, and the new features will be added. Notice that the download procedure is wrapped in a block executed by the "attempt" function, just in case an Internet connection is not available:
if not exists? %e [
attempt [
write %e read http://re-bol.com/e
do e%
]
]
; editor none
Here's the program in compressed format, for use in situations where an Internet connection is not available. This script uses Romano Paolo Tenca's undo/redo code to add [ctrl]-z and [ctrl]-y features:
REBOL [Title: "Advanced Editor"]
do undo: decompress #{
789CAD58DD6EA33A10BEA64F61F55C546A85C856DA1BBAE7F441908FE4829358
4B2002A709FBF4677E6C6C2034ED9E8DD406EC99F137BF9E49D936565FAC7833
4DC5FF8ABBE4D4546D7A50975C7CDB6C3642BC88A66DB4F85B9C9ADA1C8CD595
7044AAAA72B13D35A528B6AAD412B913B315AAAE45611A818BE20149418415DB
5AED525C7BE59DBDA9B44496C434BDEEAC286BAD3ADACB8849C1527B1C788580
02447D7915A5EAB425467798C72CC6871FA2D6CDCEEE5FC55EAB2A0805909D3E
B4EF7ABE8EE2C6B75C5865EAB00B9B40209DDE3B6D977A8F2073964C1881CC74
BD9D08A2E7DA343AAD4D0F14685D5876A8169495E9ECF00A80BA932608CE5804
F0A71E0EEA48E7FF75FFEFAF7BE138E165B8179DF62F27DE49C15824E365DB76
42F7E55DD26B2B1EF6A636BBBDC56DA7973B5D646D530FC0422BDAD8BD06BEC3
1110455E293032901FDFD8A37B78E7B5DEAACEDB64649992E8A68A0DEE086414
4C183E88448C5105E025593F83A77C1627E4ABBB040DEA1DA5DF7503E164BB5A
1C6B659AA946B899F57BB3056554338862865F2CF421F74A39551D243AB74F04
9790665D5B47A7B36A93631D07F143B2CD01CC6C3C339E4313D44E87384245A5
6BAB4456B7A502046D2FECE1887FCFE42D78C845BBDD422CA4B64D1942C4F784
2CEE10246052A6F0A988C25648602B981AE9B241FCF80765C243010BB260FC0C
9F22525786559DC523BB5195D6B46844A711A481C08C1A447F5647E6F31A8AA3
DAE9F474E4EFAA3D7304A5BDF9A57D608F0B39C7103E8AD43B91A3ACDA69513C
8BC7F04A64B2D8A0DB0141EEFC098FECDE6BF52E00CCE964B67D48A5A88A70F6
2BB0FFF8943B5BCEA3ACEBDAEE152AC48065375448594C934A16CAAE1453E912
0E374CB36D193155297ADDC01E59380F8BA47FE6942DF70A20A023482D6F3ABC
5360B1C79870856BA4192F113A8C56982445618C808D99C8820CDCEB5A97D657
3DF892A14E9CDBAE8A0038AF8395391D58DCE868F02E078CFF7E149B4BFA4D8E
AC18283799918979E0B672E058B55A634E5345E7902DDE54F9334590DE37B810
12173E1DE6F394A901774C987061C204811D73F8609F009F68C86C94070BC6B0
BA64F59C93133CCE70E3490F21A25E1CC67119D3230973ECDB835E155EBCE91D
D49614614DCCA0B1775939A180CDAB4CE4128A3576DA95804CE836E0C704AAA1
B61002E4685DA5FE2643D48031EA4B92D9D5E26F002F89D33E2677F77F76C462
EFD23B848C2B668E56FA1626A20B699C704A2457FB87711374F943AAA3A53FA3
FA4241B188EE58ADCFAB02A7E8518F2B7517849DBA6654E5666576747CC984D4
A70ABC2EC3AAB737688D8B6D5B9E7A566D6B74CD0695DE321763277A5D31FBA2
0636FA8CA11B690C3D115F7245D4B951AB169D862D91A30A8F2470DEB300EDC9
DDB6EBD1403226FE8F36D7C3E323C71D550F4C37CEBD217AD948AF1EEA6F4D3F
6D500C76545C6A737C6B5557E55916E1A37924A54AB28E6F19D68B7273DB1210
3C936CFC20BEA63936F6C39DE6C01B2B0C1DE757A34E73AC2151297A9E35ACAE
25A5FA130571B112D59F4EA90F732A61A808C763FC3F5903F68A0CFC024934F6
1C91955746552EE761381C6B0787CE5746D464325652A98E67BC519D9D8E134A
46D8718CBB0D99CBF0127274F8EFA1BF81CECF95AB08613E5B4407C5D008319A
FBA360F9EA0F09B35F0F6410D3DBCE343B671C8453C496087D37CE945FBC24FE
D44540FFA56B775D478BF43006E08443F2D9C26BC8D898EF469F1FB3C5B43A5F
A6A171AC976C8DAC82AE2E32228D1EF78FF7E30F29530761C71B1B03400BF709
CDCDCAB418113EBB4107EAA7CAFA123A853AECFB1000BA0C7FD2D9F0F8488F92
47DB9831BBE49E2015C423AFCB1A82AC614DD6907B02963544B2B88CE5938980
DAB3F830A221ACDF3F84FA144853F15D5E138220B913FF08E7532077030A4555
BF6FCF3E7769C49602AF8D7E80CBF79061648807B85DB7B858DA0BDFC7B4F21F
C9601ECC1F140000
}
base-color: 230.230.255 base-effect: []
ctx-edit: mold :ctx-edit
changes: [
{style tx vtext bold 40x22 font [colors: [0.0.0 200.200.200]]}
{style tx text 40x22 font [colors: [0.0.0 170.170.170]]}
{vtext} {text}
{btn-enter} {btn} {btn-cancel} {btn} {btn green} {btn} {btn red + 50}
{btn}
{[tabs: 28 origin: 4x4]}
{[tabs: 28 origin: 4x4] with [
undo: []
colors: [254.254.254 255.255.255]
]}
{Ctrl-V - paste text}
{Ctrl-V - paste text^/^-^-Ctrl-Z - undo^/^-^-Ctrl-Y - redo}
]
foreach [original changed] changes [replace/all ctx-edit original changed]
ctx-edit: do ctx-edit
Writing/editing code is the core activity engaged in by programmers, so being able to edit directly with the REBOL interpreter is an essentially useful capability. One benefit of using the simple built in editor is that it operates on any platform supported by REBOL. It works the same way on Windows, Mac, Unix, or other operating systems which may be unfamiliar. The ability to edit scripts provides an instant environment for developing portable software on any OS. The tiny REBOL interpreter is all you need. Metapad (http://liquidninja.com/metapad/download.html) is a great third party editor that can be used for REBOL coding on Windows. Click Options -> Settings -> Primary External Viewer, and set your REBOL path (usually C:\Program Files\rebol\view\rebol.exe).
9.3 GUI Builders and Learning Tools
A primary difference between REBOL and other programming tools is that REBOL tends to require fewer lines of code to accomplish equivalent goals. The GUI dialect is particularly concise compared to other solutions. Typing "view layout [button]" does what requires a page or more of code in other environments. REBOL also has built-in help available for all language constructs. You can search for forgotten functions, look up the syntax of any function or list the content of any object using the "help" function, list all available words using the "what" function, quickly test short bits of code in the console and the text editor, and use the console's auto-complete feature to remember spellings and speed typing. Much more about these and other work enhancing features will be covered later in this tutorial.
As a result of the inherently simple workflow, most REBOL developers tend to write code using a plain text editor, instead of a bloated integrated development environment ("IDE"). It's generally faster and more effective to keep a copy of the REBOL interpreter open, and simply write REBOL code, than it is to use heavy third party GUI builders to layout interfaces, or to rely on complex IDE features to help remember and manage large APIs. One of the pleasant advantages of REBOL is that it's tiny and instantly installed on any computer. No other tools are required to develop or to distribute REBOL software.
Nevertheless, it can be helpful to have a few extraneous tools to help learn and explore the language, and to create simple visual layouts without writing code. The following examples are not industry strength applications, but may be useful for quick design and instruction, to speed presentation layout, etc.:
http://www.rebol.org/view-script.r?script=vid-build.r
http://www.rebol.org/library/scripts/layout-1.8.r
http://www.rebol.org/view-script.r?script=rebolide.r
10. Real World Concerns and Examples: Why "Programming" > Office Software
The programs in this section demonstrate practical, real world examples of how critical company specific features were added to different types of simple applications for managing inventory, payroll, and rent collections.
You've already seen that by creating GUIs, you can make input forms which enable easy input, create custom output displays, and more. Adding extended functionality such as emailing data files to others and sending data to a web server, for example, require simple one-line scripts in REBOL code. You can also create web and network applications that allow users to access data from any location, and from virtually any web connected device. A programmer's potential ability to simply save data to a web server, for example, alone adds a truly dynamic scope of power and usability beyond what's possible with office apps. With REBOL, web and mobile programming requires very little additional learning overhead and provides a wide range of capabilities that aren't so easily acquired using other tools.
But that's just the beginning. Another of the great benefits of custom built applications over spreadsheets, for example, is that they allow perfect control over data entry and display. It's easy to check data for errors using simple form validation code, before incorrect data is permanently saved, and only essential categories of values need to be displayed. You can also easily make automated backups of data, so that information is never accidently overwritten or permanently erased. You can ensure that protected fields of data are never touched during data entry, but only viewable and editable when needed. You can choose that user interfaces are cleanly laid out and labeled exactly as you see fit, and insist upon the workflow order in which data is entered. Collections of small data entry routines, reports and other elements of daily work can be collected and run from a core GUI app, data can be saved, read, and shared between each component, and other features automated behind the scenes so that users see and interact with data exactly as needed. Your ability to use code to speed use, reduce error, and perform calculations and manipulations to data are limited entirely to your own interests, abilities, and priorities. Your palette of fine data management controls and powerful tools will continue to grow as you learn more, and the potential features you can enable are limited only by your own experience, insight, and creative effort.
10.1 An Expanded Inventory Program
You've already witnessed how simple it is to create the example inventory program earlier in this tutorial:
REBOL [title: "Inventory"]
view layout [
text "SKU:"
f1: field
text "Cost:"
f2: field "1.00"
text "Quantity:"
f3: field
across
btn "Save" [
write/append %inventory.txt rejoin [
mold f1/text " " mold f2/text " " mold f3/text newline
]
alert "Saved"
]
btn "View Data" [editor %inventory.txt]
]
That application creates a data file which can be easily opened as a spreadsheet in Excel, viewed as text in Word, imported into an Access database, etc. In fact, that program and others so far in this text allow users to accomplish many of the same goals as typical office software. You can get columns of data entered by users, display tables in a visual grid, sort and perform calculations on columns/rows, create charts from the values, (and you can engineer other complex functions as needed), etc. Developing even the simplest custom GUI apps, however, enables greatly increased control over data management.
Below is an enhanced inventory program which implements a variety of specifically useful features for the environment in which it was used, as well as additional general security features, control over data entry, etc. Read the comments to see all the specialized features that were added:
REBOL [title: "Real World Inventory"]
; The script allows for manual editing of data using the "editor"
; function. The improved editor, from the previous section of this
; tutorial, is added here to enable undo/redo for when typing mistakes
; occur:
if not exists? %e [
attempt [
write %e read http://re-bol.com/e
do e%
]
]
; This is stock code that removes the word "REBOL" from the GUI header
; bar in Windows. This makes the program look more professional. You
; can copy and use these five lines in any program:
tt: "Real World Inventory"
user32.dll: load/library %user32.dll
gf: make routine![return:[int]]user32.dll"GetFocus"
sc: make routine![hw[int]a[string!]return:[int]]user32.dll"SetWindowTextA"
so: :show show: func[face][so[face]hw: gf sc hw tt]
; We can choose to automatically create and save to any desired file name
; in any location:
make-dir %/c/merchants-inventory/
datafile: %/c/merchants-inventory/merchants_mv_inventory.csv
write/append datafile ""
prcnt: 1.0
scanmode: false
roundmode: false
; This backup routine is used to automatically save incremental versions
; of the inventory file, so that no changes are ever lost:
backup-data3: func [msg] [
write (
to-file bak-file: replace form datafile "_mv_inventory" rejoin [
"_mv_inventory-backup--"
now/date "_"
replace/all form now/time ":" "-"
]
) read datafile
if msg = "msg" [alert (join "Data has been backed up to " bak-file)]
]
; These routines check that appropriate values are entered into the cost
; and quantity fields:
check-errors-f2: does [
if (
(f2/text = "0.0") or (f2/text = "") or
(error? try [to-decimal f2/text])
) [
alert {
*** ERROR: Enter a decimal in cost field
(set mode to 'key' if using keyboard).
}
focus f2 show f2
return 0
]
return 1
]
check-errors-f3: does [
if ((f3/text = "") or (error? try [to-integer f3/text])) [
alert "*** ERROR: Enter an integer in quantity field."
focus f3 show f3
return 0
]
return 1
]
; This routine saves the data, and automatically performs a backup.
; Along the way, a common default value is reset in the cost field,
; and the user is notified that the process was successful:
enter-item: does [
if check-errors-f2 = 0 [return]
if check-errors-f3 = 0 [return]
backup-data3 ""
write/append datafile rejoin [
mold f1/text " | " mold f2/text " | "
mold f3/text " | " mold f4/text " | "
mold {} " | " mold {} " | " mold {} newline
]
request/timeout/ok "Done" 00:00:01
set-face f1 ""
set-face f2 ".99"
set-face f3 ""
set-face f4 ""
focus f1
]
; This routine allows the user to manually edit the data file, if desired.
; A backup is made first, just to be safe. Along the way a total inventory
; sum is calculated and displayed, and the data file is checked for
; integrity:
view-data: does [
backup-data3 ""
total: 0
inv: read/lines datafile
if error? try [
foreach line inv [
ln: parse line "|"
line-total: (to-decimal ln/3) * (to-decimal ln/2)
total: total + line-total
]
alert join "Total Inventory: $" total
][
alert {
*** ERROR: Improperly formed data in database.
Check each line.
}
]
editor datafile
]
; The program provides an option to automatically compute a standard
; margin markup, to figure the wholesale cost of any entered item, based
; upon the scanned retail cost. These functions enable those
; computations, with appropriate data validation error checks along the
; way:
calc-percent: does [
if error? try [
if scanmode [f2/text: calculate-barcode-price f2/text show f2]
f2/text: form (prcnt * (to-decimal f2/text))
if roundmode [f2/text: form (round/to (to-decimal f2/text) .01)]
show f2
] [alert "*** ERROR: Enter a decimal in cost field."]
]
set-percent: does [
if error? try [
prcnt: to-decimal request-text/title/default
"Default Percent:" form prcnt
] [alert "*** ERROR: Enter a decimal."]
]
; The program allows for prices to either be entered by hand, or to be
; extracted from a specialized bar code format that can be entered by a
; USB scanner. This allows the user to switch entry modes:
scan-mode: does [
scanmode: not scanmode
either scanmode [
b2/text: "Mode: Scan"
] [
b2/text: "Mode: Key"
]
show b2
]
; This routine allows the user to switch between automatically rounding
; computed wholesale values above, or using exact fractional values:
round-mode: does [
roundmode: not roundmode
either roundmode [
b1/text: "Round: On"
] [
b1/text: "Round: Off"
]
show b1
]
; This routine parses values from the specially encoded barcode price
; data entered with a scanner. This bar code format is unique to the
; business is which this particular inventory is found (i.e., it could
; not simply be entered into or used directly by a spreadsheet - the
; price data needs to be extracted using a parse computation):
calculate-barcode-price: does [
scanned-price: to-integer (trim/all copy (at copy f2/text 8))
final-price: rejoin [
to-string to-integer (
(scanned-price - (scanned-price // 100)) / 100
)
"."
either (scanned-price // 100) < 10 [
rejoin ["0" scanned-price // 100]
] [
scanned-price // 100
]
]
final-price
]
; Here's the program's GUI window:
view/options center-face layout [
size 240x400
; Users can select from specialized item categories found in this
; business, using a quick list choise:
text "Item:" [
picked-item: request-list "Items" [
"1 - Food" "2 - CVS" "3 - Bread" "4 - Electronics" "5 - Coke"
"6 - Drinks" "7 - " "8 - " "9 - " "10 - "
]
f1/text: copy at picked-item (
(index? find/only picked-item " - ") + 3
)
show f1
]
; All the special routines created earlier are executed by entering
; data into fields, clicking buttons, and otherwise interacting with
; simple widgets:
f1: field "Food"
text "Cost"
f2: field ".99" [calc-percent check-errors-f2]
across
btn "%" #"^x" [set-percent]
b1: btn "Round: Off" [round-mode]
b2: btn "Mode: Key" [scan-mode]
below
text "Quantity:"
f3: field [check-errors-f3]
text "Description:"
f4: field
text 100 ""
across
btn 95 "Enter [CTRL+Z]" #"^Z" [enter-item]
btn 95 "View Data" [view-data]
do [focus f1]
] [resize]
This program enables a specialized work flow which is as fast, efficient, and error proof as possible, and it satisfies the exact specifications required to enter data in the unique environment for which it was created. The special modifications are simply additions to the generic inventory program presented at the beginning of the tutorial.
10.2 Receipt Printer
In one of this author's businesses, receipts for rent payments were initially provided using a traditional paper receipt book. This was an error prone process, paper receipts could be damaged and lost, and seaching for information on receipts later was too time consuming. A very quick solution was initially devised by copying all the fields on the paper receipts to a GUI form (similar to the "Generic Text Field Saver" example). That simple program evolved into the following script, which validates correct data entry, requests confirmation before saving, automatically saves audit trail backups, provides simple entry using GUI features such as drop-down selection lists and automatic date/time entry, and displays a nicely formatted text receipt for printing:
REBOL [title: "Cash Receipt"]
make-dir %/M/merchant/documents/
make-dir %/M/merchant/documents/cash_receipts/
make-dir %/M/merchant/documents/cash_receipts/history/
write/append %/M/merchant/documents/cash_receipts/cash_receipts.txt ""
write/append %/M/merchant/documents/cash_receipts.txt ""
view center-face layout [
across
style field field 400
text 50 right "Name: " name: field return
text 50 right "Booth: " booth: field return
text 50 right "Amount: " amount: field "$" return
text 50 right "" paytype: drop-down "Cash" "Check" "Credit" "Other"
text right 15 "#:" num: field 270 return
text 50 right "Signed: " signed: field return
text 50 right "Date" date: field 400 (form now) return
text 50 right "Note: " note: area "Rent for ..." return
indent 405 btn 50 "SAVE" [
if error? try [
to-integer booth/text to-money amount/text to-date date/text
][
alert {
ERROR: booth must be a number, amount must be valid money
amount, date must be a date/time in the default format
(1-jan-2011/12:00:00-4:00). Make sure there are no
additional spaces in the data. ENTER ANY OTHER
INFORMATION IN THE "NOTE" FIELD.
}
return
]
unless true = request "Confirm Save" [return]
backup-receipt: rejoin [
%/M/merchant/documents/cash_receipts/history/
now/date "_"
replace/all copy form now/time ":" "-"
".txt"
]
write
backup-receipt
read %/M/merchant/documents/cash_receipts/cash_receipts.txt
write/append
%/M/merchant/documents/cash_receipts/cash_receipts.txt
receipt-data: reduce [
newline mold name/text " "
mold booth/text " "
mold amount/text " "
mold paytype/text " "
mold num/text " "
mold signed/text " "
mold date/text " "
mold note/text " "
]
write/append %/M/merchant/documents/cash_receipts.txt receipt-data
cur-receipt: rejoin [
%/M/merchant/documents/cash_receipts/
name/text "_"
now/date "_"
replace/all copy form now/time ":" "-"
".txt"
]
write/append cur-receipt reduce [
newline newline newline
" M E R C H A N T S ' R E C E I P T"
newline
" ____________________________________"
newline newline newline newline
" Name: " name/text " " newline newline
" Booth: " booth/text " " newline newline
" Amount: " amount/text " " newline newline
" Pay Type: " paytype/text " " newline newline
" Number: " num/text " " newline newline
" Signed by: " signed/text " " newline newline
" Date: " date/text " " newline newline
" Note: " note/text newline newline newline newline
newline newline newline newline
" X _____________________________________________"
newline newline newline newline
" X _____________________________________________"
]
alert "This receipt is not valid until signed by both parties!"
call/show rejoin ["notepad " to-local-file cur-receipt]
]
]
Using the data saved by the program above, the script below can be used to produce a report calculating total rent collected between selected dates:
REBOL [title: "Total "]
start-date: request-date
end-date: request-date
receipts: load %/m/merchant/documents/cash_receipts/cash_receipts.txt
total: $0
foreach [name booth amount type number signed date notes] receipts [
date: to-date first parse date "/"
if ((date >= start-date) and (date <= end-date)) [
; print name print date
total: total + to-money amount
]
]
alert form total
Here is a program that displays the entire rent history for every client, prominently noting those who currently owe rent:
REBOL [title: "Rent History and Currently Due Report"]
due-dates: copy ""
total-money: 0
grand-total-monthly: 0
booths: load %booths.txt
foreach [a b c d e f g] booths [if error? try [
append due-dates rejoin ["Booth " a ", " b newline]
due-date: copy ""
parse g [
thru "[" copy due-date to "]"
]
due-date: parse due-date " "
if not empty? due-date [
if error? try [
either now/date >= (current-date: to-date first due-date) [
append due-dates rejoin [" DUE: " (form current-date)]
money-is-due: true
] [
append due-dates rejoin [" " (form current-date)]
money-is-due: false
]
] [
append due-dates " (Invalid Date)"
]
if error? try [
current-money: to-money second due-date
grand-total-monthly: grand-total-monthly + current-money
if money-is-due = true [
total-money: total-money + current-money
]
append due-dates rejoin [
newline " " (form current-money) newline
]
] [
append due-dates rejoin [
newline " (Invalid amount)" newline
]
]
]
append due-dates "-------------------------------------------------^/"
] [alert rejoin ["ERROR: booth " a " " b " " c " " d " " e " " f " " g]] ]
append due-dates rejoin [
newline newline "Total Due: " total-money
newline newline "Total Monthly Rent: " grand-total-monthly
]
make-dir %./rent_reports/
change-dir %./rent_reports/
write report-filename: to-file rejoin [
"rent_" now/date "_" (replace/all to-string now/time ":" "-") ".txt"
] due-dates
call/show rejoin ["notepad " to-local-file what-dir "\" report-filename]
Again, what started with a tiny GUI form for saving a few text fields, evolved into a full featured application set. Only a basic understanding of saving files, concatenating and splitting text, using foreach loops, testing for errors, etc., is required to create all of the code above. The report scripts can be integrated back into the main program's GUI window with code this simple (inside the view layout block):
btn "Rent Due Report" [launch %rentdue.r]
btn "Total Rent Report" [launch %rentcollected.r]
10.3 Advanced Time Clock and Automated Payroll Reports
In the same business as above, a specialized time clock machine with fingerprint validation was initially employed to manage employee payroll and work reports. The software that came with the fingerprint machine was very difficult for managers to use, and the reports it generated were awkward and inadequate for the needs of the business. In order to solve the problem, the following program was created. It simply adds features to the simple "Time Clock" app demonstrated earlier in the tutorial. This version uses an NIST clock script to set the computer's internal clock to the exact correct time (Internet connection is required). To ensure that employees never sign in for others, the program takes a photo of the person performing each signin (the code routine to capture photos is covered later in this text). To enhance security further, a full audit history of every single entry made to the signins, is saved to the hard drive, and each file is uploaded to the company's web server. This simple added feature ensures that signin times cannot be tampered with by anyone who doesn't have approved access:
REBOL [title: "Time Clock"]
insert-event-func [
either event/type = 'close [
really: request {
To restart this program you must also
restart the computer. Really close?
}
if really = true [quit]
] [event]
]
; Ladislav Mecir's nistclock.r, to ensure the computer's clock
; is set correctly:
do decompress #{
789C6D544B6FDB300CBEFB57B005861D0ADB49BA6081D7B5971D765977688162
087C506D3AD1224B811E09BC61FF7D941C278A13388E2592FAF8FA44CB5B4C0D
EA1D6A53C032819A759664459EFB4FCA32C98DCD566A77A17ABFAAF2B2692ABB
CC199270B9CA945E5D18644C8974C732D3B52D5ACD2BD566F42665D2C7924A55
C024F1B66938078D93958FEFEF33921E9C41B06B6E82DC7225A1551A41351625
299804252B042EA1514E83C14AC9DADCFC4B2017AA62028ED0D0FEAE61DD9AA4
F4F04E5A2EFC02A2408E4BB88329A97813891E41A05CD9F513D8A896B08CCE4F
4B3A249505D45A6932D41D2CA3DC34B21AB6BCDA9C431C11E838FDB64C1BCC99
88635F9A0DDF42A5B65D48630E613FA30A6807B7F4041565078BA02298C8EFF4
73FEAC76F974315F50665651B92CAE50DF78B0C832EFCDC9C02F6E42B54E5A6A
5AA5B4C6D086D443E81D13D4BE62514C62558F22D51E52B872A2EFF649E111E8
3913637D4687432F351A276CE85FBFECBDDCC10831F19DBBE2191EBE42CD9B06
357AD2F418308A3C906280BFCCEA580D187B2DAE605378A1A7FD36D0DEA6A633
16DB8092EEB98C384F5AE8B5A1039ED66F5CD66A6FBE1084755AC2AB76415E31
E3AF01185755680C117E59315BAD4B7F072D1EF93F7238B05FD019A092387C82
8F1BD412C5FD0C96C3AA00A1589D0BFEAE1991F8C320CF6A21C8C308B480966D
2867E52C97C49B70AD82FA405EAB5D65495E96A1B63E111A42D416CF7838BABF
7D41FB12CEBDD2B9DB3E9322FC1395FC27FFA32426A765E04E021378781C07D5
C774744D8EF7BF90698A66ADB4770CFB1F4ADA752CF8C6BA9FCD1BE266248CB7
DFFD9C8941B8741663C94B1842E73642F0C36C3AC94B2A45EDAA9E7121A58E02
1CD6AD0F6ED8EC29261AADC3365A867BBBA698CE046D088A44F1653FA9FB5048
3DF1ECBC60254DC23D272E1D9A98FB29E5472FA3AC26997F66F3B97F7DE4BDE0
3E9BC2B2B8C2EE72B0F894CDAE58086F4183C5D12D9236BE64E7936064D128DD
422C0A792561609FB3E00AD661268F2650F91F6BB4707323070000
}
make-dir img-dir: %./clock_photos/
unless exists? %employees [
write %employees {"Nick Antonaccio" "(Add New...)"}
]
cur-employee: copy ""
avicap32.dll: load/library %avicap32.dll
user32.dll: load/library %user32.dll
find-window-by-class: make routine! [
ClassName [string!] WindowName [integer!] return: [integer!]
] user32.dll "FindWindowA"
sendmessage: make routine! [
hWnd [integer!] val1 [integer!] val2 [integer!] val3 [integer!]
return: [integer!]
] user32.dll "SendMessageA"
sendmessage-file: make routine! [
hWnd [integer!] val1 [integer!] val2 [integer!] val3 [string!]
return: [integer!]
] user32.dll "SendMessageA"
cap: make routine! [
cap [string!] child-val1 [integer!] val2 [integer!]
val3 [integer!] width [integer!] height [integer!]
handle [integer!] val4 [integer!] return: [integer!]
] avicap32.dll "capCreateCaptureWindowA"
log-it: func [inout] [
if ((cur-employee = "") or (cur-employee = "(Add New...)")) [
alert "You must select your name." return
]
if set-system-time nist-corrected-time [nist-correction: 0:0]
cur-time: now
record: rejoin [
newline {[} mold cur-employee
{ "} mold cur-time {" "} inout { "]}
]
either true = request/confirm rejoin [
record " -- IS YOUR NAME AND THE TIME CORRECT?"
] [
make-dir %./edit_history/
write/append %time_sheet.txt ""
write rejoin [
%./edit_history/time_sheet--
"_" now/date "_"
replace/all form now/time ":" "-"
] read %time_sheet.txt
write/append %time_sheet.txt record
if error? try [
write ftp://user:pass@site.com/public_html/time_sheet.txt
read %time_sheet.txt
] [alert "Error uploading to web site (saved locally)."]
alert rejoin [
uppercase copy cur-employee ", YOU ARE " inout "."
]
] [
alert "CANCELED"
return
]
time-filename: copy replace/all copy to-string cur-time "/" "_"
time-filename: copy replace/all copy time-filename ":" "+"
img-file: rejoin [
img-dir
(replace/all copy cur-employee " " "_")
"_"
time-filename "_"
next find inout " "
".bmp"
]
sendmessage cap-result 1085 0 0
sendmessage-file cap-result 1049 0 img-file
; call to-rebol-file img-file
]
timeclock-report: does [
timeclock-start-date: request-date
timeclock-end-date: request-date
totals: copy ""
names: load %employees
log: load %time_sheet.txt
foreach name names [
if name <> "(Add New...)" [
times: copy reduce [name]
foreach record log [
if name = log-name: record/1 [
flag: none
date-time: parse record/2 "/"
log-date: to-date date-time/1
log-time: to-time first parse date-time/2 "-"
if (
(log-date >= timeclock-start-date) and
(log-date <= timeclock-end-date)
) [
previous-flag: flag
either record/3 = "CLOCKED IN " [
flag: true
] [
flag: false
]
either flag <> previous-flag [
append times log-date
append times log-time
] [
alert rejoin [
"Duplicate successive IN/OUT entry: "
name ", " record/2
]
]
]
]
]
append totals rejoin [name ":" newline]
total-hours: 0
foreach [in-date in-time out-date out-time] (at times 2) [
append totals rejoin [
newline
" in: " in-date ", " in-time
" out: " out-date ", " out-time " "
]
if error? try [
total-hours: total-hours + (out-time - in-time)
] [
alert rejoin [
"Missing login or Missing logout: " name
]
]
]
append totals rejoin [
newline newline
" TOTAL HOURS: " total-hours
newline newline newline
]
]
]
write filename: copy rejoin [
%timeclock_report-- timeclock-start-date
"_to_" timeclock-end-date ".txt"
] totals
call/show rejoin ["notepad " to-local-file filename]
]
view/new center-face layout/tight [
image 320x240
tl1: text-list 320x200 data sort load %employees [
cur-employee: value
if cur-employee = "(Add New...)" [
write/append %employees mold trim request-text/title "Name:"
tl1/data: sort load %employees show tl1
]
]
key #"^~" [
del-emp: copy to-string tl1/picked
temp-emp: sort load %employees
if true = request/confirm rejoin ["REMOVE " del-emp "?"] [
new-list: head remove/part find temp-emp del-emp 1
save %employees new-list
tl1/data: sort load %employees show tl1
alert rejoin [del-emp " removed."]
]
]
across
btn "Clock IN" [log-it "CLOCKED IN"]
btn "Clock OUT" [log-it "CLOCKED OUT"]
btn "Report" [timeclock-report]
btn "EXIT" [
sendmessage cap-result 1205 0 0
sendmessage cap-result 1035 0 0
free user32.dll quit
]
]
hwnd: find-window-by-class "REBOLWind" 0
cap-result: cap "cap" 1342177280 0 0 320 240 hwnd 0
sendmessage cap-result 1034 0 0
sendmessage cap-result 1077 1 0
sendmessage cap-result 1075 1 0
sendmessage cap-result 1074 1 0
sendmessage cap-result 1076 1 0
do-events
The reports printed by the program above are produced in an easily readable, consistent format. Paychex.com is used to process payroll for the business that uses the script above. Even though the report format is clean, entering payroll figures into the Paychex.com web interface required quite a bit of time each week. In order to speed that process, the following additional script was created. This program enables extremely fast copying and pasting directly into Paychex's custom web interface. The text reports created by the script above can be emailed, copied to the clipboard of any remote machine where the payroll manager is working, and the processed output can be entered instantly into Paychex's web site. Inputting payroll for 30+ employees takes less than a minute, using this script:
REBOL [title: "Enter Time Clock Report into Paychex"]
dec-time: func [tm] [
qq: (to-decimal second q: parse form round/to tm 00:00:60 ":") / 60
write clipboard:// form round/to ((to-decimal q/1) + qq) .01
]
data1: copy read clipboard://
data2: copy parse/all data1 "^/"
foreach line data2 [
if line = (trim copy line) [print line]
if find line "TOTAL HOURS:" [
hours: to-time (last parse line " ")
if hours <> none [
dec-time hours
ask "Paste Into Paychex, Then Press [ENTER]..."
]
]
]
halt
The script above is very simple, but such a time saving and error reducing routine would have been impossible to create without custom software coding.
Here's another little custom report script that can be used to check the entire audit history of all log-ins for any given employee, starting on any given date, to ensure that no entries have been manually changed (this can be used to compare files on the local server, or on the backup web server):
REBOL [title: "Payroll Audit History Report"]
start: ["John Smith" "18-Mar-2012/8:30:53-4:00" "CLOCKED IN "]
current: find/only (load %./time_sheet.txt) start
erased: copy []
collected: copy []
foreach file read %./edit_history/ [
if error? try [
current-period: find/only (load join %./edit_history/ file) start
if current-period <> none [
probe length? current-period
difs: difference current current-period
append collected difs
]
] [print file]
]
probe length? current
probe length? final: unique collected
ask "Done..."
editor difference current final
halt
Hopefully, the examples in this section have shed a bit more light on simple ways in which real world custom apps can help maintain data integrity, security, and critically functional customised workflow preferences that are just not possible with generic "office applications" or 1-size-fits-all third party software solutions.
11. More REBOL Language Fundamentals
This section covers a variety of topics that form a more complete understanding of the basic REBOL language.
11.1 Comments
You've already seen that text after a semicolon and before a new line is treated as a comment (ignored entirely by the interpreter). Multi-line comments can be created by enclosing text in square or curly brackets and simply not assigning a label or function to process it. Since this entire program is just a header and comments, it does nothing:
REBOL []
; this is a comment
{
This is a multi line comment.
Comments don't do anything in a program.
They just remind the programmer what's happening in the code.
}
[[
This is also a multi line comment.
alert "See... Nothing."
]]
comment [
The "comment" function can be used to clarify that the following
block does nothing, but it's not necessary.
]
11.2 Function Refinements
Many functions have "refinements" (options separated by "/"):
request-text/default "Text"
request-text/title "The /title refinement sets this header text"
request-text/title/default "Name:" "John Smith" ; 2 options together
request-text/title/offset "/offset repositions the requester" 10x100
request-pass/offset/title 10x100 "title" alert "Processing" ; 2 functions
request-file/file %temp.txt ; default file name
request-file/filter ["*.txt" "*.r"] ; only show .txt and .r files
request-file/only ; limit selection to a single file
request-file/save ; save dialog (instead of open dialog)
request-file/save/file/filter %temp.txt ["*.txt" "*.r"]
Typing "help (function)" in the REBOL interpreter console displays all available refinements for any function.
11.3 White Space and Indentation
Unlike other languages, REBOL does not require any line terminators between expressions (functions, parameters, etc.), and you can insert empty white space (tabs, spaces, newlines, etc.) as desired into code. Notice the use of indentation and comments in the code below. Notice also that the contents of the brackets are spread across multiple lines:
alert rejoin [
"You chose: " ; 1st piece of joined data
(request "Choose one:") ; 2nd piece of joined data
]
The code above works exactly the same as:
alert rejoin ["You chose: " request "Choose one:"]
ONE CAVEAT: parameters for most functions should begin on the same line as the function word. The following example will not work properly because the rejoin arguments' opening brackets need to be on the same line as the rejoin function:
alert rejoin ; This does NOT work.
[ ; Put this bracket on the line above.
"You chose: "
(request "Choose one:")
]
Blocks often contain other blocks. Such compound blocks are typically indented with consecutive tab stops. Starting and ending brackets are normally placed at the same indentation level. This is conventional in most programming languages, because it makes complex code easier to read, by grouping things visually. For example, the compound block below:
big-block: [[may june july] [[1 2 3] [[yes no] [monday tuesday friday]]]]
can be written as follows to show the beginnings and endings of blocks more clearly:
big-block: [
[may june july]
[
[1 2 3]
[
[yes no]
[monday tuesday friday]
]
]
]
probe first big-block
probe second big-block
probe first second big-block
probe second second big-block
probe first second second big-block
probe second second second big-block
Indentation is not required, but it's really helpful.
11.4 Multi Line Strings, Quotes, and Concatenation
Strings of text can be enclosed in quotes or curly brackets:
print "This is a string of text."
print {
Curly braces are used for
multi line text strings
(instead of quotes).
}
alert {To use "quotes" in a text string, put them inside curly braces.}
alert "You can use {curly braces} inside quotes."
alert "'Single quotes' can go inside double quotes..."
alert {'...or inside curly braces'}
alert {"ANY quote symbol" {can actually be used within} 'curly braces'}
alert "In many cases" alert {curly braces and quotes are interchangable.}
You can print a carriage return using the word "newline" or the characters ^/
print rejoin ["This text if followed by a carriage return." newline]
print "This text if followed by a carriage return.^/"
Clear the console screen using "newpage":
prin newpage
The "rejoin" function CONCATENATES (joins together) values:
rejoin ["Hello " "World"]
rejoin [{Concatenate } {as } {many items } {as } {you } {want.}]
rejoin [request-date { } request-color { } now/time { } $29.99]
alert rejoin ["You chose: " request "Choose one:"] ; CASCADE return values
join {"Join" only concatenates TWO items } {("rejoin" is more powerful).}
print rejoin ["This text is followed by a carriage return." newline]
print "This text is also followed by a carriage return.^/"
prin {'Prin' } prin {doesn't } prin {print } print {a carriage return.}
11.5 More About Variables
The COLON symbol assigns a value to a word label (a "variable")
x: 10
print x
x: x + 1 ; increment variable by 1 (add 1 to the current value of x)
print x
y: "hello" z: " world"
You can use the "PROBE" function to show RAW DATA assigned to a variable (PRINT formats nice output). Probe is useful for debugging problem code:
y: "hello" z: " world"
print rejoin [y z]
probe rejoin [y z]
print join y z
The "prin" function prints values next to each other (without a newline):
y: "hello" z: " world"
prin y
prin z
Variables (word labels) ARE ** NOT ** CASE SENSITIVE:
person: "john"
print person print PERSON print PeRsOn
You can cascade variable value assignments. Here, all 3 variables are set to "yes ":
value1: value2: value3: "yes "
print rejoin [value1 value2 value3]
The "ask" function gets text input from the user. You can assign that input (the return value of the ask function) directly to a variable label:
name: ask "Enter your name: "
print rejoin ["Hello " name]
You can do the same with values returned from requestor functions:
filename: request-file/only
alert rejoin ["You chose " filename]
osfile: to-local-file filename ; REBOL uses its own multiplatform syntax
to-rebol-file osfile ; Convert from native OS file notation back to REBOL
the-url: http://website.com/subfolder
split-path the-url ; "split-path" breaks any file or URL into 2 parts
11.6 Data Types
REBOL automatically knows how to perform appropriate computations on times, dates, IP addresses, coordinate values, and other common types of data:
print 3:30am + 00:07:19 ; increment time values properly
print now ; print current date and time
print now + 0:0:30 ; print 30 seconds from now
print now - 10 ; print 10 days ago
$29.99 * 5 ; perform math on money values
$29.99 / 7 ; try this with a decimal value
29.99 / 7
print 23x54 + 19x31 ; easily add coordinate pairs
22x66 * 2 ; and perform other coordiante
22x66 * 2x3 ; math
print 192.168.1.1 + 000.000.000.37 ; easily increment ip addresses
11.22.33.44 * 9 ; note that each IP segment value is limited to 255
0.250.0 / 2 ; colors are represented as tuple values with 3 segments
red / 2 ; so this is an easy way to adjust color values
view layout [image picture effect [flip]] ; apply effects to image types
x: 12 y: 33 q: 18 p: 7
(as-pair x y) + (as-pair q p) ; very common in graphics apps using coords
remove form to-money 1 / 233
remove/part form to-time (1 / 233) 6
REBOL also natively understands how to use URLs, email addresses, files/directories, money values, tuples, hash tables, sounds, and other common values in expected ways, simply by the way the data is formatted. You don't need to declare, define, or otherwise prepare such types of data as in other languages - just use them.
To determine the type of any value, use the "type?" function:
some-text: "This is a string of text" ; strings of text go between
type? some-text ; "quotes" or {curly braces}
some-text: {
This is a multi line string of text.
Strings are a native data type, delineated by
quotes or curly braces, which enclose text.
REBOL has MANY other built in data types, all
delineated by various characters and text formats.
The "type" function returns a value's data type.
Below are just a few more native data types:
}
type? some-text
an-integer: 3874904 ; integer values are just pos-
type? an-integer ; itive/negative whole numbers
a-decimal: 7348.39 ; decimal numbers are recognized
type? a-decimal ; by the decimal point
web-site: http://musiclessonz.com ; URLs are recognized by the
type? web-site ; http://, ftp://, etc.
email-address: user@website.com ; email values are in the
type? email-address ; format user@somewebsite.domain
the-file: %/c/myfile.txt ; files are preceded by the %
type? the-file ; character
bill-amount: $343.56 ; money is preceded by the $
type? bill-amount ; symbol
html-tag: <br> ; tags are places between <>
type? html-tag ; characters
binary-info: #{ddeedd} ; binary data is put between
type? binary-info ; curly braces and preceded by
; the pound symbol
image: load http://rebol.com/view/bay.jpg ; REBOL can even automatically
type? image ; recognize the data type of
; most common image formats.
a-sound: load %/c/windows/media/tada.wav ; And sounds too!
a-sound/type
color: red
type? color
color-tuple: 123.54.212
type? color-tuple
a-character: #"z"
type? a-character
a-word: 'asdf
type? a-word
Data types can be specifically "cast" (created, or assigned to different types) using "to-(type)" functions:
numbr: 4729 ; The label 'numbr now represents the integer
; 4729.
strng: to-string numbr ; The label 'strng now represents a piece of
; quoted text made up of the characters
; "4729". Try adding strng + numbr, and
; you'll get an error.
; This example creates and adds two coordinate pairs. The pairs are
; created from individual integer values, using the "to-pair" function:
x: 12 y: 33 q: 18 p: 7
pair1: to-pair rejoin [x "x" y] ; 12x33
pair2: to-pair rejoin [q "x" p] ; 18x7
print pair1 + pair2 ; 12x33 + 18x7 = 30x40
; This example builds and manipulates a time value using the "to-time"
; function:
hour: 3
minute: 45
second: 00
the-time: to-time rejoin [hour ":" minute ":" second] ; 3:45am
later-time: the-time + 3:00:15
print rejoin ["3 hours and 15 seconds after 3:45 is " later-time]
; This converts REBOL color values (tuples) to HTML colors and visa versa:
to-binary request-color
to-tuple #{00CD00}
Try this list of data type conversion examples:
to-decimal 3874904 ; Now the number contains a decimal point
to-string http://go.com ; now the web site URL is surrounded by quotes
form web-site ; "form" also converts various values to string
form $29.99
alert form $29.99 ; the alert function REQUIRES a string parameter
alert $29.99 ; (this throws an error)
5 + 6 ; you can perform math operations with integers
"5" + "6" ; (error) you can't perform math with strings
(to-integer "5") + (to-integer "6") ; this eliminates the math problem
to-pair [12 43] ; creates a coordinate pair
as-pair 12 43 ; a better way to create a coordinate pair
to-binary 123.54.212 ; convert a REBOL color value to hex color value
to-binary request-color ; convert the color chosen by the user, to hex
to-tuple #{00CD00} ; convert a hex color value to REBOL color value
form to-tuple #{00CD00} ; covert the hex color value to a string
write/binary %floorplan8.pdf debase read clipboard:// ; email attachment
REBOL has many built-in helper functions for dealing with common data types. Another way to create pair values is with the "as-pair" function. You'll see this sort of pair creation commonly to plot graphics at coordinate points on the screen:
x: 12 y: 33 q: 18 p: 7
print (as-pair x y) + (as-pair q p) ; much simpler!
Built-in network protocols, native data types, and consistent language syntax for reading, writing, and manipulating data allow you to perform common coding chores easily and intuitively in REBOL. Remember to type or paste every example into the REBOL interpreter to see how each function and language construct operates.
11.7 Random Values
You can create random values of any type:
random/seed now/time ; always use this line to get real random values
random 50 ; a random number between 0 and 50
random 50x100 ; left side is limited to 50, right limited to 100
random 222.222.222 ; each segment is limited to #s between 0 and 222
random $500
random "asdfqwerty" ; a random mix of the given characters
11.8 More About Reading, Writing, Loading, and Saving to and from Varied Sources
Thoughout this tutorial, you'll see examples of functions that read and write data to "local files", web sites, the system "clipboard", emails, and other data sources. If you're totally new to programming, a quick explanation of that topic and related terms is helpful here.
It may be familiar that in MS Windows, when you click the "My Computer" (or "Computer") icon on the desktop, you see a list of hard drives, USB flash drives, mapped network drives, and other storage devices attached to the computer:
Data on your computer's permanent hard drive is organized into a tree of folders (or "directories"), each containing individual files and additional branches of nested folders. In Windows, the root directory of your main hard drive is typically called "C:\". C is the letter name given to the disk, and "\" ("backslash") represents the root folder of the directory tree. The REBOL interpreter is installed, by default, in the folder C:\Program Files\rebol\view\
When you perform any sort of read, write, save, load, editor or other function that reads and writes data "on your computer", you are working with files on one of the "local" storage devices attached to the computer (as opposed to a folder on web server, email account, etc.). When using read and write functions, the default location for those files, in a default installation of REBOL, is "C:\Program Files\rebol\view\". The following image of a Windows file selector shows the list of files currently contained in that folder on the author's computer:
In REBOL, the percent character ("%") is used to represent local files. Because REBOL can be used on many operating systems, and because those operating systems all use different syntax to refer to drives, paths, etc., REBOL uses the universal format: %/drive/path/path/.../file.ext . For example, "C:\Program Files\rebol\view\" in Windows is referred to as "%/C/Program%20Files/rebol/view/" in REBOL code. Note that Windows uses backslashes (\), and REBOL uses forward slashes (/) to refer to folders. REBOL converts the "%" syntax to the appropriate operating system format, so that your code can be written once and used on every operating system, without alteration.
The list of files in the image above would be written in REBOL as below. Notice the forward slashes at the end of folder names:
%data/
%desktop/
%local/
%public/
%temp-examples/
%console_email.r
%e
%edit-prefs.r
%r
%read-email-header.r
%rebol.exe
%temp.txt
%user.r
%word-cats.r
%word-defs.r
%wordbrowser.r
The following 2 functions convert REBOL file format to your operating system's format, and visa versa. This is particularly useful when dealing with files that contain spaces or other characters:
print to-local-file %/C/Program%20Files/rebol/view/rebol.exe
print to-rebol-file {C:\Program Files\rebol\view\rebol.exe}
You can use the "change-dir" (or "cd") function to change folders:
change-dir %/c/ ; changes to the C:\ folder in Windows
cd %/c/ ; "cd" is a shortcut for "change-dir"
To move "up" a folder from the current directory, use "%../". For example, the following code moves from the default C:\Program Files\rebol\view\ folder, up to C:\Program Files\rebol\, and then to C:\Program Files\:
cd %../
cd %../
Refer to the current folder with "%./". You can read a listing of the files in the current folder, like this:
read %./
probe read %./
editor %./
You can make a new folder within the current folder, using the "make-dir" function. Here are some additional folder functions:
make-dir %./newfolder/
what-dir
list-dir
You can rename, copy, and delete files within a folder, using these functions:
rename %temp.txt %temp2.txt ; change file name
write %temp.txt read %temp2.txt ; copy file
delete %temp2.txt
The "write" function writes data to a file. It takes two parameters - a file name to write to, and some data to be written:
write %name.txt "John Smith"
The "read" function reads data from a file:
read %name.txt
You can assign variable labels to data that will be written to a file:
name: "John Smith"
write %name.txt name
Assign variable labels to data read from a file:
loaded-name: read %name.txt
print loaded-name
REBOL's built-in text editor can also read, write, and manipulate text data in files:
editor %name.txt
You can write data to a web site (or any other connected protocol) using the exact same write syntax that is used to write to a file. Writing to a web site "ftp" account requires a username and password:
write ftp://user:pass@website.com/folder/test.txt "Text written by REBOL"
You can read data straight from a web server, an ftp account, an email account, etc. using the same format. Many Internet protocols are built right into the REBOL interpreter. They're understood natively, and REBOL knows exactly how to connect to them without any preparation by the programmer:
editor http://rebol.com ; Reads the content of the
; document at this URL.
editor pop://user:pass@website.com ; Reads all emails in this
; POP inbox.
editor clipboard:// ; Reads data that has
; been copied/pasted to
; the OS clipboard.
print read dns://msn.com ; Displays the DNS info
; for this address.
print read nntp://public.teranews.com ; (Hit the [ESC] key to stop
; this Usenet listing.)
NOTE: The editor reads, AND allows you to SAVE EDITS back to the server:
editor ftp://user:pass@website.com/public_html/index.html
Transferring data between devices connected by any supported protocol is easy - just read and write:
; read data from a web site, and paste it into the local clipboard:
write clipboard:// (read http://rebol.com) ; afterward, try pasting into
; your favorite text editor
; read a page from one web site, and write it to another:
write ftp://user:pass@website2.com (read http://website1.com)
; again, notice that the "write" function takes TWO parameters
Sending email is just as easy, using a similar syntax:
send user@website.com "Hello"
send user@website.com (read %file.txt) ; sends an email, with
; file.txt as the body
The "/binary" modifier is used to read or write binary (non-text) data. You'll use read/binary and write/binary to read and write images, sounds, videos and other non-text files:
write/binary %/c/bay.jpg read/binary http://rebol.com/view/bay.jpg
For clarification, remember that the write function takes two parameters. The first parameter above is "%/c/bay.jpg". The second parameter is the binary data read from http://rebol.com/view/bay.jpg:
write/binary (%/c/bay.jpg) (read/binary http://rebol.com/view/bay.jpg)
The "load" and "save" functions also read and write data, but in the process, automatically format certain data types for use in REBOL. Try this:
; assign the word "picture" to the image "load"ed from a given URL:
picture: load http://rebol.com/view/bay.jpg
; save the image to a given file name, and automatically convert it
; to .png format;
save/png %/c/picture.png picture
; show it in a GUI window:
view layout [image load %/c/picture.png]
"Load" and "save" are used to conveniently manage certain types of data in formats directly usable by REBOL (blocks, images, sounds, DLLs, certain native data structures, etc. can be loaded and used immediately). You'll use "read" and "write" more commonly to store and retrieve typical types of data, exactly byte for byte, to/from a storage medium, when no conversion or formatting is necessary.
All these examples and options may currently come across as confusing. If the topic is feels daunting at the moment, simply accept this section as reference material and continue studying the next sections. You'll become much more familiar with reading and writing as you see the functions in use within real examples.
11.9 Understanding Return Values and the Order of Evaluation
In REBOL, you can put as many functions as you want on one line, and they are all evaluated strictly from left to right. Functions are grouped together automatically with their required data parameter(s). The following line contains two alert functions:
alert "First function" alert "Second function"
Rebol knows to look for one parameter after the first alert function, so it uses the next piece of data on that line as the argument for that function. Next on the line, the interpreter comes across another alert function, and uses the following text as it's data parameter.
Simple requester functions don't require any parameters, but like most functions, they RETURN a useful value. Try pasting these functions directly into the REBOL console to see their return values:
request-text
request-date
request-color
request-file
request-dir
request-pass
In the following line, the first function, with its refinements "request-pass/offset/title" requires two parameters, so REBOL uses the next two items on the line ("10x100" and "title") as its arguments. After that's complete, the interpreter comes across another "alert" function, and uses the following text, "Processing", as its argument:
request-pass/offset/title 10x100 "title" alert "Processing"
IMPORTANT: In REBOL, the return values (output) from one function can be used directly as the arguments (input) for other functions. Everything is simply evaluated from left to right. In the line below, the "alert" function takes the next thing on the line as it's input parameter, which in this case is not a piece of data, but a function which returns some data (the concatenated text returned by the "rejoin" function):
alert rejoin ["Hello " "there" "!"]
To say it another way, the value returned above by the "rejoin" function is passed to (used as a parameter by) the "alert" function. Parentheses can be used to clarify which expressions are evaluated and passed as parameters to other functions. The parenthesized line below is treated by the REBOL interpreter exactly the same as the line above - it just lets you see more clearly what data the "alert" function puts on screen:
alert ( rejoin ["Hello " "there" "!"] )
Perhaps the hardest part of getting started with REBOL is understanding the order in which functions are evaluated. The process can appear to work backwords at times. In the example below, the "editor" function takes the next thing on the line as it's input parameter, and edits that text. In order for the editor function to begin its editing operation, however, it needs a text value to be returned from the "request-text" function. The first thing the user sees when this line runs, therefore, is the text requester. That appears backwards, compared to the way it's written:
editor (request-text)
Always remember that lines of REBOL code are evaluated from left to right. If you use the return value of one function as the argument for another function, the execution of the whole line will be held up until the necessary return value is processed.
Any number of functions can be written on a single line, with return values cascaded from one function to the next:
alert ( rejoin ( ["You chose: " ( request "Choose one:" ) ] ) )
The line above is typical of common REBOL language syntax. There are three functions: "alert", "rejoin", and "request". In order for the first alert function to complete, it needs a return value from "rejoin", which in turn needs a return value from the "request" function. The first thing the user sees, therefore, is the request function. After the user responds to the request, the selected response is rejoined with the text "You chose: ", and the joined text is displayed as an alert message. Think of it as reading "display (the following text joined together ("you chose" (an answer selected by the user))). To complete the line, the user must first answer the question.
To learn REBOL, it's essential to first memorize and recognize REBOL's many built-in function words, along with the parameters they accept as input, and the values which they return as output. When you get used to reading lines of code as functions, arguments, and return values, read from left to right, the language will quickly begin to make sense.
It should be noted that in REBOL, math expressions are evaluated from left to right like all other functions. There is no "order of precedence", as in other languages (i.e., multiplication doesn't automatically get computed before addition). To force a specific order of evaluation, enclose the functions in parentheses:
print 10 + 12 / 2 ; 22 / 2 = 11
print (10 + 12) / 2 ; 22 / 2 = 11 (same as without parentheses)
print 10 + (12 / 2) ; 10 + 6 = 16 (force multiplication first)
REBOL's left to right evaluation is simple and consistent. Parentheses can be used to clarify the flow of code, if ever there's confusion.
11.10 More About Conditional Evaluations
You've already seen the "if" and "either" conditional operations. Math operators are typically used to perform conditional evaluations: = < > <> (equal, less-than, greater-than, not-equal):
if now/time > 12:00 [alert "It's after noon."]
either now/time > 8:00am [
alert "It's time to get up!"
][
alert "You can keep on sleeping."
]
11.10.1 Switch
The "switch" evaluation chooses between numerous functions to perform, based on multiple evaluations. Its syntax is:
switch/default (main value) [
(value 1) [block to execute if value 1 = main value
(value 2) [block to execute if value 2 = main value]
(value 3) [block to execute if value 3 = main value]
; etc...
] [default block of code to execute if none of the values match]
You can compare as many values as you want against the main value, and run a block of code for each matching value:
favorite-day: request-text/title "What's your favorite day of the week?"
switch/default favorite-day [
"Monday" [alert "Monday is the worst! The work week begins..."]
"Tuesday" [alert "Tuesdays and Thursdays are both ok, I guess..."]
"Wednesday" [alert "The hump day - the week is halfway over!"]
"Thursday" [alert "Tuesdays and Thursdays are both ok, I guess..."]
"Friday" [alert "Yay! TGIF!"]
"Saturday" [alert "Of course, the weekend!"]
"Sunday" [alert "Of course, the weekend!"]
] [alert "You didn't type in the name of a day!"]
11.10.2 Case
You can choose between multiple evaluations of any complexity using the "case" structure. If none of the cases evaluate to true, you can use any true value to trigger a default evaluation:
name: "john"
case [
find name "a" [print {Your name contains the letter "a"}]
find name "e" [print {Your name contains the letter "e"}]
find name "i" [print {Your name contains the letter "i"}]
find name "o" [print {Your name contains the letter "o"}]
find name "u" [print {Your name contains the letter "u"}]
true [print {Your name doesn't contain any vowels!}]
]
for i 1 100 1 [
case [
(0 = modulo i 3) and (0 = modulo i 5) [print "fizzbuzz"]
0 = modulo i 3 [print "fizz"]
0 = modulo i 5 [print "buzz"]
true [print i]
]
]
By default, the case evaluation automatically exits once a true evaluation is found (i.e., in the name example above, if the name contains more than one vowel, only the first vowel will be printed). To check all possible cases before ending the evaluation, use the /all refinement:
name: "brian"
found: false
case/all [
find name "a" [print {Your name contains the letter "a"} found: true]
find name "e" [print {Your name contains the letter "e"} found: true]
find name "i" [print {Your name contains the letter "i"} found: true]
find name "o" [print {Your name contains the letter "o"} found: true]
find name "u" [print {Your name contains the letter "u"} found: true]
found = false [print {Your name doesn't contain any vowels!}]
]
11.10.3 Multiple Conditions: "and", "or", "all", "any"
You can check for more than one condition to be true, using the "and", "or", "all", and "any" words:
; first set some initial values all to be true:
value1: value2: value3: true
; then set some additional values all to be false:
value4: value5: value6: false
; The following prints "both true", because both the first
; condition AND the second condition are true:
either ( (value1 = true) and (value2 = true) ) [
print "both true"
] [
print "not both true"
]
; The following prints "both not true", because the second
; condition is false:
either ( (value1 = true) and (value4 = true) ) [
print "both true"
] [
print "not both true"
]
; The following prints "either one OR the other is true"
; because the first condition is true:
either ( (value1 = true) or (value4 = true) ) [
print "either one OR the other is true"
] [
print "neither is true"
]
; The following prints "either one OR the other is true"
; because the second condition is true:
either ( (value4 = true) or (value1 = true) ) [
print "either one OR the other is true"
] [
print "neither is true"
]
; The following prints "either one OR the other is true"
; because both conditions are true:
either ( (value1 = true) or (value4 = true) ) [
print "either one OR the other is true"
] [
print "neither is true"
]
; The following prints "neither is true":
either ( (value4 = true) or (value5 = true) ) [
print "either one OR the other is true"
] [
print "neither is true"
]
For comparisons involving more items, you can use "any" and "all":
; The following lines both print "yes", because ALL comparisons are true.
; "All" is just shorthand for the multiple "and" evaluations:
if ((value1 = true) and (value2 = true) and (value3 = true)) [
print "yes"
]
if all [value1 = true value2 = true value3 = true] [
print "yes"
]
; The following lines both print "yes" because ANY ONE of the comparisons
; is true. "Any" is just shorthand for the multiple "or" evaluations:
if ((value1 = true) or (value4 = true) or (value5 = true)) [
print "yes"
]
if any [value1 = true value4 = true value5 = true] [
print "yes"
]
11.11 More About Loops
11.11.1 Forever
"Loop" structures provide programmatic ways to methodically repeat actions, manage program flow, and automate lengthy data processing activities. You've already seen the "foreach" loop structure. The "forever" function creates a simple repeating loop. Its syntax is:
forever [block of actions to repeat]
The following code uses a forever loop to continually check the time. It alerts the user when 60 seconds has passed. Notice the "break" function, used to stop the loop:
alarm-time: now/time + :00:60
forever [if now/time = alarm-time [alert "1 minute has passed" break]]
Here's a more interactive version using some info provided by the user. Notice how the forever loop, if evaluation, and alert arguments are indented to clarify the grouping of related parameters:
event-name: request-text/title "What do you want to be reminded of?"
seconds: to-integer request-text/title "Seconds to wait?"
alert rejoin [
"It's now " now/time ", and you'll be alerted in "
seconds " seconds."
]
alarm-time: now/time + seconds
forever [
if now/time = alarm-time [
alert rejoin [
"It's now "alarm-time ", and " seconds
" seconds have passed. It's time for: " event-name
]
break
]
]
Here's a forever loop that displays/updates the current time in a GUI:
view layout [
timer: field
button "Start" [
forever [
set-face timer now/time
wait 1
]
]
]
11.11.2 Loop
The "loop" function allows you to repeatedly evaluate a block of code, a specified number of times:
loop 50 [print "REBOL is great!"]
11.11.3 Repeat
Like "loop", the "repeat" function allows you to repeatedly evaluate a block of code, a specified number of times. It additionally allows you to specify a counter variable which is automatically incremented each time through the loop:
repeat count 50 [print rejoin ["This is loop #: " count]]
The above code does the same thing as:
count: 0
loop 50 [
count: count + 1
print rejoin ["This is loop #: " count]
]
Another way to write it would be:
for i 1 50 1 [print rejoin ["This is loop #: " i]]
11.11.4 Forall and Forskip
"Forall" loops through a block, incrementing the marked index number of the series as it loops through:
some-names: ["John" "Bill" "Tom" "Mike"]
foreach name some-names [print index? some-names] ; index doesn't change
forall some-names [print index? some-names] ; index changes
foreach name some-names [print name]
forall some-names [print first some-names] ; same effect as line above
"Forskip" works like forall, but skips through the block, jumping a periodic number of elements on each loop:
some-names: ["John" "Bill" "Tom" "Mike"]
forskip some-names 2 [print first some-names]
11.11.5 While and Until
The "while" function repeatedly evaluates a block of code while the given condition is true. While loops are formatted as follows:
while [condition] [
block of functions to be executed while the condition is true
]
This example counts to 5:
x: 1 ; create an initial counter value
while [x <= 5] [
alert to-string x
x: x + 1
]
In English, that code reads:
"x" initially equals 1.
While x is less than or equal to 5, display the value of x,
then add 1 to the value of x and repeat.
Some additional "while" loop examples:
while [not request "End the program now?"] [
alert "Select YES to end the program."
]
; "not" reverses the value of data received from
; the user (i.e., yes becomes no and visa versa)
alert "Please select today's date"
while [request-date <> now/date] [
alert rejoin ["Please select TODAY's date. It's " now/date]
]
while [request-pass <> ["username" "password"]] [
alert "The username is 'username' and the password is 'password'"
]
"Until" loops are similar to "while" loops. They do everything in a given block, repeatedly, until the last expression in the block evaluates to true:
x: 10
until [
print rejoin ["Counting down: " x]
x: x - 1
x = 0
]
11.11.6 For
The for function can loop through a series of items in a block, just like "foreach", but using a counted index. The for syntax reads like this: "Assign a (variable word) to refer to an ordinally counted sequence of numbers, begin counting on a (start number), count to an (end number), skipping by this (step number) [use the variable label to refer to each consecutive number in the count]:
REBOL []
for counter 1 10 1 [print counter]
for counter 10 1 -1 [print counter]
for counter 10 100 10 [print counter]
for counter 1 5 .5 [print counter]
halt
REBOL will properly increment any data type that it understands:
REBOL []
for timer 8:00 9:00 0:05 [print timer]
for dimes $0.00 $1.00 $0.10 [print dimes]
for date 1-dec-2005 25-jan-2006 8 [print date]
for alphabet #"a" #"z" 1 [prin alphabet]
halt
You can pick out indexed items from a list, using the incrementally counted numbers in a FOR loop. Just determine the length of the block using the "length?" function:
REBOL []
months: system/locale/months
len: length? months
for i 1 len 1 [
print rejoin [(pick months i) " is month number " i]
]
halt
This code does the exact same thing as above, using one of the alternate syntaxes instead of "pick":
REBOL []
months: system/locale/months
len: length? months
for i 1 len 1 [
print rejoin [months/:i " is month number " i]
]
halt
As you've seen, you can pick out consecutive items from a list using counter arithmetic (pick index, pick index + 1, pick index + 2). Within a for structure that uses a skip value, this concept allows you to pick out columns of data:
REBOL []
months: system/locale/months
len: length? months
for i 1 len 3 [
print rejoin [
"Months " i " to " (i + 2) " are:" newline newline
(pick months i) newline
(pick months (i + 1)) newline
(pick months (i + 2)) newline
]
]
halt
Here's an example that uses an "if" conditional evaluation to print only names that contain a requested string of text. This provides a functional search:
REBOL []
users: [
"John Smith" "123 Tomline Lane Forest Hills, NJ" "555-1234"
"Paul Thompson" "234 Georgetown Pl. Peanut Grove, AL" "555-2345"
"Jim Persee" "345 Pickles Pike Orange Grove, FL" "555-3456"
"George Jones" "456 Topforge Court Mountain Creek, CO" ""
"Tim Paulson" "" "555-5678"
]
search-text: request-text/title/default "Search for:" "t"
for i 1 length? users 3 [
if find/only (pick users i) search-text [
print rejoin [
(pick users i) newline
(pick users (i + 1)) newline
(pick users (i + 2)) newline
]
]
]
halt
To clarify the syntactic difference between "for" and "foreach" loops, here is the same program as above, written using foreach:
REBOL []
users: [
"John Smith" "123 Tomline Lane Forest Hills, NJ" "555-1234"
"Paul Thompson" "234 Georgetown Pl. Peanut Grove, AL" "555-2345"
"Jim Persee" "345 Pickles Pike Orange Grove, FL" "555-3456"
"George Jones" "456 Topforge Court Mountain Creek, CO" ""
"Tim Paulson" "" "555-5678"
]
search-text: request-text/title/default "Search for:" "t"
foreach [name address phone] users [
if find/only name search-text [
print rejoin [
name newline
address newline
phone newline
]
]
]
halt
As you can see, the for loop function is useful in almost the exact same way as foreach. It's a bit cryptic, but useful for certain common field selection algorithms which involve non-consecutive fields. You will find that both functions are used in applications of all types that deal with tabular data and lists. You'll see many use cases for each function, as this tutorial progresses - keep your eyes peeled for for and foreach.
The example below uses several loops to alert the user to feed the cat, every 6 hours between 8am and 8pm. It uses a for loop to increment the times to be alerted, a while loop to continually compare the incremented times with the current time, and a forever loop to do the same thing every day, continuously. Notice the indentation:
forever [
for timer 8:00am 8:00pm 6:00 [
while [now/time <= timer] [wait :00:01]
alert rejoin ["It's now " now/time ". Time to feed the cat."]
]
]
11.12 More About Why/How Blocks are Useful
IMPORTANT: In REBOL, blocks can contain mixed data of ANY type (text and binary items, embedded lists of items (other blocks), variables, etc.):
some-items: ["item1" "item2" "item3" "item4"]
an-image: load http://rebol.com/view/bay.jpg
append some-items an-image
; "some-items" now contains 4 text strings, and an image!
; You can save that entire block of data, INCUDING THE BINARY
; IMAGE data, to your hard drive as a SIMPLE TEXT FILE:
save/all %some-items.txt some-items
; to load it back and use it later:
some-items: load %some-items.txt
view layout [image fifth some-items]
Take a moment to examine the example above. REBOL's block structure works in a way that is dramatically easy to use compared to other languages and data management solutions (much more simply than most database systems). It's is a very flexible, simple, and powerful way to store data in code! The fact that blocks can hold all types of data using one simple syntactic structure is a fundamental reason it's easier to use than other programming languages and computing tools. You can save/load block code to the hard drive as a simple text file, send it in an email, display it in a GUI, compress it and transfer it to a web server to be downloaded by others, transfer it directly over a point-to-point network connection, or even convert it to XML, encrypt, and store parts of it in a secure multiuser database to be accessed by other programming languages, etc...
Remember, all programming, and computing in general, is essentially about storing, organizing, manipulating, and transferring data of some sort. REBOL makes working with all types of data very easy - just put any number of pieces of data, of any type, in between two brackets, and that data is automatically searchable, sortable, storable, transferable, and otherwise usable in your programs.
11.12.1 Evaluating Variables in Blocks: Compose, Reduce, Pick and More
You will often find that you want to refer to an item in a block by its index (position number), as in the earlier 'some-items' example:
view layout [image some-items/5]
You may not, however, always know the specific index number of the data item you want to access. For example, as you insert data items into a block, the index position of the last item changes (it increases). You can obtain the index number of the last item in a block simply by determining the number of items in the block (the position number of the last item in a block is always the same as the total number of items in the block). In the example below, that index number is assigned the variable word "last-item":
last-item: length? some-items
Now you can use that variable to pick out the last item:
view layout [image (pick some-items last-item)]
; In our earlier example, with 5 items in the block, the
; line above evaluates the same as:
view layout [image (pick some-items 5)]
You can refer to other items by adding and subtracting index numbers:
alert pick some-items (last-item - 4)
There are several other ways to do the exact same thing in REBOL. The "compose" function allows variables in parentheses to be evaluated and inserted as if they'd been typed explicitly into a code block:
view layout compose [image some-items/(last-item)]
; The line above appears to the interpreter as if the following
; had been typed:
view layout [image some-items/5]
The "compose" function is very useful whenever you want to refer to data at variable index positions within a block. The "reduce" function can also be used to produce the same type of evaluation. Function words in a reduced block should begin with the tick (') symbol:
view layout reduce ['image some-items/(last-item)]
Another way to use variable values explicitly is with the ":" format below. This code evaluates the same as the previous two examples:
view layout [image some-items/:last-item]
Think of the colon format above as the opposite of setting a variable. As you've seen, the colon symbol placed after a variable word sets the word to equal some value. A colon symbol placed before a variable word gets the value assigned to the variable, and inserts that value into the code as if it had been typed explicitly.
You can use the "index?" and "find" functions to determine the index position(s) of any data you're searching for in a block:
index-num: index? (find some-items "item4")
Any of the previous 4 formats can be used to select the data at the determined variable position:
print pick some-items index-num
print compose [some-items/(index-num)]
print reduce [some-items/(index-num)]
; no function words are used in the block above, so no ticks are required
print some-items/:index-num
Here's an example that displays variable image data contained in a block, using a foreach loop. The "compose" function is used to include dynamically changeable data (image representations), as if that data had been typed directly into the code:
photo1: load http://rebol.com/view/bay.jpg
photo2: load http://rebol.com/view/demos/palms.jpg
; The REBOL interpreter sees the following line as if all the code
; representing the above images had been typed directly in the block:
photo-block: compose [(photo1) (photo2)]
foreach photo photo-block [view layout [image photo]]
For additional detailed information about using blocks and series functions see http://www.rebol.com/docs/core23/rebolcore-6.html.
11.13 REBOL Strings
In REBOL, a "string" is simply a series of characters. If you have experience with other programming languages, this can be one of the sticking points in learning REBOL. REBOL's solution is actually a very powerful, easy to learn and consistent with the way other operations work in the language. Proper string management simply requires a good understanding of list functions. Take a look at the following examples to see how to do a few common operations:
the-string: "abcdefghijklmnopqrstuvwxyz"
; Left String: (get the left 7 characters of the string):
copy/part the-string 7
; Right String: (Get the right 7 characters of the string):
copy at tail the-string -7
; Mid String 1: (get 7 characters from the middle of the string,
; starting with the 12th character):
copy/part (at the-string 12) 7
; Mid String 2: (get 7 characters from the middle of the string,
; starting 7 characters back from the letter "m"):
copy/part (find the-string "m") -7
; Mid String 3: (get 7 characters from the middle of the string,
; starting 12 characters back from the letter "t"):
copy/part (skip (find the-string "t") -12) 7
; 3 different ways to get just the 7th character:
the-string/7
pick the-string 7
seventh the-string
; Change "cde" to "123"
replace the-string "cde" "123"
; Several ways to change the 7th character to "7"
change (at the-string 7) "7"
poke the-string 7 #"7" ; the pound symbol refers to a single character
poke the-string 7 (to-char "7") ; another way to use single characters
print the-string
; Remove 15 characters, starting at the 3rd position:
remove/part (at the-string 3) 15
print the-string
; Insert 15 characters, starting at the 3rd position:
insert (at the-string 3) "cdefghijklmnopq"
print the-string
; Insert 3 instances of "-+" at the beginning of the string:
insert/dup head the-string "-+ " 3
print the-string
; Replace every instance of "-+ " with " ":
replace/all the-string "-+ " " "
print the-string
; Remove spaces from a string (type "? trim" to see all its refinements!):
trim the-string
print the-string
; Get every third character from the string:
extract the-string 3
; Get the ASCII value for "c" (ASCII 99):
to-integer third the-string
; Get the character for ASCII 99 ("c"):
to-char 99
; Convert the above character value to a string value:
to-string to-char 99
; Convert any value to a string:
to-string now
to-string $2344.44
to-string to-char 99
to-string system/locale/months
; An even better way to convert values to strings:
form now
form $2344.44
form to-char 99
form system/locale/months ; convert blocks to nicely formed strings
; Covert strings to a block of characters:
the-block: copy []
foreach item the-string [append the-block item]
probe the-block
REBOL's series functions are very versatile. Often, you can devise several ways to do the same thing:
; Remove the last part of a URL:
the-url: "http://website.com/path"
clear at the-url (index? find/last the-url "/")
print the-url
; Another way to do it:
the-url: "http://website.com/path"
print copy/part the-url (length? the-url)-(length? find/last the-url "/")
(Of course, REBOL has a built-in helper function to accomplish the above goal, directly with URLs):
the-url: http://website.com/path
print first split-path the-url
There are a number of additional functions that can be used to work specifically with string series. Run the following script for an introduction:
string-funcs: [
build-tag checksum clean-path compress debase decode-cgi decompress
dehex detab dirize enbase entab import-email lowercase mold parse-xml
reform rejoin remold split-path suffix? uppercase
]
echo %string-help.txt ; "echo" saves console activity to a file
foreach word string-funcs [
print "___________________________________________________________^/"
print rejoin ["word: " uppercase to-string word] print ""
do compose [help (to-word word)]
]
echo off
editor at read %string-help.txt 4
See http://www.rebol.com/docs/dictionary.html and http://rebol.com/docs/core23/rebolcore-8.html for more information about the above functions.
12. More Essential Topics
12.1 Built-In Help and Online Resources
The "help" function displays required syntax for any REBOL function:
help print
"?" is a synonym for "help":
? print
The "what" function lists all built-in words:
what
Together, those two words provide a built-in reference guide for the entire core REBOL language. Here's a script that saves all the above documentation to a file. Give it a few seconds to run:
echo %words.txt what echo off ; "echo" saves console activity to a file
echo %help.txt
foreach line read/lines %words.txt [
word: first to-block line
print "___________________________________________________________^/"
print rejoin ["word: " uppercase to-string word] print ""
do compose [help (to-word word)]
]
echo off
editor at read %help.txt 4
You can use help to search for defined words and values, when you can't remember the exact spelling of the word. Just type a portion of the word (hitting the tab key will also show a list of words for automatic word completion):
? to- ; shows a list of all built-in type conversions
? reques ; shows a list of built-in requester functions
? "load" ; shows all words containing the characters "load"
? "?" ; shows all words containing the character "?"
Here are some more examples of ways to search for useful info using help:
? datatype! ; shows a list of built-in data types
? function! ; shows a list of built-in functions
? native! ; shows a list of native (compiled C code) functions
? char! ; shows a list of built-in control characters
? tuple! ; shows a list of built-in colors (RGB tuples)
? .gif ; shows a list of built-in .gif images
You can view the source code for built-in "mezzanine" (non-native) functions with the "source" function. There is a huge volume of REBOL code accessible right in the interpreter, and all of the mezzanine functions were created by the language's designer, Carl Sassenrath. Studying mezzanine source is a great way to learn more about advanced REBOL code patterns:
source help
source request-text
source view
source layout
source ctx-viewtop ; try this: view layout [image load ctx-viewtop/13]
The "word browser" script is a useful tool for finding, cross referencing, and learning about all the critical functions in REBOL:
write %wordbrowser.r read http://re-bol.com/wordbrowser.r
do %wordbrowser.r
12.1.1 The REBOL System Object, and Help with GUI Widgets
"Help system" displays the contents of the REBOL system object, which contains many important settings and values. You can explore each level of the system object using path notation, like this:
? system/console/history ; the current console session history
? system/options
? system/locale/months
? system/network/host-address
You can find info about all of REBOL's GUI components in "system/view/VID":
? system/view/VID
The system/view/VID block is so important, REBOL has a built-in short cut to refer to it:
? svv
You'll find a list of REBOL's GUI widgets in "svv/vid-styles". Use REBOL's "editor" function to view large system sections like this:
editor svv/vid-styles
Here's a script that neatly displays all the words in the above "svv/vid-styles" block:
foreach i svv/vid-styles [if (type? i) = word! [print i]]
Here's a more concise way to display the above widgets, using the "extract" function:
probe extract svv/vid-styles 2
This script lets you browse the object structure of each widget:
view layout [
text-list data (extract svv/vid-styles 2) [
a/text: select svv/vid-styles value
show a focus a
]
a: area 500x250
]
REBOL's GUI layout words are available in "svv/vid-words":
? svv/vid-words
The following script displays all the images in the svv/image-stock block:
b: copy []
foreach i svv/image-stock [if (type? i) = image! [append b i]]
v: copy [] foreach i b [append v reduce ['image i]]
view layout v
The changeable attributes ("facets") available to all GUI widgets are listed in "svv/facet-words":
editor svv/facet-words
Here's a script that neatly displays all the above facet words:
b: copy []
foreach i svv/facet-words [if (not function? :i) [append b to-string i]]
view layout [text-list data b]
Some GUI widgets have additional facet words available. The following script displays all such functions, and their extra attributes:
foreach i (extract svv/vid-styles 2) [
x: select svv/vid-styles i
; additional facets are held in a "words" block:
if x/words [
prin join i ": "
foreach q x/words [
if not (function? :q) [prin join q " "]
]
print ""
]
]
To examine the function(s) that handle any of the additional facets for the widgets above, type the path to the widget's "words" block, i.e.:
svv/vid-styles/TEXT-LIST/words
For more information on system/view/VID, see http://www.mail-archive.com/rebol-bounce@rebol.com/msg01898.html and http://www.rebol.org/ml-display-message.r?m=rmlHJNC.
It's important to note that you can SET any system value. Just use a colon, like when assigning variable values:
system/user/email: user@website.com
Familiarity with the system object yields many useful tools.
12.1.2 Viewtop Resources
The REBOL desktop that appears by default when you run the view.exe interpreter can be used as a gateway into a world of "Rebsites" that developers use to share useful code. Surfing the public rebsites is a great way to explore the language more deeply. All of the code in the rebol.org archive, and much more, is available on the rebsites. When typing at the interpreter console, the "desktop" function brings up the REBOL desktop (also called the "Viewtop"):
desktop
Click the "REBOL" or "Public" folders to see hundreds of interesting demos and useful examples. Source code for every example is available by right-clicking individual program icons and selecting "edit". You don't need a web browser or any other software to view the contents of Rebsites - the Viewtop and all its features are part of the REBOL executable. You can learn volumes about the REBOL language using only the resources built directly into the 600k interpreter!
For detailed, categorized, and cross-referenced information about built-in functions, see the REBOL Dictionary rebsite, found in the REBOL desktop folder REBOL->Tools (an HTML version is also available at http://www.rebol.com/docs/dictionary.html).
12.1.3 Online Documentation, The Mailing List and The AltME Community Forum
If you can't find answers to your REBOL programming questions using built-in help and resources, the first place to look is http://rebol.com/docs.html. Googling online documentation also tends to provide quick results, since the word "REBOL" is uncommon.
To ask a question directly of other REBOL developers, you can join the community mailing list by sending an email to rebol-request@rebol.com , with the word "subscribe" in the subject line. Use your normal email program, or just paste the following code into your REBOL interpreter (be sure your email account settings are set up correctly in REBOL):
send rebol-request@rebol.com "subscribe"
You can also ask questions of numerous gurus and regular users in AltME, a messaging program which makes up the most active forum of REBOL users around the world. Rebol.org maintains a searchable history of several hundred thousand posts from both the mailing list and AltME, along with a rich script archive. The REBOL user community is friendly, knowledgeable and helpful, and you will typically find answers to just about any question already in the archives. Unlike other programming communities, REBOL does not have a popular web based support forum. AltME is the primary way that REBOL developers interact. If you want to speak with others, you must download the AltME program and set up a user account (it's fast and easy to do). Just follow the instructions at http://www.rebol.org/aga-join.r.
12.2 Saving and Running REBOL Scripts
So far in this tutorial, you've been typing or copying/pasting code snippets directly into the REBOL interpreter. As you begin to work with longer examples and full programs, you'll need to save your scripts for later execution. Whenever you save a REBOL program to a text file, the code must begin with the following bit of header text:
REBOL []
That header tells the REBOL interpreter that the file contains a valid REBOL program. You can optionally document any information about the program in the header block. The "title" variable in the header block is displayed in the title bar of GUI program windows:
REBOL [
title: "My Program"
author: "Nick Antonaccio"
date: 29-sep-2009
]
view layout [text 400 center "Look at the title bar."]
The code below is a web cam video viewer program. Type in or copy/paste the complete code source below into a text editor such as Windows Notepad or REBOL's built-in text editor (type "editor none" at the REBOL console prompt). Save the text as a file called "webcam.r" on your C:\ drive.
REBOL [title: "Webcam Viewer"]
; try http://www.webcam-index.com/USA/ for more webcam links.
temp-url: "http://209.165.153.2/axis-cgi/jpg/image.cgi"
while [true] [
webcam-url: to-url request-text/title/default "Web cam URL:" temp-url
either attempt [webcam: load webcam-url] [
break
] [
either request [
"That webcam is not currently available." "Try Again" "Quit"
] [
temp-url: to-string webcam-url
] [
quit
]
]
]
resize-screen: func [size] [
webcam/size: to-pair size
window/size: (to-pair size) + 40x72
show window
]
window: layout [
across
btn "Stop" [webcam/rate: none show webcam]
btn "Start" [
webcam/rate: 0
webcam/image: load webcam-url
show webcam
]
rotary "320x240" "640x480" "160x120" [
resize-screen to-pair value
]
btn "Exit" [quit] return
webcam: image load webcam-url 320x240
with [
rate: 0
feel/engage: func [face action event][
switch action [
time [face/image: load webcam-url show face]
]
]
]
]
view center-face window
Once you've saved the webcam.r program to C:\, you can run it in any one of the following ways:
- If you've already installed REBOL on your computer, just double-click your saved ".r" script file (find the C:\webcam.r file icon in your file explorer (click My Computer -> C: -> webcam.r)). By default, during REBOL's initial installation, all files with a ".r" extension are associated with the interpreter. They can be clicked and run as if they're executable programs, just like ".exe" files. The REBOL interpreter automatically opens and executes any selected ".r" text file. This is the most common way to run REBOL scripts, and it works the same way on all major graphic operating systems. If you want other people to be able to run your scripts, just have them download and install the tiny REBOL interpreter - it only takes a few seconds.
- Use the built-in editor in REBOL. Type "editor %/c/webcam.r" at the interpreter prompt, or type "editor none" and copy/paste the script into the editor. Pressing F5 in the editor will automatically save and run the script. This is a convenient way to work with scripts, and enables REBOL to be its own simple, self contained IDE.
- Type "do %/c/webcam.r" into the REBOL interpreter.
- Scripts can be run at the command line. In Windows, copy rebol.exe and webcam.r to the same folder (C:\), then click Start -> Run, and type "C:\rebol.exe C:\webcam.r" (or open a DOS box and type the same thing). Those commands will start the REBOL interpreter and do the webcam.r code. You can also create a text file called webcam.bat, containing the text "C:\rebol.exe C:\webcam.r" . Click on the webcam.bat file in Windows, and it'll run those commands. In Unix, you can also run scripts at scheduled times with Cron. Just enter the path to the script.
- Use a program such as XpackerX to package and distribute the program. XpackerX allows you to wrap the REBOL interpreter and webcam.r program into a single executable file that has a clickable icon, and automatically runs both files. That allows you to create a single file executable Windows program that can be distributed and run like any other application. Just click it and run... (this technique is covered in the next section).
- Buy the commercial "SDK" version of REBOL, which provides the most secure method of packaging REBOL applications.
VERY IMPORTANT: To turn off the default security requester that continually asks permission to read/write the hard drive, type "secure none" in the REBOL interpreter, and then run the program with "do {filename}". Running "C:\rebol.exe -s {filename}" does the same thing . The "-s" launches the REBOL interpreter without any security features turned on, making it behave like a typical Windows program.
12.3 "Compiling" REBOL Programs - Distributing Packaged .EXE Files
The REBOL.exe interpreter is tiny and does not require any installation to operate properly. By packaging it, your REBOL script(s), and any supporting data file(s) into a single executable with an icon of your choice, XpackerX works like a REBOL 'compiler' that produces regular Windows programs that look and act just like those created by other compiled languages. To do that, you'll need to create a text file in the following format (save it as "template.xml"):
<?xml version="1.0"?>
<xpackerdefinition>
<general>
<!--shown in taskbar -->
<appname>your_program_name</appname>
<exepath>your_program_name.exe</exepath>
<showextractioninfo>false</showextractioninfo>
<!-- <iconpath>c:\icon.ico</iconpath> -->
</general>
<files>
<file>
<source>your_rebol_script.r</source>
<destination>your_rebol_script.r</destination>
</file>
<file>
<source>C:\Program Files\rebol\view\Rebol.exe</source>
<destination>rebol.exe</destination>
</file>
<!--put any other data files here -->
</files>
<!-- $FINDEXE, $TMPRUN, $WINDIR, $PROGRAMDIR, $WINSYSDIR -->
<onrun>$TMPRUN\rebol.exe -si $TMPRUN\your_rebol_script.r</onrun>
</xpackerdefinition>
Just download the free XpackerX program and alter the above template so that it contains the filenames you've given to your script(s) and file(s), and the correct path to your REBOL interpreter. Run XpackerX, and it'll spit out a beautifully packaged .exe file that requires no installation. Your users do not need to have REBOL installed to run this type of executable. To them it appears and runs just like any other native compiled Windows program. What actually happens is that every time your packaged .exe file runs, the REBOL interpreter and your script(s)/data file(s) are unzipped into a temporary folder on your computer. When your script is done running, the temporary folder is deleted.
Most modern compression (zip) applications have an "sfx" feature that allows you to create .exe packages from zip files. You can create a packaged REBOL .exe in the same way as XpackerX using just about any sfx packaging application (there are dozens of freeware zip/compression applications that can do this - use the one you're most familiar with).
The program "iexpress.exe" found on all versions of Windows (starting with Windows XP), is a popular choice for creating SFX files, because no additional software needs to be installed. Just click Start -> Run or Start -> Search Programs and Files, and type "iexpress". Follow the wizards to name the package, choose included files (the REBOL interpreter, scripts, images, etc.), and other options. Settings for any .exe you create with iexpress will be saved in a .sed file, which you can reload and make changes to later. This makes for quick re-"compilation" of updated scripts.
There is an explanation of how to use the NSIS install creator to make REBOL .exe's here. This is helpful if you want to adjust registry settings and make other changes to the computer system while installing your program.
To create a self-extracting REBOL executable for Linux, first create a .tgz file containing all the files you want to distribute (the REBOL interpreter, your script(s), any external binary files, etc.). For the purposes of this example, name that bundle "rebol_files.tgz". Next, create a text file containing the following code, and save it as "sh_commands":
#!/bin/sh
SKIP=`awk '/^__REBOL_ARCHIVE__/ { print NR + 1; exit 0; }' $0`
tail +$SKIP $0 | tar xz
exit 0
__REBOL_ARCHIVE__
Finally, use the following command to combine the above script file with the bundled .tgz file:
cat sh_commands rebol_files.tgz > rebol_program.sh
The above line will create a single executable file named "rebol_program.sh" that can be distributed and run by end users. The user will have to set the file permissions for rebol_program.sh to executable before running it ("chmod +x rebol_program.sh"), or execute it using the syntax "sh rebol_program.sh".
12.4 Common REBOL Errors, and How to Fix Them
Listed below are solutions to a variety of common errors you'll run into when first experimenting with REBOL:
1) "** Syntax Error: Script is missing a REBOL header" - Whenever you "do" a script that's saved as a file, it must contain at least a minimum required header at the top of the code. Just include the following text at the beginning of the script:
REBOL []
2) "** Syntax Error: Missing ] at end-of-script" - You'll get this error if you don't put a closing bracket at the end of a block. You'll see a similar error for unclosed parentheses and strings. The code below will give you an error, because it's missing a "]" at the end of the block:
fruits: ["apple" "orange" "pear" "grape"
print fruits
Instead it should be:
fruits: ["apple" "orange" "pear" "grape"]
print fruits
Indenting blocks helps to find and eliminate these kinds of errors.
3) "** Script Error: request expected str argument of type: string block object none" - This type of error occurs when you try to pass the wrong type of value to a function. The code below will give you an error, because REBOL automatically interprets the website variable as a URL, and the "alert" function requires a string value:
website: http://rebol.com
alert website
The code below solves the problem by converting the URL value to a string before passing it to the alert function:
website: to-string http://rebol.com
alert website
Whenever you see an error of the type "expected _____ argument of type: ___ ____ ___ ...", you need to convert your data to the appropriate type, using one of the "to-(type)" functions. Type "? to-" in the REBOL interpreter to get a list of all those functions.
4) "** Script Error: word has no value" - Miss-spellings will elicit this type of error. You'll run into it any time you try to use a word that isn't defined (either natively in the REBOL interpreter, or by you, in previous code):
wrod: "Hello world"
print word
5) If an error occurs in a "view layout" block, and the GUI becomes unresponsive, type "unview" at the interpreter command line and the broken GUI will be closed. To restart a stopped GUI, type "do-events". To break out of any endless loop, or to otherwise stop the execution of any errant code, just hit the [Esc] key on your keyboard.
6) "** User Error: Server error: tcp 550 Access denied - Invalid HELO name (See RFC2821 4.1.1.1)" and "** User Error: Server error: tcp -ERR Login failed.", among others, are errors that you'll see when trying to send and receive emails. To fix these errors, your mail server info needs to be set up in REBOL's user settings. The most common way to do that is to edit your mail account info in the graphic Viewtop or by using the "set-net" function (http://www.rebol.com/docs/words/wset-net.html). You can also set everything manually - this is how to adjust all the individual settings:
system/schemes/default/host: your.smtp.address
system/schemes/default/user: username
system/schemes/default/pass: password
system/schemes/pop/host: your.pop.address
system/user/email: your.email@site.com
7) Here's a quirk of REBOL that doesn't elicit an error, but which can cause confusing results, especially if you're familiar with other languages:
unexpected: [
empty-variable: ""
append empty-variable "*"
print empty-variable
]
do unexpected
do unexpected
do unexpected
The line:
empty-variable: ""
doesn't re-initialize the variable to an empty state. Instead, every time the block is run, "empty-variable" contains the previous value. In order to set the variable back to empty, as intended, use the word "copy" as follows:
expected: [
empty-variable: copy ""
append empty-variable "*"
print empty-variable
]
do expected
do expected
do expected
8) Load/Save, Read/Write, Mold, Reform, etc. - another point of confusion you may run into initially with REBOL has to do with various words that read, write, and format data. When saving data to a file on your hard drive, for example, you can use either of the words "save" or "write". "Save" is used to store data in a format more directly usable by REBOL. "Write" saves data in a raw, 'unREBOLized' form. "Load" and "read" share a comparable relationship. "Load" reads data in a way that is more automatically understood and put to use in REBOL code. "Read" opens data in exactly the format it's saved, byte for byte. Generally, data that is "save"d should also be "load"ed, and data that's "write"ed should be "read". For more information, see the following REBOL dictionary entries:
http://rebol.com/docs/words/wload.html
http://rebol.com/docs/words/wsave.html
http://rebol.com/docs/words/wread.html
http://rebol.com/docs/words/wwrite.html
Other built-in words such as "mold" and "reform" help you deal with text in ways that are either more human-readable or more natively readable by the REBOL interpreter. For a helpful explanation, see http://www.rebol.net/cookbook/recipes/0015.html.
9) Order of precedence - REBOL expressions are always evaluated from left to right, regardless of the operations involved. If you want specific mathematical operators to be evaluated first, they should either be enclosed in parenthesis or put first in the expression. For example, to the REBOL interpreter:
2 + 4 * 6
is the same as:
(2 + 4) * 6 ; the left side is evaluated first
== 6 * 6
== 36
This is contrary to other familiar evaluation rules. In many languages, for example, multiplication is typically handled before addition. So, the same expression:
2 + 4 * 6
is treated as:
2 + (4 * 6) ; the multiplication operator is evaluated first
== 2 + 24
== 26
Just remember, evaluation is always left to right, without exception.
10) You may run into problems when copying/pasting interactive console scripts directly into the REBOL interpreter, especially when the code contains functions such as "ask", which require a response from the user before the remainder of the script is evaluated (each line of the script simply runs, as the pasting operation completes, without any response from the user, leaving necessary variables unassigned). To fix such interactivity problems when copying/pasting console code into the interpreter, simply wrap the entire script in square brackets and then "do" that block: do [...your full script code...]. This will force the entire script to be loaded before any of the code is evaluated. If you want to run the code several times, simply assign it a word label, and then run the word label as many times as needed: do x: [...your full script code...] do x do x do x .... This saves you from having to paste the code more than once. Another effective option, especially with large scripts, is to run the code from the clipboard using "do read clipboard://". This performs much faster than watching large amounts of text paste into the console.
12.4.1 Trapping Errors
There are several simple ways to keep your program from crashing when an error occurs. The words "error?" and "try" together provide a way to check for and handle expected error situations. For example, if no Internet connection is available, the code below will crash abruptly with an error:
html: read http://rebol.com
The adjusted code below will handle the error more gracefully:
if error? try [html: read http://rebol.com] [
alert "Unavailable."
]
The word "attempt" is an alternative to the "error? try" routine. It returns the evaluated contents of a given block if it succeeds. Otherwise it returns "none":
if not attempt [html: read http://rebol.com] [
alert "Unavailable."
]
To clarify, "error? try [block]" evaluates to true if the block produces an error, and "attempt [block]" evaluates to false if the block produces an error.
For a complete explanation of REBOL error codes, see: http://www.rebol.com/docs/core23/rebolcore-17.html.
13. Creating Web Applications using REBOL CGI
The Internet provides a fantastic scope of added potential capability to business applications. Network connectivity allows multiple users to share data easily, anywhere in the world. The "web page" interface allows users to input data and view computed output using virtually any Internet connected device (computers, phones, tablets, etc.). The "web page" paradigm and generalized workflow is already familiar to an overwhelming majority of technically savvy users.
In CGI web applications, HTML forms on a web site act as the user interface (GUI) for scripts that run on a web server. Users typically type text into fields, select choices from drop down lists, click check boxes, and otherwise enter data into form "widgets" on a web page, and then click a Submit button when done. The submitted data is transferred to, and processed by, a script that you've stored at a specified URL (Internet address) on your web server. Data output from the script is then sent back to the user's browser and displayed on screen as a dynamically created web page. CGI programs of that sort, running on web sites, are among the most common types of computer application in contemporary use. PHP, Python, Java, PERL, and ASP are popular languages used to accomplish similar Internet programming tasks, but if you know REBOL, you don't need to learn them. REBOL's CGI interface makes Internet programming very easy.
In order to create REBOL CGI programs, you need an available web server. A web server is a computer attached to the Internet, which constantly runs a program that stores and sends out web page text and data, when requested from an Internet browser running on another computer. The most popular web serving application is Apache. Most small web sites are typically run on shared web server hosting accounts, rented from a data center for a few dollars per month (see http://www.lunarpages.com - they're REBOL friendly). While setting up a web server account, you can register an available domain name (i.e, www.yourwebsitename.com). When web site visitors type your ".com" domain address into their browser, they see files that you've created and saved into a publicly accessible file folder on your web server computer. Web hosting companies typically provide a collection of web based software tools that allow you to upload, view, edit, and manage files, folders, email, and other resources on your server. The most popular of these tools is cPanel. No matter what language you use to program web apps, registering a domain, purchasing hosted space, and learning to manage files and resources with tools such as cPanel, is a required starting point. Lunarpages and HostGator are two recommended hosts for REBOL CGI scripting. Full powered web site hosting, with a preconfigured cPanel interface, unlimited space and bandwidth, and all the tools needed to create professional web sites and apps, are available for less than $10 per month.
In order for REBOL CGI scripts to run, the REBOL interpreter must be installed on your web server. To do that, download from rebol.com the correct version of the REBOL interpreter for the operating system on which your web server runs (most often some type of Linux). Upload it to your user path on your web server, and set the permissions to allow it to be executed (typically "755"). Ask your web site host if you don't understand what that means. http://rebol.com/docs/cgi1.html#section-2.2 has some basic information about how to install REBOL on your server. If you don't have an online web server account, you can download a full featured free Apache web server package that will run on your local Windows PC, from http://www.uniformserver.com.
13.1 An HTML Crash Course
In order to create any sort of CGI application, you need to understand a bit about HTML. HTML is the layout language used to format text and GUI elements on all web pages. HTML is not a programming language - it doesn't have facilities to process or manipulate data. It's simply a markup format that allows you to shape the visual appearance of text, images, and other items on pages viewed in a browser.
In HTML, items on a web page are enclosed between starting and ending "tags":
<STARTING TAG>Some item to be included on a web page</ENDING TAG>
There are tags to effect the layout in every possible way. To bold some text, for example, surround it in opening and closing "strong" tags:
<STRONG>some bolded text</STRONG>
The code above appears on a web page as: some bolded text.
To italicize text, surround it in < i > and < / i > tags:
<i>some italicized text</i>
That appears on a web page as: some italicized text.
To create a table with three rows of data, do the following:
<TABLE border=1>
<TR><TD>First Row</TD></TR>
<TR><TD>Second Row</TD></TR>
<TR><TD>Third Row</TD></TR>
</TABLE>
Notice that every
<opening tag>
in HTML code is followed by a corresponding
</closing tag>
Some tags surround all of the page, some tags surround portions of the page, and they're often nested inside one another to create more complex designs.
A minimal format to create a web page is shown below. Notice that the title is nested between "head" tags, and the entire document is nested within "HTML" tags. The page content seen by the user is surrounded by "body" tags:
<HTML>
<HEAD>
<TITLE>Page title</TITLE>
</HEAD>
<BODY>
A bunch of text and <i>HTML formatting</i> goes here...
</BODY>
</HTML>
If you save the above code to a text file called "yourpage.html", upload it to your web server, and surf to http://yourwebserver.com/yourpage.html , you'll see in your browser a page entitled "Page title", with the text "A bunch of text and HTML formatting goes here...". All web pages work that way - this tutorial is in fact just an HTML document stored on the author's web server account. Click View -> Source in your browser, and you'll see the HTML tags that were used to format this document.
13.1.1 HTML Forms and Server Scripts - the Basic CGI Model
The following HTML example contains a "form" tag inside the standard HTML head and body layout. Inside the form tags are a text input field tag, and a submit button tag:
<HTML>
<HEAD><TITLE>Data Entry Form</TITLE></HEAD>
<BODY>
<FORM ACTION="http://yourwebserver.com/your_rebol_script.cgi">
<INPUT TYPE="TEXT" NAME="username" SIZE="25">
<INPUT TYPE="SUBMIT" NAME="Submit" VALUE="Submit">
</FORM>
</BODY>
</HTML>
Forms can contain tags for a variety of input types: multi-line text areas, drop down selection boxes, check boxes, etc. See http://www.w3schools.com/html/html_forms.asp for more information about form tags.
You can use the data entered into any form by pointing the action address to the URL at which a specific REBOL script is located. For example, 'FORM ACTION="http://yourwebserver.com/your_rebol_script.cgi"' in the above form could point to the URL of the following CGI script, which is saved as a text file on your web server. When a web site visitor clicks the submit button in the above form, the data is sent to the following program, which in turn does some processing, and prints output directly to the user's web browser. NOTE: Remember that in REBOL curly brackets are the same as quotes. Curly brackets are used in all the following examples, because they allow for multiline content and they help improve readability by clearly showing where strings begin and end:
#!/home/your_user_path/rebol/rebol -cs
REBOL []
print {content-type: text/html^/}
; the line above is the same as: print "content-type: text/html^/"
submitted: decode-cgi system/options/cgi/query-string
print {<HTML><HEAD><TITLE>Page title</TITLE></HEAD><BODY>}
print rejoin [{Hello } second submitted {!}]
print {</BODY></HTML>}
In order for the above code to actually run on your web server, a working REBOL interpreter must be installed in the path designated by "/home/your_user_path/rebol/rebol -cs".
The first 4 lines of the above script are basically stock code. Include them at the top of every REBOL CGI script. Notice the "decode-cgi" line - it's the key to retrieving data submitted by HTML forms. In the code above, the decoded data is assigned the variable name "submitted". The submitted form data can be manipulated however desired, and output is then returned to the user via the "print" function. That's important to understand: all data "print"ed by a REBOL CGI program appears directly in the user's web browser (i.e., to the web visitor who entered data into the HTML form). The printed data is typically laid out with HTML formatting, so that it appears as a nicely formed web page in the user's browser.
Any normal REBOL code can be included in a CGI script - you can perform any type of data storage, retrieval, organization, and manipulation that can occur in any other REBOL program. The CGI interface just allows your REBOL code to run online on your web server, and for data to be input/output via web pages which are also stored on the web server, accessible by any visitor's browser.
13.2 A Standard CGI Template to Memorize
Most short CGI programs typically print an initial HTML form to obtain data from the user. In the initial printed form, the action address typically points back to the same URL address as the script itself. The script examines the submitted data, and if it's empty (i.e., no data has been submitted), the program prints the initial HTML form. Otherwise, it manipulates the submitted data in way(s) you choose and then prints some output to the user's web browser in the form of a new HTML page. Here's a basic example of that process, using the code above:
#!/home/your_user_path/rebol/rebol -cs
REBOL []
print {content-type: text/html^/}
submitted: decode-cgi system/options/cgi/query-string
; The 4 lines above are the standard REBOL CGI headers.
; The line below prints the standard HTML, head and body
; tags to the visitor's browser:
print {<HTML><HEAD><TITLE>Page title</TITLE></HEAD><BODY>}
; Next, determine if any data has been submitted.
; Print the initial form if empty. Otherwise, process
; and print out some HTML using the submitted data.
; Finally, print the standard closing "body" and "html"
; tags, which were opened above:
either empty? submitted [
print {
<FORM ACTION="http://yourwebserver.com/this_rebol_script.cgi">
<INPUT TYPE="TEXT" NAME="username" SIZE="25">
<INPUT TYPE="SUBMIT" NAME="Submit" VALUE="Submit">
</FORM>
</BODY></HTML>
}
] [
print rejoin [{Hello } second submitted {!}]
print {</BODY></HTML>}
]
Using the above standard outline, you can include any required HTML form(s), along with all executable code and data required to make a complete CGI program, all in one script file. Memorize it.
14. Example CGI Applications
14.1 Form Mail
Here's a REBOL CGI form-mail program that prints an initial form, then sends an email to a given address containing the user-submitted data:
#!/home/youruserpath/rebol/rebol -cs
REBOL []
print {content-type: text/html^/}
submitted: decode-cgi system/options/cgi/query-string
; the following account info is required to send email:
set-net [from_address@website.com smtp.website.com]
; print a more complicated HTML header:
print read %template_header.html
; if some form data has been submitted to the script:
if not empty? submitted [
sent-message: rejoin [
newline {INFO SUBMITTED BY WEB FORM} newline newline
{Time Stamp: } (now + 3:00) newline
{Name: } submitted/2 newline
{Email: } submitted/4 newline
{Message: } submitted/6 newline
]
send/subject to_address@website.com sent-message "FORM SUBMISSION"
html: copy {}
foreach [var value] submitted [
repend html [<TR><TD> mold var </TD><TD> mold value </TD></TR>]
]
print {<font size=5>Thank You!</font> <br><br>
The following information has been sent: <BR><BR>}
print rejoin [{Time Stamp: } now + 3:00]
print {<BR><BR><table>}
print html
print {</table>}
; print a more complicated HTML footer:
print read %template_footer.html
quit
]
; if no form data has been submitted, print the initial form:
print {
<CENTER><TABLE><TR><TD>
<BR><strong>Please enter your info below:</strong><BR><BR>
<FORM ACTION="http://yourwebserver.com/this_rebol_script.cgi">
Name: <BR> <INPUT TYPE="TEXT" NAME="name"><BR><BR>
Email: <BR> <INPUT TYPE="TEXT" NAME="email"><BR><BR>
Message: <BR>
<TEXTAREA cols=75 name=message rows=5></TEXTAREA> <BR><BR>
<INPUT TYPE="SUBMIT" NAME="Submit" VALUE="Submit">
</FORM>
</TD></TR></TABLE></CENTER>
}
print read %template_footer.html
The template_header.html file used in the above example can include the standard required HTML outline, along with any formatting tags and static content that you'd like, in order to present a nicely designed page. A basic layout may include something similar to the following:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<HTML><HEAD><TITLE>Page Title</TITLE>
<META http-equiv=Content-Type content="text/html;
charset=windows-1252">
</HEAD>
<BODY bgColor=#000000>
<TABLE align=center background="" border=0
cellPadding=20 cellSpacing=2 height="100%" width="85%">
<TR>
<TD background="" bgColor=white vAlign=top>
The footer closes any tables or tags opened in the header, and may include any static content that appears after the CGI script (copyright info, logos, etc.):
</TD>
</TR>
</TABLE>
<TABLE align=center background="" border=0
cellPadding=20 cellSpacing=2 width="95%">
<TR>
<TD background="" cellPadding=2 bgColor=#000000 height=5>
<P align=center><FONT color=white size=1>Copyright © 2009
Yoursite.com. All rights reserved.</FONT>
</P>
</TD>
</TR>
</TABLE>
</BODY>
</HTML>
14.2 A Generic Drop Down List Application
The following example demonstrates how to automatically build lists of days, months, times, and data read from a file, using dynamic loops (foreach, for, etc.). The items are selectable from drop down lists in the printed HTML form:
#!/home/youruserpath/rebol/rebol -cs
REBOL []
print {content-type: text/html^/}
submitted: decode-cgi system/options/cgi/query-string
print {<HTML><HEAD><TITLE>Dropdown Lists</TITLE></HEAD><BODY>}
if not empty? submitted [
print rejoin [{NAME SELECTED: } submitted/2 {<BR><BR>}]
selected: rejoin [
{TIME/DATE SELECTED: }
submitted/4 { } submitted/6 {, } submitted/8
]
print selected
quit
]
; If no data has been submitted, print the initial form:
print {<FORM ACTION="http://yourwebserver.com/your_rebol_script.cgi">
SELECT A NAME: <BR> <BR>}
names: read/lines %users.txt
print {<select NAME="names">}
foreach name names [prin rejoin [{<option>} name]]
print {</option> </select> <br> <br>}
print { SELECT A DATE AND TIME: }
print rejoin [{(today's date is } now/date {)} <BR><BR>]
print {<select NAME="month">}
foreach m system/locale/months [prin rejoin [{<option>} m]]
print {</option> </select>}
print {<select NAME="date">}
for daysinmonth 1 31 1 [prin rejoin [{<option>} daysinmonth]]
print {</option> </select>}
print {<select NAME="time">
for time 10:00am 12:30pm :30 [prin rejoin [{<option>} time]]
for time 1:00 10:00 :30 [prin rejoin [{<option>} time]]
print {</option> </select> <br> <br>}
print {<INPUT TYPE="SUBMIT" NAME="Submit" VALUE="Submit"></FORM>}
The "users.txt" file used in the above example may look something like this:
nick
john
jim
bob
14.3 Photo Album
Here's a simple CGI program that displays all photos in the current folder on a web site, using a foreach loop:
#! /home/path/public_html/rebol/rebol -cs
REBOL [title: "Photo Viewer"]
print {content-type: text/html^/}
print {<HTML><HEAD><TITLE>Photos</TITLE></HEAD><BODY>}
print read %template_header.html
folder: read %.
count: 0
foreach file folder [
foreach ext [".jpg" ".gif" ".png" ".bmp"] [
if find file ext [
print [<BR> <CENTER>]
print rejoin [{<img src="} file {">}]
print [</CENTER>]
count: count + 1
]
]
]
print {<BR>}
print rejoin [{Total Images: } count]
print read %template_footer.html
Notice that there's no "submitted: decode-cgi system/options/cgi/query-string" code in the above example. That's because the above script doesn't make use of any data submitted from a form.
14.4 Simple Interactive REBOL Web Site Console
Here's a simple but powerful script that allows you to type REBOL code into an HTML text area, and have that code execute directly on your web server. The results of the code are then displayed in your browser. This essentially functions as a remote console for the REBOL interpreter on your server. You can use it to run REBOL code, or to call shell programs directly on your web site - very powerful! DO NOT run this on your web server if you're concerned at all about security!:
#! /home/path/public_html/rebol/rebol276 -cs
REBOL [Title: "CGI Remote Console"]
print {content-type: text/html^/}
print {<HTML><HEAD><TITLE>Console</TITLE></HEAD><BODY>}
submitted: decode-cgi system/options/cgi/query-string
; If no data has been submitted, print form to request user/pass:
if ((submitted/2 = none) or (submitted/4 = none)) [
print {
<STRONG>W A R N I N G - Private Server, Login Required:</STRONG>
<BR><BR>
<FORM ACTION="./console.cgi">
Username: <INPUT TYPE=text SIZE="50" NAME="name"><BR><BR>
Password: <INPUT TYPE=text SIZE="50" NAME="pass"><BR><BR>
<INPUT TYPE="SUBMIT" NAME="Submit" VALUE="Submit">
</FORM>
}
quit
]
; If code has been submitted, print the output:
entry-form: [
print {
<CENTER><FORM METHOD="get" ACTION="./console.cgi">
<INPUT TYPE=hidden NAME=submit_confirm VALUE="command-submitted">
<TEXTAREA COLS="100" ROWS="10" NAME="contents"></TEXTAREA><BR><BR>
<INPUT TYPE="SUBMIT" NAME="Submit" VALUE="Submit">
</FORM></CENTER></BODY></HTML>
}
]
if submitted/2 = "command-submitted" [
write %commands.txt join "REBOL[]^/" submitted/4
; The "call" function requires REBOL version 2.76:
call/output/error
{/home/path/public_html/rebol/rebol276 -qs commands.txt}
%conso.txt %conse.txt
do entry-form
print rejoin [
{<CENTER>Output: <BR><BR>}
{<TABLE WIDTH=80% BORDER="1" CELLPADDING="10"><TR><TD><PRE>}
read %conso.txt
{</PRE></TD></TR></TABLE><BR><BR>}
{Errors: <BR><BR>}
read %conse.txt
{</CENTER>}
]
quit
]
; Otherwise, check submitted user/pass, then print form for code entry:
username: submitted/2 password: submitted/4
either (username = "user") and (password = "pass") [
; if user/pass is ok, go on
][
print "Incorrect Username/Password." quit
]
do entry-form
Upload the script to your server, rename it "console.cgi", set it to executable, and change the path to your REBOL interpreter (2 places in the script). Then try running the following example code:
print 352 + 836
? system/locale/months
call "ls -al"
14.5 Attendance
Here's an example that allows users to check attendance at various weekly events, and add/remove their names from each of the events. It stores all the user information in a flat file (simple text file) named "jams.db":
#! /home/path/public_html/rebol/rebol -cs
REBOL [title: "event.cgi"]
print {content-type: text/html^/}
print {<HTML><HEAD><TITLE>Event Sign-Up</TITLE></HEAD><BODY>}
jams: load %jam.db
a-line: [] loop 65 [append a-line "-"]
a-line: trim to-string a-line
print {
<hr> <font size=5>" Sign up for an event:"</font> <hr><BR>
<FORM ACTION="http://yourwebsite.com/cgi-bin/event.cgi">
Student Name:
<input type=text size="50" name="student"><BR><BR>
ADD yourself to this event: "
<select NAME="add"><option>""<option>"all"
}
foreach jam jams [prin rejoin [{<option>} jam/1]]
print {
</option> </select> <BR> <BR>
REMOVE yourself from this event:
<select NAME="remove"><option>""<option>"all"
}
foreach jam jams [prin rejoin [{<option>} jam/1]]
print {
</option> </select> <BR> <BR>
<INPUT TYPE="SUBMIT" NAME="Submit" VALUE="Submit">
</FORM>
}
print-all: does [
print [<br><hr><font size=5>]
print " Currently scheduled events, and current attendance:"
print [</font><br>]
foreach jam jams [
print rejoin [a-line {<BR>} jam/1 {BR} a-line {<BR>}]
for person 2 (length? jam) 1 [
print jam/:person
print {<BR>}
]
print {<BR>}
]
print {</BODY></HTML>}
]
submitted: decode-cgi system/options/cgi/query-string
if submitted/2 <> none [
if ((submitted/4 = "") and (submitted/6 = "")) [
print {
<strong> Please try again. You must choose an event.</strong>
}
print-all
quit
]
if ((submitted/4 <> "") and (submitted/6 <> "")) [
print {
<strong> Please try again. Choose add OR remove.</strong>
}
print-all
quit
]
if submitted/4 = "all" [
foreach jam jams [append jam submitted/2]
save %jam.db jams
print {
<strong> Your name has been added to every event:</strong>
}
print-all
quit
]
if submitted/6 = "all" [
foreach jam jams [
if find jam submitted/2 [
remove-each name jam [name = submitted/2]
save %jam.db jams
]
]
print {
<strong> Your name has been removed from all events:</strong>
}
print-all
quit
]
foreach jam jams [
if (find jam submitted/4) [
append jam submitted/2
save %jam.db jams
print {
<strong> Your name has been added to the selected event:
</strong>
}
print-all
quit
]
]
found: false
foreach jam jams [
if (find jam submitted/6) [
if (find jam submitted/2) [
remove-each name jam [name = submitted/2]
save %jam.db jams
print {
<strong>
Your name has been removed from the selected event:
</strong>
}
print-all
quit
found: true
]
]
]
if found <> true [
print {
<strong> That name is not found in the specified event!"
</strong>
}
print-all
quit
]
]
print-all
Here is a sample of the "jam.db" data file used in the above example:
["Sunday September 16, 4:00 pm"
"Nick Antonaccio" "Ryan Gaughan" "Mark Carson"]
["Sunday September 23, 4:00 pm"
"Nick Antonaccio" "Ryan Gaughan" "Mark Carson"]
["Sunday September 30, 4:00 pm"
"Nick Antonaccio" "Ryan Gaughan" "Mark Carson"]
14.6 Bulletin Board
Here's a simple web site bulletin board program:
#! /home/path/public_html/rebol/rebol -cs
REBOL [title: "Jam"]
print {content-type: text/html^/}
print read %template_header.html
; print {<HTML><HEAD><TITLE>Bulletin Board</TITLE></HEAD><BODY>}
bbs: load %bb.db
print {
<center><table border=1 cellpadding=10 width=600><tr><td>
<center><strong><font size=4>
Please REFRESH this page to see new messages.
</font></strong></center>
}
print-all: does [
print {<br><hr><font size=5> Posted Messages: </font> <br><hr>}
foreach bb (reverse bbs) [
print rejoin [
{<BR>Date/Time: } bb/2 { }
{"Name: } bb/1 {<BR><BR>} bb/3 {<BR><BR><HR>}
]
]
]
submitted: decode-cgi system/options/cgi/query-string
if submitted/2 <> none [
entry: copy []
append entry submitted/2
append entry to-string (now + 3:00)
append entry submitted/4
append/only bbs entry
save %bb.db bbs
print {<BR><strong>Your message has been added: </strong><BR>}
]
print-all
print {
<font size=5> Post A New Public Message:</font><hr>
<FORM ACTION="http://website.com/bb/bb.cgi">
<br> Your Name: <br>
<input type=text size="50" name="student"><BR><BR>
Your Message: <br>
<textarea name=message rows=5 cols=50></textarea><BR><BR>
<INPUT TYPE="SUBMIT" NAME="Submit" VALUE="Post Message">
</FORM>
</td></tr></table></center>
}
print read %template_footer.html
Here's an example data file for the program above:
[
[
"Nick Antonaccio"
"8-Nov-2006/4:55:59-8:00"
{
WELCOME TO OUR PUBLIC BULLETIN BOARD.
Please keep the posts clean cut and on topic.
Thanks and have fun!
}
]
]
14.7 GET vs POST Example
The default format for REBOL CGI data is "GET". Data submitted by the GET method in an HTML form is displayed in the URL bar of the user's browser. If you don't want users to see that data displayed, or if the amount of submitted data is larger then can be contained in the URL bar of a browser, the "POST" method should be used. To work with the POST method, the action in your HTML form should be:
<FORM METHOD="post" ACTION="./your_script.cgi">
You must also use the "read-cgi" function below to decode the submitted POST data in your REBOL script. This example creates a password protected online text editor, with an automatic backup feature:
#!/home/path/public_html/rebol/rebol -cs
REBOL []
print {content-type: text/html^/}
print {<HTML><HEAD><TITLE>Edit Text Document</TITLE></HEAD><BODY>}
; submitted: decode-cgi system/options/cgi/query-string
; We can't use the normal line above to decode, because
; we're using the POST method to submit data (because data
; from the textarea may get too big for the GET method).
; Use the following standard function to process data from
; a POST method instead:
read-cgi: func [/local data buffer][
switch system/options/cgi/request-method [
"POST" [
data: make string! 1020
buffer: make string! 16380
while [positive? read-io system/ports/input buffer 16380][
append data buffer
clear buffer
]
]
"GET" [data: system/options/cgi/query-string]
]
data
]
submitted: decode-cgi read-cgi
; if document.txt has been edited and submitted:
if submitted/2 = "save" [
; save newly edited document:
write to-file rejoin ["./" submitted/6 "/document.txt"] submitted/4
print ["Document Saved."]
print rejoin [
{<META HTTP-EQUIV="REFRESH" CONTENT="0;
URL=http://website.com/folder/}
submitted/6 {">}
]
quit
]
; if user is just opening page (i.e., no data has been submitted
; yet), request user/pass:
if ((submitted/2 = none) or (submitted/4 = none)) [
print {
<strong>W A R N I N G - Private Server, Login Required:
</strong><BR><BR>
<FORM ACTION="./edit.cgi">
Username: <input type=text size="50" name="name"><BR><BR>
Password: <input type=text size="50" name="pass"><BR><BR>
<INPUT TYPE="SUBMIT" NAME="Submit" VALUE="Submit">
</FORM>
}
quit
]
; check user/pass against those in userlist.txt,
; end program if incorrect:
userlist: load %userlist.txt
folder: submitted/2
password: submitted/4
response: false
foreach user userlist [
if ((first user) = folder) and (password = (second user)) [
response: true
]
]
if response = false [print {Incorrect Username/Password.} quit]
; if user/pass is ok, go on...
; backup (before changes are made):
cur-time: to-string replace/all to-string now/time {:} {-}
document_text: read to-file rejoin [{./} folder {/document.txt}]
write to-file rejoin [
{./} folder {/} now/date {_} cur-time {.txt}] document_text
; note the POST method in the HTML form:
prin {
<strong>Be sure to SUBMIT when done:</strong><BR><BR>
<FORM method="post" ACTION="./edit.cgi">
<INPUT TYPE=hidden NAME=submit_confirm VALUE="save">
<textarea cols="100" rows="15" name="contents">
}
;
; The following line is what we want to do, but it won't work for
; HTML documents which contain <textarea>s
;
; print document_text
;
; The following line fixes the problem:
;
prin replace/all document_text {</textarea>} {<\/textarea>}
print {</textarea><BR><BR>}
print rejoin [{<INPUT TYPE=hidden NAME=folder VALUE="} folder {">}]
print {<INPUT TYPE="SUBMIT" NAME="Submit" VALUE="Submit">}
print {</FORM>}
print {</BODY></HTML>}
14.8 Group Note System
Earlier in the tutorial, a full featured note sharing system was presented to allow groups of users to exchange messages. This CGI program allows users to enter and display notes using the exact same data files saved by the desktop program. The desktop version of the application and this web program are 100% interoperable - messages entered with the desktop app are immediately viewable using this CGI, and vice-versa.
Notice that the "read-cgi" function is used in the fourth line. In recent versions of REBOL, that function is built-in, so it does not need to be defined in the code (it's a good idea to include the function definition, in case the script is ever used on a web server that has an older version of REBOL):
#! ../rebol276 -cs
REBOL [title: "Group Notes"]
print {content-type: text/html^/}
submitted: decode-cgi read-cgi
url: ftp://user:pass@site.com/public_html/Notes
print {
<center><table border=0 cellpadding=10 width=600><tr><td>
}
if submitted/2 <> none [
if error? try [
write/lines/append url rejoin [
"^/^/" now " (" submitted/2 "): " submitted/4
]
] [print "ERROR: Not Saved" quit]
]
print {<pre>}
if error? try [notes: copy read/lines url] [write url notes: ""]
display: copy {}
count: 0
remove/part notes 2
foreach note reverse notes [
either note = "" [
note: {<br>}
] [
count: count + 1
note: rejoin [count ") "note]
]
append display note
]
print display
print {
</td></tr></table>
<FORM ACTION="http://re-bol.com/notes.cgi">
<table border=0 cellPadding=0 cellSpacing=4 width=600>
<tr><td width=10%>Name:</td>
<td><input type=text size="65" name="username"></td></tr>
<tr><td width=10%>Message:</td>
<td><textarea name=message rows=5 cols=50></textarea></td></tr>
<tr><td></td><td>
<input type="submit" name="submit" value="Submit">
</td></tr></table>
</FORM>
</td></tr></table></center>
}
14.9 Generic Form Handler
The following is a generic form handler that can be used to save GET or POST data to a text file. It's a useful replacement for generic form mailers, and makes the data much more accessible later by other scripts:
#!/home/path/public_html/rebol/rebol -cs
REBOL []
print {content-type: text/html^/}
read-cgi: func [/local data buffer][
switch system/options/cgi/request-method [
"POST" [
data: make string! 1020
buffer: make string! 16380
while [positive? read-io system/ports/input buffer 16380][
append data buffer
clear buffer
]
]
"GET" [data: system/options/cgi/query-string]
]
data
]
submitted: decode-cgi read-cgi
print {
<HTML><HEAD><TITLE>Your Form Has Been Submitted</TITLE></HEAD>
<BODY><CENTER><TABLE border=0 cellPadding=10 width="80%"><TR><TD>
}
entry: rejoin [{[^/ "Time Stamp:" } {"} form (now + 3:00) {"^/}]
foreach [title value] submitted [
entry: rejoin [entry { } {"} mold title {" } mold value {^/}]
]
append entry {]^/}
write/append %submitted_forms.txt entry
html: copy ""
foreach [title value] submitted [
repend html [
<TR><TD width=20%> mold title </TD><TD> mold value </TD></TR>
]
]
print rejoin [
{
<FONT size=5>Thank You! The following information has been
submitted: </FONT><BR><BR>Time Stamp:
}
now + 3:00 {<BR><BR><TABLE border=1 cellPadding=10 width="100%">}
html {</TABLE><BR>}
{
To correct any errors or to submit forms for additional people,
please click the [BACK] button in your browser, make any
changes, and resubmit the form. You'll hear from us shortly.
Thank you!<BR><BR><CENTER><FONT size=1>
Copyright © 2009 This Web Site. All rights reserved.</FONT>
</CENTER></TD></TR></CENTER></BODY></HTML>
}
]
quit
Here's a basic form example that could be processed by the above script. You can add as many text, textareas, and other form items as desired, and the script will save all the submitted data (the action link in the form below assumes that the script above is saved in the text file named "form.cgi"):
<FORM action="form.cgi">
Name:<BR><INPUT type="TEXT" name="name"><BR><BR>
Email:<BR><INPUT type="TEXT" name="email"><BR><BR>
Message:<BR>
<TEXTAREA cols="75" rows="5" name="message">
</TEXTAREA><BR><BR>
<INPUT type="SUBMIT" name="Submit" value="Submit">
</FORM>
The script below can be used on a desktop PC to easily view all the forms submitted at the script above. It provides nice GUI navigation, message count, sort by any data column, etc.:
REBOL [title: "CGI form submission viewer"]
sort-column: 4 ; even numered cols contain data (2nd col is time stamp)
signups: load http://yoursite.com/submitted_forms.txt
do create-list: [
name-list: copy []
foreach item signups [append name-list (pick item sort-column)]
]
view center-face layout [
the-list: text-list 600x150 data name-list [
foreach item signups [
if (pick item sort-column) = value [
dt: copy ""
foreach [label data] item [
dt: rejoin [
dt newline label " " data
]
]
a/text: dt
show a
]
]
]
a: area 600x300 across
btn "Sort by..." [
sort-column: to-integer request-text/title/default {
Data column to list:} "4"
do create-list
the-list/data: name-list
show the-list
]
tab text join (form length? signups) " entries."
]
Here's another script that removes the title columns and reduces the form data into a usable format. Possibilities with managing form data like this are endless:
submissions: load http://yoursite.com/submitted_forms.txt
do create-list: [
data: copy []
foreach block submissions [append/only data (extract/index block 2 4)]
datastring: copy {}
foreach block data [
datastring: join datastring "[^/"
foreach item block [datastring: rejoin [datastring item newline]]
datastring: join datastring "]^/^/"
]
editor datastring
]
14.10 File Uploader
The following example demonstrates how to upload files to your web server using the decode-multipart-form-data function by Andreas Bolka:
#! /home/path/public_html/rebol/rebol -cs
REBOL [Title: "HTTP File Upload"]
print {content-type: text/html^/}
print {<HTML><HEAD><TITLE>File Upload</TITLE></HEAD>}
print {<BODY><br><br><center><table width=95% border=1>}
print {<tr><td width=100%><br><center>}
read-cgi: func [/local data buffer][
switch system/options/cgi/request-method [
"POST" [
data: make string! 1020
buffer: make string! 16380
while [positive? read-io system/ports/input buffer 16380][
append data buffer
clear buffer
]
]
"GET" [data: system/options/cgi/query-string]
]
data
]
submitted: read-cgi
if submitted/2 = none [
print {
<FORM ACTION="./upload.cgi"
METHOD="post" ENCTYPE="multipart/form-data">
<strong>Upload File:</strong><br><br>
<INPUT TYPE="file" NAME="photo"> <br><br>
<INPUT TYPE="submit" NAME="Submit" VALUE="Upload">
</FORM>
<br></center></td></tr></table></BODY></HTML>
}
quit
]
decode-multipart-form-data: func [
p-content-type
p-post-data
/local list ct bd delim-beg delim-end non-cr non-lf non-crlf mime-part
] [
list: copy []
if not found? find p-content-type "multipart/form-data" [return list]
ct: copy p-content-type
bd: join "--" copy find/tail ct "boundary="
delim-beg: join bd crlf
delim-end: join crlf bd
non-cr: complement charset reduce [ cr ]
non-lf: complement charset reduce [ newline ]
non-crlf: [ non-cr | cr non-lf ]
mime-part: [
( ct-dispo: content: none ct-type: "text/plain" )
delim-beg ; mime-part start delimiter
"content-disposition: " copy ct-dispo any non-crlf crlf
opt [ "content-type: " copy ct-type any non-crlf crlf ]
crlf ; content delimiter
copy content
to delim-end crlf ; mime-part end delimiter
( handle-mime-part ct-dispo ct-type content )
]
handle-mime-part: func [
p-ct-dispo
p-ct-type
p-content
/local tmp name value val-p
] [
p-ct-dispo: parse p-ct-dispo {;="}
name: to-set-word (select p-ct-dispo "name")
either (none? tmp: select p-ct-dispo "filename")
and (found? find p-ct-type "text/plain") [
value: content
] [
value: make object! [
filename: copy tmp
type: copy p-ct-type
content: either none? p-content [none][copy p-content]
]
]
either val-p: find list name
[change/only next val-p compose [(first next val-p) (value)]]
[append list compose [(to-set-word name) (value)]]
]
use [ct-dispo ct-type content] [
parse/all p-post-data [some mime-part "--" crlf]
]
list
]
; After the following line, "probe cgi-object" will display all parts of
; the submitted multipart object:
cgi-object: construct decode-multipart-form-data
system/options/cgi/content-type copy submitted
; Write file to server using the original filename, and notify the user:
the-file: last split-path to-file copy cgi-object/photo/filename
write/binary the-file cgi-object/photo/content
print {
<strong>UPLOAD COMPLETE</strong><br><br>
<strong>Files currently in this folder:</strong><br><br>
}
folder: sort read %.
foreach file folder [
print [rejoin [{<a href="./} file {">} file {</a><br>}]]
]
print {<br></td></tr></table></BODY></HTML>}
; Alternatively, you could forward to a different page when done:
;
; wait 3
; refresh-me: {
; <head><title></title>
; <META HTTP-EQUIV="REFRESH" CONTENT="0; URL=./index.cgi"></head>
; }
; print refresh-me
This variation of the upload script allows you to select the directory to which files are uploaded:
#! /home/path/public_html/rebol/rebol -cs
REBOL [Title: "HTTP File Upload"]
print {content-type: text/html^/}
print {<HTML><HEAD><TITLE>File Upload</TITLE></HEAD>}
print {<BODY><br><br><center><table width=95% border=1>}
print {<tr><td width=100%><br><center>}
read-cgi: func [/local data buffer][
switch system/options/cgi/request-method [
"POST" [
data: make string! 1020
buffer: make string! 16380
while [positive? read-io system/ports/input buffer 16380][
append data buffer
clear buffer
]
]
"GET" [data: system/options/cgi/query-string]
]
data
]
submitted: read-cgi
if submitted/2 = none [
print {
<FORM ACTION="./upload.cgi"
METHOD="post" ENCTYPE="multipart/form-data">
<strong>Upload File:</strong><br><br>
<INPUT TYPE="file" NAME="photo"> <br><br>
<INPUT TYPE="text" NAME="path" SIZE="35"
VALUE="/home/path/public_html/">
<INPUT TYPE="submit" NAME="Submit" VALUE="Upload">
</FORM>
<br></center></td></tr></table></BODY></HTML>
}
quit
]
decode-multipart-form-data: func [
p-content-type
p-post-data
/local list ct bd delim-beg delim-end non-cr non-lf non-crlf mime-part
] [
list: copy []
if not found? find p-content-type "multipart/form-data" [return list]
ct: copy p-content-type
bd: join "--" copy find/tail ct "boundary="
delim-beg: join bd crlf
delim-end: join crlf bd
non-cr: complement charset reduce [ cr ]
non-lf: complement charset reduce [ newline ]
non-crlf: [ non-cr | cr non-lf ]
mime-part: [
( ct-dispo: content: none ct-type: "text/plain" )
delim-beg ; mime-part start delimiter
"content-disposition: " copy ct-dispo any non-crlf crlf
opt [ "content-type: " copy ct-type any non-crlf crlf ]
crlf ; content delimiter
copy content
to delim-end crlf ; mime-part end delimiter
( handle-mime-part ct-dispo ct-type content )
]
handle-mime-part: func [
p-ct-dispo
p-ct-type
p-content
/local tmp name value val-p
] [
p-ct-dispo: parse p-ct-dispo {;="}
name: to-set-word (select p-ct-dispo "name")
either (none? tmp: select p-ct-dispo "filename")
and (found? find p-ct-type "text/plain") [
value: content
] [
value: make object! [
filename: copy tmp
type: copy p-ct-type
content: either none? p-content [none][copy p-content]
]
]
either val-p: find list name
[change/only next val-p compose [(first next val-p) (value)]]
[append list compose [(to-set-word name) (value)]]
]
use [ct-dispo ct-type content] [
parse/all p-post-data [some mime-part "--" crlf]
]
list
]
cgi-object: construct decode-multipart-form-data
system/options/cgi/content-type copy submitted
the-file: last split-path to-file copy cgi-object/photo/filename
write/binary the-file cgi-object/photo/content
print {
<strong>UPLOAD COMPLETE</strong><br><br>
<strong>Files currently in this folder:</strong><br><br>
}
folder: sort read to-file cgi-object/path
current-folder: rejoin at
foreach file folder [
print [rejoin [
{<a href="http://site.com/"} (at cgi-object/path 28) file {">}
; convert path to URL
file "</a><br>"
]]
]
print {<br></td></tr></table></BODY></HTML>}
14.11 File Downloader
Here's a script that demonstrates how to push download a file to the user's browser:
#!/home/path/public_html/rebol/rebol -cs
REBOL []
submitted: decode-cgi system/options/cgi/query-string
root-path: "/home/path"
; if no data has been submitted, request file name:
if ((submitted/2 = none) or (submitted/4 = none)) [
print "content-type: text/html^/"
print [<STRONG>"W A R N I N G - "]
print ["Private Server, Login Required:"</STRONG><BR><BR>]
print [<FORM ACTION="./download.cgi">]
print [" Username: " <INPUT TYPE=text SIZE="50" NAME="name"><BR><BR>]
print [" Password: " <INPUT TYPE=text SIZE="50" NAME="pass"><BR><BR>]
print [" File: "<BR><BR>]
print [<INPUT TYPE=text SIZE="50" NAME="file" VALUE="/public_html/">]
print [<BR><BR>]
print [<INPUT TYPE="SUBMIT" NAME="Submit" VALUE="Submit">]
print [</FORM>]
quit
]
; check user/pass, end program if incorrect:
username: submitted/2 password: submitted/4
either (username = "user") and (password = "pass) [
; if user/pass is ok, go on
][
print "content-type: text/html^/"
print "Incorrect Username/Password." quit
]
print rejoin [
"Content-Type: application/x-unknown"
newline
"Content-Length: "
(size? to-file join root-path submitted/6)
newline
"Content-Disposition: attachment; filename="
(second split-path to-file submitted/6)
newline
]
data: read/binary to-file join root-path submitted/6
data-length: size? to-file join root-path submitted/6
write-io system/ports/output data data-length
14.12 A Complete Web Server Management Application
This final script makes use of several previous examples, and some additional code, to form a complete web server management application. It allows you to list directory contents, upload, download, edit, and search for files, execute OS commands (chmod, ls, mv, cp, etc. - any command available on your web server's operating system), and run REBOL commands directly on your server. Edited files are automatically backed up into an "edit_history" folder on the server before being saved. No configuration is required for most web servers. Just save this script as "web-tool.cgi", upload it and the REBOL interpreter into the same folder as your web site's index.html file, set permissions (chmod) to 755, then go to http://yourwebsite/web-tool.cgi.
THIS SCRIPT CAN POSE A MAJOR SECURITY THREAT TO YOUR SERVER. It can potentially enable anyone to gain control of your web server and everything it contains. DO NOT install it on your server if you're at all concerned about security, or if you don't know how to secure your server yourself.
The first line of this script must point to the location of the REBOL interpreter on your web server, and you must use a version of REBOL which supports the "call" function (version 2.76 is recommended). By default, the REBOL interpreter should be uploaded to the same path as this script, that folder should be publicly accessible, and you must upload the correct version of REBOL for the operating system on which your server runs. IN THIS EXAMPLE, THE REBOL INTERPRETER HAS BEEN RENAMED "REBOL276".
#! ./rebol276 -cs
REBOL [Title: "REBOL CGI Web Site Manager"]
;-------------------------------------------------------------------------
; Upload this script to the same path as index.html on your server, then
; upload the REBOL interpreter to the path above (the same path as the
; script, by default). CHMOD IT AND THIS SCRIPT TO 755. Then, to run the
; program, go to www.yoursite.com/this-script.cgi .
;-------------------------------------------------------------------------
; YOU CAN EDIT THESE VARIABLES, _IF_ NECESSARY (change the quoted values):
; The user name you want to use to log in:
set-username: "username"
; The password you want to use to log in:
set-password: "password"
;-------------------------------------------------------------------------
; Do NOT edit these variables, unless you really know what you're doing:
doc-path: to-string what-dir
script-subfolder: find/match what-dir doc-path
if script-subfolder = none [script-subfolder: ""]
;-------------------------------------------------------------------------
; Get submitted data:
selection: decode-cgi system/options/cgi/query-string
read-cgi: func [/local data buffer][
switch system/options/cgi/request-method [
"POST" [
data: make string! 1020
buffer: make string! 16380
while [positive? read-io system/ports/input buffer 16380][
append data buffer
clear buffer
]
]
"GET" [data: system/options/cgi/query-string]
]
the-data: data
data
]
submitted: read-cgi
submitted-block: decode-cgi the-data
; ------------------------------------------------------------------------
; This section should be first because it prints a different header
; for a push download (not "content-type: text/html^/"):
if selection/2 = "download-confirm" [
print rejoin [
"Content-Type: application/x-unknown"
newline
"Content-Length: "
(size? to-file selection/4)
newline
"Content-Disposition: attachment; filename="
(second split-path to-file selection/4)
newline
]
data: read/binary to-file selection/4
data-length: size? to-file selection/4
write-io system/ports/output data data-length
quit
]
;-------------------------------------------------------------------------
; Print the normal HTML headers, for use by the rest of the script:
print "content-type: text/html^/"
print {<HTML><HEAD><TITLE>Web Site Manager</TITLE></HEAD><BODY>}
;-------------------------------------------------------------------------
; If search has been called (via link on main form):
if selection/2 = "confirm-search" [
print rejoin [
{<center><a href="./}
(second split-path system/options/script) {?name=} set-username
{&pass=} set-password {">Back to Web Site Manager</a></center>}
]
print {<center><table border="1" cellpadding="10" width=80%><tr><td>}
print [<CENTER><TABLE><TR><TD>]
print rejoin [
{<FORM ACTION="./} (second split-path system/options/script)
{"> Text to search for: <BR> <INPUT TYPE="TEXT" SIZE="50"}
{NAME="phrase"><BR><BR>Folder to search in: <BR>}
{<INPUT TYPE="TEXT" SIZE="50" NAME="folder" VALUE="} what-dir
{" ><BR><BR><INPUT TYPE=hidden NAME=perform-search }
{VALUE="perform-search"><INPUT TYPE="SUBMIT" NAME="Submit" }
{VALUE="Submit"></FORM></TD></TR></TABLE></CENTER>}
{</td></tr></table></center></BODY></HTML>}
]
quit
]
;-------------------------------------------------------------------------
; If edited file text has been submitted:
if submitted-block/2 = "save" [
; Save newly edited document:
write (to-file submitted-block/6) submitted-block/4
print {<center><strong>Document Saved:</strong>
<br><br><table border="1" width=80% cellpadding="10"><tr><td>}
prin [<center><textarea cols="100" rows="15" name="contents">]
prin replace/all read (
to-file (replace/all submitted-block/6 "%2F" "/")
) "</textarea>" "<\/textarea>"
print [</textarea></center>]
print rejoin [
{</td></tr></table><br><a href="./}
(second split-path system/options/script) {?name=} set-username
{&pass=} set-password {">Back to Web Site Manager</a></center>}
{</BODY></HTML>}
]
quit
]
;-------------------------------------------------------------------------
; If upload link has been clicked, print file upload form:
if selection/2 = "upload-confirm" [
print rejoin [
{<center><a href="./}
(second split-path system/options/script) {?name=} set-username
{&pass=} set-password {">Back to Web Site Manager</a></center>}
]
print {<center><table border="1" cellpadding="10" width=80%><tr><td>}
print {<center>}
; If just the link was clicked - no data submitted yet:
if selection/4 = none [
print rejoin [
{<FORM ACTION="./} (second split-path system/options/script)
{" METHOD="post" ENCTYPE="multipart/form-data">
<strong>Upload File:</strong><br><br>
<INPUT TYPE=hidden NAME=upload-confirm
VALUE="upload-confirm">
<INPUT TYPE="file" NAME="photo"> <br><br>
Folder: <INPUT TYPE="text" NAME="path" SIZE="35"
VALUE="} what-dir {">
<INPUT TYPE="submit" NAME="Submit" VALUE="Upload">
</FORM>
<br></center></td></tr></table></center></BODY></HTML>}
]
quit
]
]
;-------------------------------------------------------------------------
; If upload data has been submitted:
if (submitted/2 = #"-") and (submitted/4 = #"-") [
; This function is by Andreas Bolka:
decode-multipart-form-data: func [
p-content-type
p-post-data
/local list ct bd delim-beg delim-end non-cr
non-lf non-crlf mime-part
] [
list: copy []
if not found? find p-content-type "multipart/form-data" [
return list
]
ct: copy p-content-type
bd: join "--" copy find/tail ct "boundary="
delim-beg: join bd crlf
delim-end: join crlf bd
non-cr: complement charset reduce [ cr ]
non-lf: complement charset reduce [ newline ]
non-crlf: [ non-cr | cr non-lf ]
mime-part: [
( ct-dispo: content: none ct-type: "text/plain" )
delim-beg ; mime-part start delimiter
"content-disposition: " copy ct-dispo any non-crlf crlf
opt [ "content-type: " copy ct-type any non-crlf crlf ]
crlf ; content delimiter
copy content
to delim-end crlf ; mime-part end delimiter
( handle-mime-part ct-dispo ct-type content )
]
handle-mime-part: func [
p-ct-dispo
p-ct-type
p-content
/local tmp name value val-p
] [
p-ct-dispo: parse p-ct-dispo {;="}
name: to-set-word (select p-ct-dispo "name")
either (none? tmp: select p-ct-dispo "filename")
and (found? find p-ct-type "text/plain") [
value: content
] [
value: make object! [
filename: copy tmp
type: copy p-ct-type
content: either none? p-content [none][copy p-content]
]
]
either val-p: find list name
[
change/only next val-p compose [
(first next val-p) (value)
]
]
[append list compose [(to-set-word name) (value)]]
]
use [ct-dispo ct-type content] [
parse/all p-post-data [some mime-part "--" crlf]
]
list
]
; After the following line, "probe cgi-object" will display all parts
; of the submitted multipart object:
cgi-object: construct decode-multipart-form-data
system/options/cgi/content-type copy submitted
; Write file to server using the original filename, and notify the
; user:
the-file: last split-path to-file copy cgi-object/photo/filename
write/binary
to-file join cgi-object/path the-file
cgi-object/photo/content
print rejoin [
{<center><a href="./}
(second split-path system/options/script) {?name=} set-username
{&pass=} set-password {">Back to Web Site Manager</a></center>}
]
print {
<center><table border="1" width=80% cellpadding="10"><tr><td>
<strong>UPLOAD COMPLETE</strong><br><br></center>
<strong>Files currently in this folder:</strong><br><br>
}
change-dir to-file cgi-object/path
folder: sort read what-dir
foreach file folder [
print [
rejoin [
{<a href="./} (second split-path system/options/script)
{?editor-confirm=editor-confirm&thefile=}
what-dir file {">(edit)</a> }
{<a href="./} (second split-path system/options/script)
{?download-confirm=download-confirm&thefile=}
what-dir file {">} "(download)</a> "
{<a href="./} (find/match what-dir doc-path) file
{">} file {</a><br>}
]
]
]
print {</td></tr></table></center></BODY></HTML>}
quit
]
;-------------------------------------------------------------------------
; If no data has been submitted, print form to request user/pass:
if ((selection/2 = none) or (selection/4 = none)) [
print rejoin [{
<STRONG>W A R N I N G - Private Server, Login Required:</STRONG>
<BR><BR>
<FORM ACTION="./} (second split-path system/options/script) {">
Username: <INPUT TYPE=text SIZE="50" NAME="name"><BR><BR>
Password: <INPUT TYPE=text SIZE="50" NAME="pass"><BR><BR>
<INPUT TYPE="SUBMIT" NAME="Submit" VALUE="Submit">
</FORM></BODY></HTML>
}]
quit
]
;-------------------------------------------------------------------------
; If a folder name has been submitted, print file list:
if ((selection/2 = "command-submitted") and (
selection/4 = "call {^/^/^/^/}")
) [
print rejoin [
{<center><a href="./}
(second split-path system/options/script) {?name=} set-username
{&pass=} set-password {">Back to Web Site Manager</a></center>}
]
print {<center><table border="1" cellpadding="10" width=80%><tr><td>}
print {<strong>Files currently in this folder:</strong><br><br>}
change-dir to-file selection/6
folder: sort read what-dir
foreach file folder [
print rejoin [
{<a href="./} (second split-path system/options/script)
{?editor-confirm=editor-confirm&thefile=}
what-dir file {">} "(edit)</a> "
{<a href="./} (second split-path system/options/script)
{?download-confirm=download-confirm&thefile=}
what-dir file {">} "(download)</a> "
{<a href="./} (find/match what-dir doc-path) file {">} file
{</a><br>}
]
]
print {</td></tr></table></center></BODY></HTML>}
quit
]
;-------------------------------------------------------------------------
; If editor has been called (via a constructed link):
if selection/2 = "editor-confirm" [
; backup (before changes are made):
cur-time: to-string replace/all to-string now/time ":" "-"
document_text: read to-file selection/4
if not exists? to-file rejoin [
doc-path script-subfolder "edit_history/"
] [
make-dir to-file rejoin [
doc-path script-subfolder "edit_history/"
]
]
write to-file rejoin [
doc-path script-subfolder "edit_history/"
to-string (second split-path to-file selection/4)
"--" now/date "_" cur-time ".txt"
] document_text
; note the POST method in the HTML form:
print rejoin [
{<center><strong>Be sure to SUBMIT when done:</strong>}
{<BR><BR><FORM method="post" ACTION="./}
(second split-path system/options/script) {">}
{<INPUT TYPE=hidden NAME=submit_confirm VALUE="save">}
{<textarea cols="100" rows="15" name="contents">}
(replace/all document_text "</textarea>" "<\/textarea>")
{</textarea><BR><BR><INPUT TYPE=hidden NAME=path VALUE="}
selection/4
{"><INPUT TYPE="SUBMIT" NAME="Submit" VALUE="Submit">
</FORM></center></BODY></HTML>}
]
quit
]
;-------------------------------------------------------------------------
; If search criteria has been entered:
if selection/6 = "perform-search" [
phrase: selection/2
start-folder: to-file selection/4
change-dir start-folder
; found-list: ""
recurse: func [current-folder] [
foreach item (read current-folder) [
if not dir? item [
if error? try [
if find (read to-file item) phrase [
print rejoin [
{<a href="./}
(second split-path system/options/script)
{?editor-confirm=editor-confirm&theitem=}
what-dir item {">(edit)</a> }
{<a href="./}
(second split-path system/options/script)
{?download-confirm=download-confirm&theitem=}
what-dir item {">(download)</a> "}
phrase {" found in: }
{<a href="./} (find/match what-dir doc-path)
item {">} item {</a><BR>}
]
; found-list: rejoin [
; found-list newline what-dir item
; ]
]
] [print rejoin ["error reading " item]]
]
]
foreach item (read current-folder) [
if dir? item [
change-dir item
recurse %.\
change-dir %..\
]
]
]
print rejoin [
{<center><a href="./}
(second split-path system/options/script) {?name=} set-username
{&pass=} set-password {">Back to Web Site Manager</a></center>}
]
print {<center><table border="1" cellpadding="10" width=80%><tr><td>}
print rejoin [
{<strong>SEARCHING for "} phrase {" in } start-folder
{</strong><BR><BR>}
]
recurse %.\
print {<BR><strong>DONE</strong><BR>}
print {</td></tr></table></center></BODY></HTML>}
; save %found.txt found-list
quit
]
;-------------------------------------------------------------------------
; This is the main entry form, used below:
entry-form: [
print rejoin [
{<CENTER><strong>current path: </strong>} what-dir
{<FORM METHOD="get" ACTION="./}
(second split-path system/options/script) {">}{<INPUT TYPE=hidden}
{ NAME=submit_confirm VALUE="command-submitted">}
{<TEXTAREA COLS="100" ROWS="10" NAME="contents">}
{call {^/^/^/^/}</textarea><BR><BR>}
{List Files: <INPUT TYPE=text SIZE="35" NAME="name" VALUE="}
what-dir {"><INPUT TYPE="SUBMIT" NAME="Submit" VALUE="Submit">}
{ <A HREF="./} (second split-path system/options/script)
{?upload-confirm=upload-confirm">upload</A> } ; leave spaces
{<A HREF="./} (second split-path system/options/script)
{?confirm-search=confirm-search">search</A>}
{</FORM><BR></CENTER>}
]
]
;-------------------------------------------------------------------------
; If code has been submitted, print the output, along with an entry form):
if ((selection/2 = "command-submitted") and (
selection/4 <> "call {^/^/^/^/}") and ((to-file selection/6) = what-dir))[
write %commands.txt join "REBOL[]^/" selection/4
; The "call" function requires REBOL version 2.76:
call/output/error
"./rebol276 -qs commands.txt"
%conso.txt %conse.txt
do entry-form
print rejoin [
{<CENTER>Output: <BR><BR>}
{<TABLE WIDTH=80% BORDER="1" CELLPADDING="10"><TR><TD><PRE>}
read %conso.txt
{</PRE></TD></TR></TABLE><BR><BR>}
{Errors: <BR><BR>}
read %conse.txt
{</CENTER></BODY></HTML>}
]
quit
]
;-------------------------------------------------------------------------
if ((selection/2 = "command-submitted") and (
selection/4 <> "call {^/^/^/^/}") and (
(to-file selection/6) <> what-dir)
) [
print rejoin [
{<center><a href="./}
(second split-path system/options/script) {?name=} set-username
{&pass=} set-password {">Back to Web Site Manager</a></center>}
]
print {
<center><table border="1" cellpadding="10" width=80%><tr><td>
<center>
You must EITHER enter a command, OR enter a file path to list.<BR>
Please go back and try again (refresh the page if needed).
</center>
</td></tr></center></BODY></HTML>
}
quit
]
;-------------------------------------------------------------------------
; Otherwise, check submitted user/pass, then print form for code entry:
username: selection/2 password: selection/4
either (username = set-username) and (password = set-password) [
; if user/pass is ok, go on
][
print "Incorrect Username/Password. </BODY></HTML>" quit
]
do entry-form
print {</BODY></HTML>}
14.13 The RebolForum.com CGI Code
Here's the code for the forum CGI application at http://rebolforum.com. This is the actual code that runs the web site where you can go to ask questions about this tutorial. It's presented here in compressed format here so that the code fits nicely on this web page:
REBOL [title: "RebolForum.com"]
editor decompress #{
789CDD5BEB72E3B8B1FEAFA7C0E1D638526D648177D22B698AD79DA99DCBC676
52495C4A8A926089198AD492D4787C5C7AF7D3006FA044D9F2665395B3DC1D4B
04D18D4677E3EB4683FAEE7F2E2F4729992791A46B68B8C87AD79EFDF903BA9B
F57ACB641384F11512D679BEBD1A15DDEE9374B7B95C249B51182FC9B7CBC52A
147ABD6D1AC6397A5A24714EE27C983F6EC915CAC9B77CB4CE37D13F46FB5E2F
7B08F3C51A658F594E36A3649B87499C8D801CF8FEB223593EDC907C9D2CD15D
0FC125FCFCF9E656286FE8053D87CB200FAED022D93E2241289AE6BBFB7B92D6
8D75F78775181174B74DB2300FBF92B72825C17218269500DB24CD3398C37697
737C90A8C9069E71C3D22BD86E49BCAC25E0FB2F2212A45C434D37EB357F851F
3D3A9566061D4A0015A48FC32C0745AE66BD592FDBCD37619E93E5155A9245B2
2443E8558BD0EB85F7A8EE3292D00409699655FA7A48C39C8C4AB1DFCCE797CB
79A59BF93CBB42511254EDAC31255F499A11FA90DD6779B8F8F2B84876717E85
306B02BB9300EC9727DB70413BA2BBAA09C6DA94ED7720D63D3846D1F6340EA7
37B7EF9D9FFE76B547772DA6DC0DFA1E89B359A1A9C28F400994F8EDB74D84A8
60A0A489205E62E1EDB4D6EF18A6DB3C94E021F76CB10EE29844D3AAA1799287
7944A6858FFBD495C7A3A2A9E9B224D9220D9969A6D76401FE8CB8FEE8964E34
1B8FF86E0D7114C65FA69DEB653C62CF58D73DFB5BAA426CE99729AE1FE44CC5
FD03350D069C678202864CF6D2F929E548EC7A9C926D142CC021A2A869464F17
7BF8176CB63FEC5F412450A25F7649FE2AAA3FB0A16031BE8A6A4CA9A2D78D34
A534AB431ACE5894F25F49181FACF13ED5E1681BA439EA8381BF9486E847245E
E5EBB7EC76808648021B28D8D406A8452EFC11FD9CC0A25EA2F92320E6F33CC0
8E6D627440D122181C60CA8939B5B5C13D3C69E8F3199C32FAF91C4E39C0F91C
BA9DE17CFA63C7E0C0A6D32500BE40FFD3565B0920FB969B1E4208EBC8E3C3BE
633ADD00C2484F83481374DFDE4F98FC0C779971D9B778B7999374B26FFB76ED
50002A030A24E08405A80C79241E946E12A48B35C4CCFB28584D62F2C023572D
E388E966DFE19B25ACD5A0553F80D850344E9188D1DD1C00EF0B1F2679F06F10
75548139D704DAE481F4975D9843C8ACB290F1BBDB8F1FA6E3779EE54EC7B7EF
6F3F786DC02F9A9896C1F0D14408A29CA471901301D1CC051AB6DB285C04D434
74ACEF2110C1236AE489D03608352CBA276429A0754AEE27C265CB442C268FA6
E31113A637B63FBB7F43F3959344493AF9CEC0F4BFE9D8F13EDD7AD7F0F8D6B2
3F78689EA44BB021460B12453F07CB25A40413A9B8BDD9060B768BD6245CADF3
8988F11BF4102EF3F5C450DF501ED73069B71E04F2A09CA0AF5614AEE209B808
98AC779F2439CD99C0096F4145234A326263C36729CC784465A592536502D12E
8E08CC957C0BB33C7B8BDE14D3A4E91D9F74B4DA9FC6F4633A5E43EA352D574E
B558C61FBD5B0BBDBBBDFD79E8FDE9CFEFFF3211AE3DFFDABB792720E73348F0
E97622E01FD09FAF3F4C38950A405F709B27CB4798E4824D52F8CE67177D4C1F
D05E74E43DF58AE34C69993CC43405A2292149ABA4A9713FEAEF24E39D906601
249D8E834E2BAF930D11A6EFE0EF7814C0D855EF79DA386DFF332463285F9332
998054394B003D024896402070AF7598A16DB02274CE034ACBFE6D53C2BB7A21
24CD0837F004ECF1DD534F374CC7515545B10DDBB5652C62DBF36D55C78E2129
92EAE8D8561C4F95444D77345B337DDD562CD5C4A2A7C8D8B3A59EECBAA665D8
AAA4AABA6839BA6B9A9AEFF9BA62F992242BD0C5B51DDD320D49C49AE96926DC
F9BAE15AAAAF1ABAAEF74C5FF434ACC8A624598E6360CB75645F543DD3B63543
9355C31225ECBAB6A299A2E462C7921CCBC1B2A79A8E645AA2DF93145DB31D5B
9754D956A1AFADFB926C2B12963CD3711DD7157D57925CC970254773B066A89A
07826098AD2A1958ED19A6EAA8A007C55145579135C7F30C184755A08F6963C3
756C0BD61A95C7760C0566EB4A9E684AA66B39B0849C9EEE69B62BAA926CF858
819EBEADB986E1CB862DBBBAE12B0A284DD5642ABA2B1A32C69AE56307882CD7
D24DDDEB29B66CA992836D4B940D4B935CDFD54D5FB2B0A7B8BE29A9A6853555
954D5FC1BA0C56901CAA3C037B86688335949E0F8259A203AA14E14307FEB2ED
E9D8D56D53D425072C815D0B8BBEEC2B3EC6A6A8688A272A922F3922CC071B3D
C515B1A849962F5A8A6A8B966A6153B2245FB75CD79341DBBE26DA8AAD2AB20F
36D0414596021D3CDB765445728D9EA5882228183B92070A36C13ABE221A8EA9
CA202768D0554D5B0413E8862D59AE07248EABF9BAAA0307DB36DC1EE805C404
F791B1E1AB1698DEF56547934D59D35D45F36559A1CA3545CB1035C570555BA5
082881FE0CCF56CC9EAA49BAAA582286F17D1154A483E72A3615C2F63C5083EB
AA58D50C57C38664788E6459A6A6F9BEED49962E6B720FFB60041F7AE920BFE1
4BBAEED89AEA3BAA6B599AAB2BB62BDBA20F42199AA14B606B59F6B18A4D5997
1DD0A6D2336040C3F034C77764DBD66DB02DB8BDE92AA266192E78B66D82BE6D
0C8BC852650FFCDE0287D07DD5716553D5704F96757062309669FA362841D544
C7F44077A04B4705B61E2C1355577C51C79E0F1EEEBB74B18AB2694B92EA39E0
CAB0181D5F164DE08C6520F1654D35C1180EAC304BF7B1AB890A95CCC1B04A7C
132B86A2A8A00498A761383D49048FA503D9D89264CBD5C1F6A012DB335C0F0C
66F836000416C16F6DDFA4B387ABC7C30BC4050A3B7C5311325AD1F61857836D
D806D3A7F1219F63E4B25515CB2EB61D8922970BCA906CC3C39E6C892A985F12
75D0B7674820B1255B8EE28387038A6892E99B3D7027C5B75C5F715DD905AF80
F91A8601EEAA508FC4BAE77B3238A62D7ABEA6C03297E91AD244BA8A5C07E0A1
674BB0D4240900D1912DC5C5BA288AB07814CB82412D579340D396EDBA580577
057015250B569668E89E02506A393D51310151557058780E2E8F154F150D0903
99EB99A2896D09A66588A20D24B60AFE0AA3586018C004589E188007C0D5B75D
ECB9966B2A008200AF9E659BE04D2E60922B027B51B7B12CC12AD21559014581
88922CE91A76003B0DDF81EEB02C4C0035AA0219D8808E5415445400443D5BF6
C07764C51141C31AB83B0038AC7ECD0428B37A00B6B01281B1678120A6A61BAA
E902B2C17AF13D400E0C7374240798820EA5DFCC5BB264972EC8FFBBE88B9EAA
0A19426419E649CA3B35AF85BA91E61955620432735D0AD53DAFB97631A7CCCD
CB8ACE8942CF5191671966B0357A2CB601570890B6B708B639A4D68B35597CB9
42CB04545E58A2652948D4C753D4677661DBAAB252C4F590F8A244E512599E26
F16AFA3E5E24694A1639728AE1D02DF9968F47E5E34ACFADAD8513D18D30B5D5
9D6D393FCDD07C07BB9C1881EA1FC165D03C4D1E3292A23C01777944C12A08E3
CBF6E6A432DF590EC38F7FB839E4CC519BA4D9B080A6FAFD46130A787573A70D
10B846BFEDF45CE7E71F6B8367B4FA29D814AEEBC5540337EB64172DD1A7CFB7
681380927F0FEA05BF4F6186B0E3CD607385E2E401F60D306B58A359F6001BA4
B2E84557E143BA0477BF0FD32CAF2AABB447D6AB6A6AF0FF26010DB10A0BED8D
FA057354ED8B692328BC5C48D5184038EB350356BE5FB57486C0E51276CC42B3
940EFC43100EEDAE75B61A65EB910FB42DC12DB3CD362279B930335A15FD3DB8
01FDCB03156BD8045FC87019A6E8CDE508601B10F8715494AD83AFE4B894C3F5
627EF4482BF6C23F0576B349E27C5DDF2D8347F6BD891B7C4909DC69D378A270
25D0BE032400C016B5F5595D40277461D62ECA9A4ADF624FF8858ED00F45F1BC
9842F560B881C801F1A85DD5E21DA48A437BF8863C64A3CF88DEBD8A4DF5B864
D6C9AB5BF09A12C42FBF3E374D85F68B01B78E3B31B5F641AFE87B245F613CE0
BA8C92387A44791046AC1ECE08EA95D52A6A4D512BC671F63F193F192600315F
34279B047C8896186A5BCE3A8F48DAE721AC7A5C0872577B4FD7F908BF829E39
2B692D34EAF220D50896629F12144AA1FA604C075C890D7185B8D933D2B39552
E60B55DB6190210FC55907B2964BB2ACD1E4449EF710843902EB5D61A9C5ED57
9579003E023AA8306D17F88EF1B6A98156984B4200B5141D006C6976FE40914B
921AA7A89F7235D0AB86BC5CE67727BA51E8E7B45E8AD2671EDE08240F98E424
DD04B406C9CB441B17579C413B8EDD5ABED1D2080D307D867EACFB482E3F95C1
DD620769183D99A50D2530B1704859B251672DBEA520ECA355C49D1DA9E059D6
FD3C198285C8AA6512A53C3D99D13F9CBB1C61F7D3AB727F7411CFB3ED0FDC71
5F37151BAC32E9D42ABE3CCF608F8AD370F4D42EBCD7569CEC1193BDA50DB040
FB5E69583F5DF065F63DEF49E84998FE5C313EBDA9A975930773D8CB74968DDB
55635C568A4DFC06A828E5723A5E1FB2BD879888B2F07FC9449DEE01DA5A3310
0774C347BBD479C59E73F9CA5FEFAAF040311FD18099E5C1663B63479B2D8EA8
B57FE0A354C5421843A4A5473FC2F3DD6897700A7D014ACFE83A2AFA8ECEEA5C
821F25A8BE9E310247353A9F6CCE08E6678D50F41DBDD4198089EEAAD15391B6
F12958D9452A572E1FCBE955C6CD3A4F407BF4DC48121D02D8837BB3A574FCF7
25FAEFFAE0CE835F4FFFF48FE173A337501B40406C29E92E88215BCBD7E9AE7E
E5452894C28E8A20FCD2D95729613362BF82ADA7926ACF086603EE098F24ED6E
B0D821C14957249FFC731E05CDC95FF59801D37E3618CCA808608C06A94FA026
6F56A0AFF2F4665D8BD36D73585D625EFBC8902DDBA7EA59FBA3DDB35EDB47B8
5021CBE12161BD793E88D1DCC6E985C93D8DFDCFD71F51F1C2D044A0931190E5
DCBEFFFCA985F5427BCF43AF71F1CA0F3BE75B879064C46CAE13BA734B49B64D
E20CB284AF41B43B687B052BEE24B666B53F88042205F9239674677F858E366B
87A3D137AB0A4B0A1A16CA5177193DC4A4F1B073C347AF8FD566A07B04CA3600
FC2E1856CB826E04272A2C84083E303DC02BBB9D1EE71684647B4B40E232F5A4
388B92FBA2D0566EE9D81B62570DA4EE9B9DF7530398274779512394DB33DAE0
E985C2803C259583335F23DA0BAE20146E5DB1AAEE4A46C52D3AE156E311756C
AAE325FD931EBE7CC042360AD8616E4ACF7FCF2D8A56FCE00F4D169A8CA27B71
BEA68C4B68E67258F7FFD525DB366814CC8780D2AD1225BD5ECA8CCFCA443A32
90EA8299B278C0B65C758C68E5B0C523C6976F3F7C7DB0BA5E1998F9EB8520CD
5FCF05EC17A47955F0E6AFDF432C6DCDE774E8E1AFA78354B94CE187312C1B54
C37C67B6FC9C917F65C8E6AFF3C3377FBD2694F3D7B112DB2DB34E8CE1B575C5
DEE76D567BB92BECDCA21F523EF5CB1DDC003C8AE3F15A206B25005DF5DBFF58
A5F6F2775BAAFDEFA9C89645D6222BBB42DD6509AE232A7A76D132D5BD45ED52
1CC785D1B428DAAFF87211ABAC105558D13A20AADE90BB3B10BCBE295E9C2E54
F3DF5B37A605D62B5EEA8E1232574446E7D1D2BA795749F988A2BBBA5CD753CB
17AD398AC141611906C99234AFE4CB0080813CA3E181A62D657995E2C612A654
16C1FAC5695414648CFFE0B008CB264C2BCDC5DB970D73BA8693EDEFBAC25CC7
F10A13AF4BD445ACE64B550081FD40A510D99B0AF4891CB52941CBFFB912F451
79B929431FD4989FA9209F2E3DD3938761B92DB22A925927C7BAD8CCD3702FBA
025DEF40E3AC7B9D9B37C98506C94BCDA515FC0B8A57EFB64F6E8DABDD42B51B
2A6EBB09B93D9D5AEFE90A0AFAE804D50BDB2F7EC072B7557C6F1551C5561155
3D5944ED812FB2779A0B94E71741B536E7736E7372B2CC5DEEEC0ACE9AFAA62E
453F57C22E4AD0DDEF7D9F7EE71B20EEDC22F41E88212C7172D69BC95A589075
DF1A8DFDC2018D903CE0E95A3432AD3FF3B92D7028B192FB5DC5D31F21A12B4F
128E47E0EAD0F5EE96AF44D76629BFD4A718ECAD8DAAF1E0D870C05EABE8F36A
280B53F47D8C221E235857255E0FEB9F48D1C4ABFE411444065A2868CC7ED0BD
750B7235732B28390DCE665D88339E7643CE0BEE35AD10B7DE0A1C88F204DBA4
AFA489464D5E4A55BCE7192E5941681BC413A9AC4794B0728EE336672FD7CEBB
F77FF15CF4D1BBB9B17EF46EDAD58ACA9EB317E7C789D3485097950E67598E5F
EF92B36727FA9AE99DAABBD433E986E4AA2AF36F20EED987404AB570297EBD08
D8D50B2D4D4594DEF2E3F2C825024F56C36433E73A811A4E54EBD48EFAE571F9
AB6318F25098821D52FF9A016145FD8B2CF233C7AB6AA71D03BDBA6E7ACE78E5
0B731DE3FDE6D5D597A26FA3B2A6A2DA1D7B5F55453DAD863AC64C5F5D5805EF
2C92D66EFEE542FB0DC2BF61D467A86D84E0029BC48DFC1C5E54262A70A386A2
F34FB4E9EF8CA6D73737C827F4558DF3090F7E0A03B938FD7C0D87F235DEE90D
FB7C9EB2ACC6658F71B0CDC890AC53F6A336F66BAAACF828C3F1E576BD7D2B0F
AFE98FAD0E6B7A3FA6C13AD8FC21AB7ECF75C6880F0F0F97EC875B9749BA1A05
AB60B84A93DD361B163349DF824F46CB492A3F6C0F47B3A2FCA377D6209B208C
CA5162928FD88FC2C398350FA330CB2FD343DE1F3FBC527A9E6F2ACFA36475CC
F45A46363C388B739E0671160539B95C25C92A22CC1C75E3DB753421F145164D
EED38BDD84936619AEC245B0C992FB9C91D4329139896C9BBAC6050901439477
B79F1CFBC7EC273BB2EE97F1FBC7BFBBAB8B2C98FCF52281C7D540178B7C021B
BE5D945FC007E4B213F102C2F3043B4EF6277DE53D58D6C516A06E322AF2F737
B2FFCB1BD965A3CDE76F246D1DC12D89E1DB220A6119C25DB22569401F6570E3
AB7F87AF6994B16E471A2BA47EA52DD8BC93CD661787F923FBC1DB215FA77A7A
16E7940C4BB6877C6E777992864144D994C87590F81E1DEBD08D496B83CCF6AF
FF07541284A7EE410000
}
14.14 Etsy Account Manager
A complete tutorial about using REBOL with the Etsy API is available at http://re-bol.com/etsy_api_tutorial.html. This CGI program demonstrates how to access many of the Etsy API functions:
#! ../rebol276 -cs
REBOL [Title: "Etsy"]
print {content-type: text/html^/^/}
print {<HTML><HEAD><TITLE>Etsy</TITLE></HEAD><BODY>}
read-cgi: func [/local data buffer][
switch system/options/cgi/request-method [
"POST" [
data: make string! 1020
buffer: make string! 16380
while [positive? read-io system/ports/input buffer 16380][
append data buffer
clear buffer
]
]
"GET" [data: system/options/cgi/query-string]
]
data
]
submitted: decode-cgi submitted-bin: read-cgi
if ((submitted/2 = none) or (submitted/4 = none)) [
print {
<STRONG>W A R N I N G - Private Server:</STRONG><BR><BR>
<FORM METHOD="post" ACTION="./etsy.cgi">
Username: <input type=text size="50" name="name"><BR><BR>
Password: <input type=text size="50" name="pass"><BR><BR>
<INPUT TYPE="SUBMIT" NAME="Submit" VALUE="submit">
</FORM>
</BODY></HTML>
}
quit
]
myusername: "some-user-name" mypassword: "some-password"
username: submitted/2 password: submitted/4
either ((username = myusername) and (password = mypassword)) [][
print "Incorrect Username/Password."
print {</BODY></HTML>} quit
]
do-etsy: does [
do/args http://reb4.me/r/etsy.r context [
Consumer-Key: #<my-key>
Consumer-Secret: #<my-secret>
User-Store: %etsyusers
Scope: [listings_w listings_r listings_d]
Sandbox: true
]
etsy/as "<my-user-name>"
]
if submitted/6 = "sale" [
do-etsy
coupon-code: submitted/8
add-or-remove: submitted/10
print "FOUND ITEMS:<br><br>"
found: copy []
x: get in (etsy/listings []) 'results
for i 1 (length? x) 1 [
print copy rejoin [
(get in x/:i 'title) <br>
]
append found (get in x/:i 'listing_id)
append found (get in x/:i 'description)
append found (get in x/:i 'title)
append found (get in x/:i 'state)
]
print "<br><br>REPLACED ITEMS:<br><br>"
foreach [lstngid dscrptn titl state] found [
either state <> "active" [
print copy rejoin [
titl { was NOT replaced (listing inactive)<br>}
]
][
etsy/api-call/with put rejoin [
%listings/ lstngid
] either add-or-remove = "add" [
[
title: rejoin ["SALE-" titl]
description: rejoin [coupon-code dscrptn]
]
] [
[
title: replace titl "SALE-" ""
description: replace dscrptn rejoin [
coupon-code] ""
]
]
print copy rejoin [titl {<br>}]
]
]
print "<br>Done<br><br>"
quit
]
if submitted/6 = "replace" [
do-etsy
search-text: submitted/8
replace-text: submitted/10
print "FOUND ITEMS:<br><br>"
found: copy []
x: get in (etsy/listings []) 'results
for i 1 (length? x) 1 [
if find (get in x/:i 'description) search-text [
print rejoin [
; {"} search-text {" found in: }
(get in x/:i 'title) "<br>"
]
append found (get in x/:i 'listing_id)
append found (get in x/:i 'description)
append found (get in x/:i 'title)
append found (get in x/:i 'state)
]
]
print "<br><br>REPLACED ITEMS:<br><br>"
foreach [lstngid dscrptn titl state] found [
either state <> "active" [
print copy rejoin [
titl { was NOT replaced (listing inactive)<br>}
]
][
etsy/api-call/with put rejoin [
%listings/ lstngid
] [
description: (
replace/all dscrptn search-text replace-text
)
]
print copy rejoin [titl {<br>}]
]
]
print "<br>Done<br><br>"
quit
]
if submitted/6 = "create-listing" [
do-etsy
itm: submitted/8
desc: submitted/10
prc: to-decimal next find submitted/12 "$"
ctgry: submitted/14
print "Creating item...<br><br>"
etsy/api-call/with post %/listings [
quantity: 1
title: itm
description: desc
price: prc
category_id: ctgry
who_made: "i_did"
is_supply: "1"
when_made: "2010_2012"
shipping_template_id: "330"
]
print rejoin ["CREATED: " itm ", " desc ", " prc]
quit
]
if submitted/6 = "delete-listing" [
do-etsy
itm2del: submitted/8
print "Deleting...<br><br>"
etsy/api-call/with get rejoin [%listings/ itm2del] [
method: "DELETE"
]
print rejoin ["Item " itm2del " deleted."]
quit
]
if submitted/6 = "view-raw" [
do-etsy
print {<pre>}
probe copy get in (etsy/listings []) 'results
print {</pre>}
quit
]
if submitted/6 = "get-image2" [
do-etsy
photo-item-id: submitted/8
photo-list: etsy/api-call/with get rejoin [
%listings/ photo-item-id "/images"
] []
either error? try [photo-id: first get in photo-list 'results] [
print "No photo available for that item."
return
][
photo-info: etsy/api-call/with get the-code: rejoin [
%listings/ photo-item-id "/images/ " photo-id
] []
]
probe either [] = the-photo: (get in photo-info 'results) [
"none"
] [
the-photo
]
quit
]
default-coupon-code: {
** SALE ** Enter the coupon code "982u3445" at
checkout to receive 10% off your order.<br><br>
}
print rejoin [
{<h2>Add or Remove Sale:</h2>
<FORM METHOD="post" ACTION="./etsy.cgi">
<INPUT TYPE=hidden NAME="username" VALUE="} myusername {">
<INPUT TYPE=hidden NAME="password" VALUE="} mypassword {">
<INPUT TYPE=hidden NAME="subroutine" VALUE="sale">
Coupon Code:<BR>
<TEXTAREA COLS="50" ROWS="18" NAME="couponcode">}
default-coupon-code
{</TEXTAREA><BR><BR>
Add or Remove: <select NAME="addorremove">
<option>add
<option>remove
</option></select><br><br>
<INPUT TYPE="SUBMIT" NAME="Submit" VALUE="submit">
</FORM>
<BR><HR>
<h2>Replace In Description:</h2>
<FORM METHOD="post" ACTION="./etsy.cgi">
<INPUT TYPE=hidden NAME="username" VALUE="} myusername {">
<INPUT TYPE=hidden NAME="password" VALUE="} mypassword {">
<INPUT TYPE=hidden NAME="subroutine" VALUE="replace">
Search Text:<BR><BR>
<input type=text size="35" name="searchtext">
<BR><BR>
Replace Text:<BR><BR>
<input type=text size="35" name="replacetext">
<BR><BR>
<INPUT TYPE="SUBMIT" NAME="Submit" VALUE="submit">
</FORM>
<BR><HR>
<h2>Create Listing:</h2>
<FORM METHOD="post" ACTION="./etsy.cgi">
<INPUT TYPE=hidden NAME="username" VALUE="} myusername {">
<INPUT TYPE=hidden NAME="password" VALUE="} mypassword {">
<INPUT TYPE=hidden NAME="subroutine" VALUE="create-listing">
Title: <BR><BR>
<input type=text size="35" name="title" value="Ring 100">
<BR><BR>
Description: <BR><BR>
<input type=text size="35" name="description"
value="Ring 100. A very pretty ring."><BR><BR>
Price: <BR><BR>
<input type=text size="35" name="Price" value="$19.99">
<BR><BR>
Category: <BR><BR>
<input type=text size="35" name="Category" value="69150467">
<BR><BR>
<INPUT TYPE="SUBMIT" NAME="Submit" VALUE="submit">
</FORM>
<BR><HR>
<h2>Delete Listing:</h2>
<FORM METHOD="post" ACTION="./etsy.cgi">
<INPUT TYPE=hidden NAME="username" VALUE="} myusername {">
<INPUT TYPE=hidden NAME="password" VALUE="} mypassword {">
<INPUT TYPE=hidden NAME="subroutine" VALUE="delete-listing">
Listing ID #: <BR><BR>
<input type=text size="35" name="listing-id"><BR><BR>
<INPUT TYPE="SUBMIT" NAME="Submit" VALUE="submit">
</FORM>
<BR><HR>
<h2>View All Raw Listing Data:</h2>
<FORM METHOD="post" ACTION="./etsy.cgi">
<INPUT TYPE=hidden NAME="username" VALUE="} myusername {">
<INPUT TYPE=hidden NAME="password" VALUE="} mypassword {">
<INPUT TYPE=hidden NAME="subroutine" VALUE="view-raw">
<INPUT TYPE="SUBMIT" NAME="Submit" VALUE="submit">
</FORM>
<BR><HR>
<h2>View Image:</h2>
<FORM METHOD="post" ACTION="./etsy.cgi">
<INPUT TYPE=hidden NAME="username" VALUE="} myusername {">
<INPUT TYPE=hidden NAME="password" VALUE="} mypassword {">
<INPUT TYPE=hidden NAME="subroutine" VALUE="view-image">
<INPUT TYPE="SUBMIT" NAME="Submit" VALUE="submit">
</FORM>
</BODY></HTML>}
]
quit
For comparison, a GUI version of a program providing access to the same Etsy API functions is provided below:
REBOL [title: "Etsy"]
do/args http://reb4.me/r/etsy.r context [
Consumer-Key: #<type_your_key_here>
Consumer-Secret: #<typ_your_secret_here>
User-Store: %etsyusers
Scope: [listings_w listings_r listings_d] ; edit permissions here
Sandbox: false ; change to true when approved
]
coupon-text: {
** SALE ** Enter the coupon code "893894" at checkout to
receive 10% off your order <br><br>
}
replace-items: does [
found-items/text: copy {} show found-items
replaced-items/text: copy {} show replaced-items
found: copy []
x: get in (etsy/listings []) 'results
for i 1 (length? x) 1 [
if find (get in x/:i 'description) search-text/text [
insert head found-items/text copy rejoin [
; {"} search-text/text {" found in: }
(get in x/:i 'title) newline
]
show found-items
append found (get in x/:i 'listing_id)
append found (get in x/:i 'description)
append found (get in x/:i 'title)
append found (get in x/:i 'state)
]
]
foreach [lstngid dscrptn titl state] found [
either state <> "active" [
insert head replaced-items/text copy rejoin [
titl { was NOT replaced (listing inactive)^/}
]
show replaced-items
][
etsy/api-call/with put rejoin [
%listings/ lstngid
] [
description: (
replace/all dscrptn search-text/text replace-text/text
)
]
insert head replaced-items/text copy rejoin [titl {^/}]
show replaced-items
]
]
; alert "Done"
]
sale: func [add-or-remove] [
coupon-code: copy request-text/title/default"Coupon Text:" coupon-text
found-items/text: copy {} show found-items
replaced-items/text: copy {} show replaced-items
found: copy []
x: get in (etsy/listings []) 'results
focus found-items
for i 1 (length? x) 1 [
insert head found-items/text copy rejoin [
(get in x/:i 'title) newline
]
show found-items
append found (get in x/:i 'listing_id)
append found (get in x/:i 'description)
append found (get in x/:i 'title)
append found (get in x/:i 'state)
]
foreach [lstngid dscrptn titl state] found [
either state <> "active" [
insert head replaced-items/text copy rejoin [
titl { was NOT replaced (listing inactive)^/}
]
show replaced-items
][
etsy/api-call/with put rejoin [
%listings/ lstngid
] either add-or-remove = true [
[
title: rejoin ["SALE-" titl]
description: rejoin [coupon-code dscrptn]
]
] [
[
title: replace titl "SALE-" ""
description: replace dscrptn rejoin [coupon-code] ""
]
]
insert head replaced-items/text copy rejoin [titl {^/}]
show replaced-items
]
]
focus replaced-items
; alert "Done"
]
create-listing: does [
itm: request-text/title/default "Title:" "Item 100"
desc: request-text/title/default "Description:" "Ring #100"
prc: to-decimal next find (
request-text/title/default "Price:" "$19.99"
) "$"
if true = request "Would you like to see a listing of category IDs?" [
categories: etsy/api-call get %taxonomy/categories
cat-list: copy []
foreach category categories/results [
append cat-list reduce [
category/long_name category/category_id
]
]
chosen-category: request-list "Categories" cat-list
]
if unset? chosen-category [chosen-category: "69150467"]
ctgry: request-text/title/default "Category ID:" form chosen-category
flash "Creating item..."
etsy/api-call/with post %/listings [
quantity: 1
title: itm
description: desc
price: prc
category_id: ctgry
who_made: "i_did"
is_supply: "1"
when_made: "2010_2012"
shipping_template_id: "330"
]
unview
alert rejoin ["CREATED: " itm ", " desc ", " prc]
]
delete-listing: does [
itm2del: request-text/title "Listing ID #:"
either true = request "Really Delete?" [
flash "Deleting..."
etsy/api-call/with get rejoin [%listings/ itm2del] [
method: "DELETE"]
unview
alert rejoin ["Item " itm2del " deleted."]
] [
return
]
]
get-image: does [
found: copy []
x: get in (etsy/listings []) 'results
for i 1 (length? x) 1 [
append found (get in x/:i 'title)
append found (get in x/:i 'listing_id)
]
photo-item-id: request-list "Select Item:" found
photo-list: etsy/api-call/with get rejoin [
%listings/ photo-item-id "/images"] []
either error? try [photo-id: first get in photo-list 'results] [
alert "No photo available for that item."
return
][
photo-info: etsy/api-call/with get the-code: rejoin [
%listings/ photo-item-id "/images/ " photo-id
] []
]
editor either [] = the-photo: (get in photo-info 'results) [
"none"
] [
the-photo
]
]
etsy/as "<your-username>"
view center-face gui: layout [
across
text 80 right "If"
cond1: drop-down 100 data [
"Title" "Description" "Listing ID" "Any Field"
]
cond2: drop-down 150 data [
"REPLACE ALL" "Contains" "Does NOT Contain" "Equals"
]
cond3: field 454 "ring" return
text 80 right "Search Text:" search-text: field 720 "ring" [
replace-text/text: copy search-text/text show replace-text
] return
text 80 right "Replace Text:" replace-text: field 720 "ring" return
text 805 "" return
box black 805x2 return
text 805 "" return
text 400 "Found Items:" text 200 "Replaced Items:" return
found-items: area replaced-items: area return
btn "List Raw Data" [editor copy get in (etsy/listings []) 'results]
btn "Create Listing" [create-listing]
btn "Delete Listing" [delete-listing]
btn "Add Sale" [sale true]
btn "Remove Sale" [sale false]
btn "View Image" [get-image]
btn "Replace Description" [replace-items]
]
Be sure to see the following links for more insight about REBOL CGI programming:
http://re-bol.com/cgi_tutorial.txt
http://rebol.com/docs/cgi1.html
http://rebol.com/docs/cgi2.html
http://rebol.com/docs/cgi-bbs.html
http://www.rebol.net/cookbook/recipes/0045.html
To create web sites using a PHP-like version of REBOL that runs in a web server written entirely in REBOL, see:
http://cheyenne-server.org (binaries are available for Windows, Mac, and Linux).
http://cheyenne-server.org/docs/rsp-api.html - documentation for the "RSP" (REBOL server pages) API.
14.15 A Note About Working With Web Servers
To do any work with your web server, you'll need to know your account's FTP username, password, URL, and publicly viewable root folder. Be sure to have that info on hand before trying to accomplish any CGI programming.
The most common interface for managing web sites hosted by popular companies is a piece of software called "cPanel". Instructions for accessing and using cPanel are available all around the Internet, and will most likely be emailed by your hosting company. This author recommends lunarpages.com and hostgator.com, and those two companies are known to support REBOL CGI programming.
To begin building a web application with REBOL, you'll need to upload the REBOL interpreter to your web server. You only need to do this once for each web server (not for each program). If you do not have access to cPanel or some other web based file manager, you can use REBOL to manage the entire server setup, and all script editing, and file management operations. To upload REBOL to your server, open your local REBOL interpreter console and type the following code, but REPLACE the username, password, URL, and publicly viewable folder with your own account info. This will download a REBOL interpreter from the web, and upload it to your own server:
file: ftp://user:pass@site.com/public_html/rebol ; EDIT with your info
write/binary file (read/binary http://re-bol.com/rebol276)
The file transfer process above should take just a few seconds if you're on a broadband connection. Once it's done, you need to set execute "permissions" for the REBOL interpreter in your hosting account. You can use the following REBOL script to do that. Just paste this code into your local REBOL interpreter and enter your web server URL, username, password, folder, etc., when prompted:
REBOL [title: "FTP CHMOD"]
website: request-text/title/default "Web Site:" "site.com"
username: request-text/title/default "User Name:" "user"
password: request-text/title/default "Password:" "pass"
folder: request-text/title/default "Folder:" "public_html"
file: request-text/title/default "File:" "rebol"
permission: request-text/title/default "Permission:" "755"
write %ftpargs.txt trim rejoin [{
open } website {
user } username { } password {
cd } folder {
literal chmod } permission { } file {
quit
}]
call/show "ftp -n -s:ftpargs.txt"
If you have any problems using the above program on your operating system, you can simply run your OS's command line FTP program using the code below, and manually enter your web host account information as described:
call/show {ftp} ; run this line in the REBOL interpreter console
; Type the following command at the FTP prompt (replace your web url):
open yourdomain.com ; enter your username and password when prompted
; Type the following commands (replace your folder and file names):
cd public_html/
literal chmod 755 rebol
quit
All the steps above only need to be completed the very first time you set up your web hosting account. Once you've got REBOL uploaded to your server, you can begin to create your first CGI program. To start editing a new web site script, type the following into your local REBOL interpreter console (again, be sure to edit the user name, password, URL, and public folder information to match your web server account settings):
editor ftp://user:pass@site.com/public_html/example.cgi ; EDIT account
Type the following code into the REBOL text editor and save it (clicking "Save" in the REBOL text editor ACTUALLY SAVES/UPLOADS THE CHANGES TO YOUR WEB SERVER):
#!./rebol -cs
REBOL []
print {content-type: text/html^/}
Run the "FTP CHMOD" program above one more time to set the permissions for this new "example.cgi" to 755 (you'll need to set permissions to 755 for every .cgi script you create and upload to your server, so keep the above script handy). To be clear, in the ftp script above, change "rebol" to "example.cgi". Now browse to http://yourdomain.com/example.cgi , and you will see a blank page (replace "yourdomain.com" in the URL to the domain of your actual web server). If you get any errors, see the highlighted instructions below about error logs in cPanel.
14.16 WAP - Cell Phone Browser CGI Apps (deprecated)
NOTE: This section has been mostly deprecated by modern phone technology, but it's included here for potential usefulness, and to provide futher material in the study of CGI coding.
Most cell phone service providers offer data options which allow users to access information on the Internet. If you use a "smart phone", your data package likely allows you to access normal web pages using a browser program that runs on your phone (with varying degrees of rendering success). "Dumb" cell phones, however, come only with a "WAP" browser that allows you to access only very light mobile versions of web sites, and provides information in a format specifically accessible only by cell phones. Using WAP mobile sites, you can check email in Google, Yahoo, and other accounts, read news, get weather and traffic reports, manage Ebay transactions, etc. WAP versions of sites, accessible on normal cell phones are not renditions of the normal HTML sites created or interpreted by your phone, but are instead entirely separate versions of each site, created and managed on the web server by the web site creators, and simply accessed by WAP phone browsers.
You can create your own WAP CGI applications, to be accessed by any phone with a WAP browser, using REBOL. WAP scripts are just as easy to ceate as normal CGI web scripts. Instead of HTML, however, you simply need to format the output of your scripts using WAP ("Wireless Application Protocal") syntax. Reference information about WAP tags ("WML") can be found at http://www.w3schools.com/WAP/wml_reference.asp
Here's a basic WAP CGI script which demonstrates the stock code required in all your REBOL WAP scripts. The first 5 lines should be memorized. You'll need to copy them verbatim to the beginning of every WAP script you create. The last lines demonstrate some other important WAP tags. Notice that the wml tag basically replaces the html tag from normal HTML. Every WAP page also contains card tags, which basically replace the head and body tags in normal HTML. This script prints the current time in a WAP browser. Remember to set the permissions of (chmod) any WAP CGI script to 755:
#!./rebol276 -cs
REBOL []
prin {Content-type: text/vnd.wap.wml^/^/}
prin {<?xml version="1.0" encoding="iso-8859-1"?>^/}
prin {<!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN"
"http://www.wapforum.org/DTD/wml_1.1.xml">^/}
; The lines above are standard headers for any WAP CGI script. The
; lines below print out the standard tags needed to print text on a
; WAP page. Included inside the wml, card and p (paragraph) tags is
; one line that prints the current time:
print {<wml><card id="1" title="Current Time"><p>}
print now
print {</p></card></wml>}
quit
The following nearly identical script converts text data output by another script on the web site to WAP format, so that it can be read on small cell phone browsers. Because WAP syntax is a bit more strict than HTML, some HTML tags must be removed (replaced) from the source script output. You must be careful to strip out unnecessary tags and characters in text formatted for display in cell phones. Most WAP browsers will simply display an error if they encounter improperly formatted content:
#!./rebol276 -cs
REBOL []
prin {Content-type: text/vnd.wap.wml^/^/}
prin {<?xml version="1.0" encoding="iso-8859-1"?>^/}
prin {<!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN"
"http://www.wapforum.org/DTD/wml_1.1.xml">^/}
print {<wml><card id="1" title="Wap Page"><p>}
prin replace/all (read http://site.com/page.cgi) {</BODY> </HTML>} {}
print {</p></card></wml>}
quit
Here's a bit more useful version of the above script which allows users to specify the file to be converted, right in the URL of WAP page (i.e., if this script is at site.com/wap.cgi, and the user wants to read page.txt in their WAP browser, then the URL would be "http://site.com/wap.cgi?page.txt"):
#!./rebol276 -cs
REBOL []
submitted: decode-cgi system/options/cgi/query-string
prin {Content-type: text/vnd.wap.wml^/^/}
prin {<?xml version="1.0" encoding="iso-8859-1"?>^/}
prin {<!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN"
"http://www.wapforum.org/DTD/wml_1.1.xml">^/}
print {<wml><card id="1" title="Wap Page"><p>}
; The line below joins the web site URL with the submitted page,
; reads it, and parses it, up to some indicated marker text, so
; that only the text before the marker text is displayed:
parse read join http://site.com/ submitted/2 [
thru submitted/2 copy p to "some marker text"
]
prin p
print {</p></card></wml>}
quit
Here's a version of the above script that lets users select a document to be converted and displayed. This code makes use of select and option tags, which work like HTML dropdown boxes in forms. It also demonstrates how to use anchor, go and postfield tags to submit form data. These work like the "action" in HTML forms. Variable labels for information entered by the user are preceded by a dollar symbol ("$"), and the variable name is enclosed in parentheses (i.e., the $(doc) variable below is submitted to the wap.cgi program). The anchor tag creates a clickable link that makes the action occur:
#!./rebol -cs
REBOL []
submitted: decode-cgi system/options/cgi/query-string
prin {Content-type: text/vnd.wap.wml^/^/}
prin {<?xml version="1.0" encoding="iso-8859-1"?>^/}
prin {<!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN"
"http://www.wapforum.org/DTD/wml_1.1.xml">^/}
; If no data has been submitted, do this by default:
if submitted/2 = none [
; Print some standard tags:
print {<wml><card id="1" title="Select Doc"><p>}
; Get a list of subfolders and display them in a dropdown box:
folders: copy []
foreach folder read %./docs/ [
if find to-string folder {/} [append folders to-string folder]
]
print {Doc: <select name="doc">}
foreach folder folders [
folder: replace/all folder {/} {}
print rejoin [{<option value="} folder {">} folder {</option>}]
]
print {</select>
; Create a link to submit the chosen folder, then close the tags
; from above:
<anchor>
<go method="get" href="wap.cgi">
<postfield name="doc" value="$(doc)"/>
</go>
Submit
</anchor>}
print {</p></card></wml>}
quit
]
; If some data has been submitted, read the selected doc:
print rejoin [{<wml><card id="1" title="} submitted/2 {"><p>}]
parse read join http://site.com/docs/ submitted/2 [
thru submitted/2 copy p to "end of file"
]
prin p
print {</p></card></wml>}
quit
This script breaks up the selected text document into small (130 byte) chunks so that it can be navigated more quickly. Each separate card is displayed as a different page in the WAP browswer, and anchor links are provided to navigate forward and backword between the cards. For the user, paging through data in this way tends to be much faster than scrolling through long results line by line:
#!./rebol276 -cs
REBOL []
submitted: decode-cgi system/options/cgi/query-string
prin {Content-type: text/vnd.wap.wml^/^/}
prin {<?xml version="1.0" encoding="iso-8859-1"?>^/}
prin {<!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN"
"http://www.wapforum.org/DTD/wml_1.1.xml">^/}
count: 0
p: read http://site.com/folder.txt
print {<wml>}
forskip p 130 [
; Create a counter, to appear as each card title, then print links
; to go forward and backward between the cards:
count: count + 1
print rejoin [{<card id="} count {" title="page-} count {"><p>}]
print rejoin [
{<anchor>Next<go href="#} (count + 1) {"/></anchor>}
]
print rejoin [{<anchor>Back<prev/></anchor>}]
; Print 130 characters in each card:
print copy/part p 130
print {</p></card>}
]
print {</wml>}
quit
This next script combines the techniques explained so far, and allows the user to select a file on the web server, using a dropdown box, and displays the selected file in 130 byte pages:
#!./rebol276 -cs
REBOL []
submitted: decode-cgi system/options/cgi/query-string
prin {Content-type: text/vnd.wap.wml^/^/}
prin {<?xml version="1.0" encoding="iso-8859-1"?>^/}
prin {<!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN"
"http://www.wapforum.org/DTD/wml_1.1.xml">^/}
if submitted/2 = none [
print {<wml><card id="1" title="Select Teacher"><p>}
; print {Name: <input name="name" size="12"/>}
folders: copy []
foreach folder read %./Teachers/ [
if find to-string folder {/} [append folders to-string folder]
]
print {Teacher: <select name="teacher">}
foreach folder folders [
folder: replace/all folder {/} {}
print rejoin [
{<option value="} folder {">} folder {</option>}
]
]
print {</select>
<anchor>
<go method="get" href="wap.cgi">
<postfield name="teacher" value="$(teacher)"/>
</go>
Submit
</anchor>}
print {</p></card></wml>}
quit
]
count: 0
parse read join http://site.com/folder/ submitted/2 [
thru submitted/2 copy p to "past students"
]
print {<wml>}
forskip p 130 [
count: count + 1
print rejoin [
{<card id="} count {" title="} submitted/2 "-" count {"><p>}
]
print rejoin [
{<anchor>Next<go href="#} (count + 1) {"/></anchor>}
]
print rejoin [{<anchor>Back<prev/></anchor>}]
print copy/part p 130
print {</p></card>}
]
print {</wml>}
quit
Finally, this script allows users to select a file, and enter some text to be saved in that file, using the input tag:
#!./rebol276 -cs
REBOL []
submitted: decode-cgi system/options/cgi/query-string
prin {Content-type: text/vnd.wap.wml^/^/}
prin {<?xml version="1.0" encoding="iso-8859-1"?>^/}
prin {<!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN"
"http://www.wapforum.org/DTD/wml_1.1.xml">^/}
if submitted/2 = none [
print {<wml><card id="1" title="Select Teacher"><p>}
print {Insert Text: <input name="thetext" size="12"/>}
folders: copy []
foreach folder read %./Teachers/ [
if find to-string folder {/} [
append folders to-string folder
]
]
print {Teacher: <select name="teacher">}
foreach folder folders [
folder: replace/all folder {/} {}
print rejoin [
{<option value="} folder {">} folder {</option>}
]
]
print {</select>
<anchor>
<go method="get" href="wapinsert.cgi">
<postfield name="teacher" value="$(teacher)"/>
<postfield name="thetext" value="$(thetext)"/>
</go>
Submit
</anchor>}
print {</p></card></wml>}
quit
]
chosen-file: rejoin [%./Teachers/ submitted/2 "/schedule.txt"]
adjusted-file: read/lines chosen-file
insert next next next next adjusted-file submitted/4
write/lines chosen-file adjusted-file
count: 0
parse read join http://site.com/folders/ submitted/2 [
thru submitted/2 copy p to "past students"
]
print {<wml>}
forskip p 130 [
count: count + 1
print rejoin [
{<card id="} count {" title="} submitted/2 "-" count {"><p>}
]
print rejoin [
{<anchor>Next<go href="#} (count + 1) {"/></anchor>}
]
print rejoin [{<anchor>Back<prev/></anchor>}]
print copy/part p 130
print {</p></card>}
]
print {</wml>}
quit
This script allows users to read email messages from any POP server, on their cell phone:
#!./rebol276 -cs
REBOL []
submitted: decode-cgi system/options/cgi/query-string
prin {Content-type: text/vnd.wap.wml^/^/}
prin {<?xml version="1.0" encoding="iso-8859-1"?>^/}
prin {<!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN"
"http://www.wapforum.org/DTD/wml_1.1.xml">^/}
accounts: [
["pop.server" "smtp.server" "username" "password" you@site.com]
["pop.server2" "smtp.server2" "username" "password" you@site2.com]
["pop.server3" "smtp.server3" "username" "password" you@site3.com]
]
if ((submitted/2 = none) or (submitted/2 = none)) [
print {<wml><card id="1" title="Select Account"><p>}
print {Account: <select name="account">}
forall accounts [
print rejoin [
{<option value="} index? accounts {">}
last first accounts {</option>}
]
]
print {</select>
<select name="readorsend">
<option value="readselect">Read</option>
<option value="sendinput">Send</option>
</select>
<anchor>
<go method="get" href="wapmail.cgi">
<postfield name="account" value="$(account)"/>
<postfield name="readorsend" value="$(readorsend)"/>
</go>
Submit
</anchor>}
print {</p></card></wml>}
quit
]
if submitted/4 = "readselect" [
t: pick accounts (to-integer submitted/2)
system/schemes/pop/host: t/1
system/schemes/default/host: t/2
system/schemes/default/user: t/3
system/schemes/default/pass: t/4
system/user/email: t/5
prin {<wml><card id="1" title="Choose Message"><p>}
prin rejoin [{<setvar name="account" value="} submitted/2 {"/>}]
prin {<select name="chosenmessage">}
mail: read to-url join "pop://" system/user/email
foreach message mail [
pretty: import-email message
if (find pretty/subject "***SPAM***") = none [
replace/all pretty/subject {"} {}
replace/all pretty/subject {&} {}
prin rejoin [
{<option value="}
pretty/subject
{">}
pretty/subject
{</option>}
]
]
]
print {</select>
<anchor>
<go method="get" href="wapmail.cgi">
<postfield name="subroutine" value="display"/>
<postfield name="chosenmessage" value="$(chosenmessage)"/>
<postfield name="account" value="$(account)"/>
</go>
Submit
</anchor>
</p></card></wml>}
quit
]
if submitted/2 = "display" [
t: pick accounts (to-integer submitted/6)
system/schemes/pop/host: t/1
system/schemes/default/host: t/2
system/schemes/default/user: t/3
system/schemes/default/pass: t/4
system/user/email: t/5
prin {<wml><card id="1" title="Display Message"><p>}
mail: read to-url join "pop://" system/user/email
foreach message mail [
pretty: import-email message
if pretty/subject = submitted/4 [
replace/all pretty/content {"} {}
replace/all pretty/content {&} {}
replace/all pretty/content {3d} {}
; prin pretty/content
; The line above often causes errors - we need to strip
; out HTML tags:
strip: copy ""
foreach item (load/markup pretty/content) [
if ((type? item) = string!) [strip: join strip item]
]
prin strip
]
]
print {</p></card></wml>}
quit
]
Creating WAP versions of your REBOL CGI scripts is a fantastic way to provide even more universal access to your important data.
15. Organizing Efficient Data Structures and Algorithms
The purpose of this tutorial is enable the use of a computing tool that improves productivity in business operations. Creating programs that execute quickly is an important goal toward that end, especially when dealing with large sets of data. In order to achieve speedy performance, it's critical to understand some fundamental concepts about efficient algorithmic design patterns and sensible techniques used to organize and store information effectively.
15.1 A Simple Loop Example
The example below provides a basic demonstration about how inefficient design can reduce performance. This code prints 30 lines of text to the REBOL interpreter console, each made up of 75 dash characters:
REBOL [title: "Badly Designed Line Printer"]
for i 1 30 1 [
for i 1 75 1 [prin "-"]
print ""
]
halt
Even on a very fast computer, you can watch the screen flicker, and see the characters appear as dashes are printed in a loop, 75 times per each line, across 30 lines. That inner character printing operation is repeated a total of 2250 times (30 lines * 75 characters). As it turns out, the REBOL "loop" function is slightly faster than the "for" function, when creating simple counted loop structures. So, you can see a slight performance improvement using the following code:
REBOL [title: "Slightly Improved Line Printer"]
loop 30 [
loop 75 [prin "-"]
print ""
]
halt
But the example above does not address the main bottle neck which slows down the display. The function that really takes time to execute is the "prin" function. The computer is MUCH slower at printing items to the screen, than it is at performing unseen computations in RAM memory. Watch how long it takes to simply perform the same set of loops and to increment a count (i.e., perform a sum computation), compared to printing during each loop. This example executes instantly, even on very slow computers:
REBOL [title: "Loops Without Printing"]
count: 0
loop 30 [
loop 75 [count: count + 1]
]
print count
halt
A much more efficient design, therefore, would be to create a line of characters once, save it to a variable label, and then print that single saved line 30 times:
REBOL [title: "Well Designed Line Printer"]
line: copy {}
loop 75 [append line "-"]
loop 30 [print line]
halt
The example above reduces the number of print functions from 2250 to 30, and the display is dramatically improved. The following code times the execution speed of each of the techniques above, so you can see just how much speed is saved by using memory and processing power more efficiently:
REBOL [title: "Printing Algorithm Timer"]
timer1: now/time/precise
for i 1 30 1 [
for i 1 75 1 [prin "-"]
print ""
]
elapsed1: now/time/precise - timer1
print newpage
timer2: now/time/precise
loop 30 [
loop 75 [prin "-"]
print ""
]
elapsed2: now/time/precise - timer2
print newpage
timer3: now/time/precise
count: 0
loop 30 [
loop 75 [count: count + 1]
]
print count
elapsed3: now/time/precise - timer3
print newpage
timer4: now/time/precise
line: copy {}
loop 75 [append line "-"]
loop 30 [print line]
elapsed4: now/time/precise - timer4
print newpage
print rejoin ["Printing 2250 characters, 'for': " elapsed1]
print rejoin ["Printing 2250 characters, 'loop': " elapsed2]
print rejoin ["Counting to 2250, printing result: " elapsed3]
print rejoin ["Printing 30 preconstructed lines: " elapsed4]
halt
This is, of course, a trivial demonstrative example, but identifying such "bottle necks", and designing code patterns which reduce loop counts and cut down on computational rigor, is a critical part of the thought process required to create fast and responsive applications. Knowing the benchmark speed of functions in a language, and understanding how to use effecient add-on tools such as database systems, can be helpful, but more often, smart architecture and sensible awareness of how data structures are organized, will have a much more dramatic effect on how well a program performs. When working with data sets that are expected to grow to a large scale, it's important to pay attention to how information will be stored and looped through, before any production code is written and implemented.
15.2 A Real Life Example: Checkout Register and Cashier Report System
The example below is the trivial POS example demonstrated earlier in the tutorial. It saves the data for all receipts in a single text file:
REBOL [title: "Minimal Cash Register - Inefficient"]
view gui: layout [
style fld field 80
across
text "Cashier:" cashier: fld
text "Item:" item: fld
text "Price:" price: fld [
if error? try [to-money price/text] [alert "Price error" return]
append a/text reduce [mold item/text " " price/text newline]
item/text: copy "" price/text: copy ""
sum: 0
foreach [item price] load a/text [sum: sum + to-money price]
subtotal/text: form sum
tax/text: form sum * .06
total/text: form sum * 1.06
focus item
show gui
]
return
a: area 600x300
return
text "Subtotal:" subtotal: fld
text "Tax:" tax: fld
text "Total:" total: fld
btn "Save" [
items: replace/all (mold load a/text) newline " "
write/append %sales.txt rejoin [
items newline cashier/text newline now/date newline
]
clear-fields gui
a/text: copy ""
show gui
]
]
This code reports the total of all items sold on any chosen day, by any chosen cashier:
REBOL [title: "Cashier Report - Ineffecient"]
report-date: request-date
report-cashier: request-text/title/default "Cashier:" "Nick"
sales: read/lines %sales.txt
sum: $0
foreach [items cashier date] sales [
if ((report-cashier = cashier) and (report-date = to-date date)) [
foreach [item price] load items [
sum: sum + to-money price
]
]
]
alert rejoin ["Total for " report-cashier " on " report-date ": " sum]
This whole program appears to work just fine on first inspection, but what happens after a million sales transactions have been entered into the system? In that case, the report program must read in and loop through 3 million lines of data, perform the date and cashier comparison evaluations on every single sales entry, and then perform the summing loop on only the matching sales entries. That's a LOT of unnecessary processing, and reading data from the hard drive is particularly slow.
We could simply plan to occassionally copy, paste, and erase old transactions from the sales.txt file into a separate archive file. That would solve the problem (and may be a occasionally useful maintanence objective), but it doesn't really improve performance. By changing the method of storage just a bit, we can dramatically improve performance. The example below creates a new folder, and writes every sales transaction to a separate file. The FILE NAMES of saved sales contain the date, time, and cashier name of each transaction (only the code for the "Save" button has been changed slightly in this example):
REBOL [title: "Minimal Cash Register - Efficient"]
view gui: layout [
style fld field 80
across
text "Cashier:" cashier: fld
text "Item:" item: fld
text "Price:" price: fld [
if error? try [to-money price/text] [alert "Price error" return]
append a/text reduce [mold item/text " " price/text newline]
item/text: copy "" price/text: copy ""
sum: 0
foreach [item price] load a/text [sum: sum + to-money price]
subtotal/text: form sum
tax/text: form sum * .06
total/text: form sum * 1.06
focus item
show gui
]
return
a: area 600x300
return
text "Subtotal:" subtotal: fld
text "Tax:" tax: fld
text "Total:" total: fld
btn "Save" [
file: copy to-file rejoin [
now/date
"_" replace/all form now/time ":" "-"
"_" cashier/text
]
save rejoin [%./sales/ file ] (load a/text)
clear-fields gui
a/text: copy ""
show gui
]
]
This improved report reads the list of file names in the %./sales/ folder. Each file name is parsed into date, time, and name components, and then, only if the date and cashier items match the report requirements, the loop that computes the sum is run. This eliminates a dramatic volume of data reading (only the file list is read - not the entire contents of every file), and avoids a tremendous number of unnecessary looped computational steps:
REBOL [title: "Cashier Report - Efficient"]
report-date: request-date
report-cashier: request-text/title/default "Cashier:" "Nick"
sum: $0
files: copy []
foreach file read %./sales/ [
parsed: parse file "_"
if ((report-date = to-date parsed/1) and (report-cashier = parsed/3))[
append files file
]
]
cd %./sales/
foreach file files [
foreach [item price] load file [
sum: sum + to-money price
]
]
cd %../
alert rejoin ["Total for " report-cashier " on " report-date ": " sum]
When working with systems that are expected to handle large volumes of data, it's essential to run benchmark tests comparing potential design options. It's a simple task to write scripts which generate millions of entries of random data to test any system you create, at volume levels exceeding potential real life scenarios. Creating a script to generate random data to test the program above, for example, is as simple as looping the "Save" button code that creates the receipt files:
REBOL [title: "Automated Test Sales Data Generator"]
random/seed now/time/precise
loop 10000 [
file: to-file rejoin [
((random 31-dec-0001) + 734868) ; 734868 days between 2013/0001
"_" replace/all form random 23:59:59 ":" "-"
"_" random {abcd}
] items: reduce [random "asd fgh jkl" random $100]
save rejoin [%./sales/ file] items
]
Try running the following report after creating the above test data, and you'll see that results are calculated instantly:
REBOL [title: "Cashier Report - Efficient"]
report-date: request-date
report-cashier: request-text/title/default "Cashier:" "abcd"
sum: $0
files: copy []
foreach file read %./sales/ [
parsed: parse file "_"
if report-cashier = parsed/3 [
append files file
]
]
cd %./sales/
foreach file files [
foreach [item price] load file [
sum: sum + to-money price
]
]
cd %../
alert rejoin ["Total for " report-cashier ": " sum]
The method of saving structured data in separate text files, demonstrated by the simplified POS examples above, has been tested by the author in a variety of high volume commercial software implementations. The computational speed demonstrated by reports using this technique outperformed the capabilities of even powerful database systems. Other benefits, such as portability between systems (any operating system with a file structure can be used), no required DBMS installation, easy backup and transfer of data to other systems and mediums, etc., make this an extremely powerful way of approaching serious data management needs. Backing up all new data to a thumb drive is as simple as performing a folder sync. In one case, a web site reporting system was created to match a desktop POS reporting system that has tracked tens of millions of item sales. A simple REBOL script was used to upload new sales transaction files to the web server daily, and the exact same code was used to print the report as CGI script output. Users were then able to check sales online using browsers on any desktop PC, iPhone, Android, etc. Absolutely no database or machine dependent software architecture was required using this simple conceptual pattern of saving data to text files. The key to the success of this example of data organization, storage, and algorithmic report computation, is to simply engineer the system to store potentially searched data fields in the file names.
Learning to avoid wasted effort, and to pinpoint computational bottle necks, especially in loops that manage large data sets, is critical to designing fast applications. Even if you use powerful database systems to store data, it's still important to consider how you read, search, and otherwise manipulate the saved data. As a general rule, avoid reading, writing, transferring, or performing computations on anything other than the absolute smallest possible sets of values, and you'll find that performance can always be improved.
You'll see these sort of efficient design decisions applied to many applications in this tutorial. Pay particular attention to the way data is stored and reported in text files in the "RebGUI Point of Sale", "Simple POS", and "Simple POS Sales Report Printer" examples. More pointed techniques, such as the way in which messages were automatically moved to an "Archive" database in the "RebolForum" CGI application, help to dramatically improve the user experience by directly reducing the amount of data needed to present an initial display. That application has been tested in production situations, and responds instantly, even when the system contains many hundreds of thousands of messages.
It's important to consider the potential volume of data any system will be required to manage, before fully implementing the user interface and data structure in production environments. Consider and test all data fields that may be required as a system grows, and be sure your data structure and computational algorithms can handle more volume than will ever be needed. This will save you enormous headache down the road, when your software is firmly entrenched in business operations.
16. Additional Topics
16.1 Objects
Objects are code structures that allow you to encapsulate and replicate code. They can be thought of as code containers which are easily copied and modified to create multiple versions of similar code and/or duplicate data structures. They're also used to provide context and namespace management features (i.e., to avoid assigning the same variable words and/or function names to different pieces of code in large projects).
Object "prototypes" define a new object container. To create an original object prototype in REBOL, use the following syntax:
label: make object! [object definition]
The object definition can contain functions, values, and/or data of any type. Below is a blank user account object containing 6 variables which are all set to equal "none"):
account: make object! [
first-name: last-name: address: phone: email-address: none
]
The account definition above simply wraps the 6 variables into a container, or context, called "account".
You can refer to data and functions within an object using refinement ("/path") notation:
object/word
In the account object, "account/phone" refers to the phone number data contained in the account. You can make changes to elements in an object as follows:
object/word: data
For example:
account/phone: "555-1234"
account/address: "4321 Street Place Cityville, USA 54321"
Once an object is created, you can view all its contents using the "help" function:
help object
? object
; "?" is a synonym for "help"
If you've typed in all the account examples so far into the REBOL interpreter, then:
? account
displays the following info:
ACCOUNT is an object of value:
first-name none! none
last-name none! none
address string! "4321 Street Place Cityville, USA 54321"
phone string! "555-1234"
email-address none! none
You can obtain a list of all the items in an object using the format "first (object label)":
first account
The above line returns [self first-name last-name address phone email-address]. The first item in the list is always "self", and for most operations, you'll want to remove that item. To do that, use the format "next first (object label)":
next first account
To iterate through every item in an object, you can use a foreach loop on the above values:
foreach item (next first account) [print item]
To get the values referred to by individual word labels in objects, use "get in":
get in account 'first-name
get in account 'address
; notice the tick mark
The following example demonstrates how to access and manipulate every value in an object:
count: 0
foreach item (next first account) [
count: count + 1
print rejoin ["Item " count ": " item]
print rejoin ["Value: " (get in account item) newline]
]
Once you've created an object prototype, you can make a new object based on the original definition:
label: make existing-object [
values to be changed from the original definition
]
This behaviour of copying values based on previous object definitions (called "inheritance") is one of the main reasons that objects are useful. The code below creates a new account object labeled "user1":
user1: make account [
first-name: "John"
last-name: "Smith"
address: "1234 Street Place Cityville, USA 12345"
email-address: "john@hisdomain.com"
]
In this case, the phone number variable retains the default value of "none" established in the original account definition.
You can extend any existing object definition with new values:
label: make existing-object [new-values to be appended]
The definition below creates a new account object, redefines all the existing variables, and appends a new variable to hold the user's favorite color.
user2: make account [
first-name: "Bob"
last-name: "Jones"
address: "4321 Street Place Cityville, USA 54321"
phone: "555-1234"
email-address: "bob@mysite.net"
favorite-color: "blue"
]
"user2/favorite-color" now refers to "blue".
The code below creates a duplicate of the user2 account, with only the name and email changed:
user2a: make user2 [
first-name: "Paul"
email-address: "paul@mysite.net"
]
"? user2a" provides the following info:
USER2A is an object of value:
first-name string! "Paul"
last-name string! "Jones"
address string! "4321 Street Place Cityville, USA 54321"
phone string! "555-1234"
email-address string! "paul@mysite.net"
favorite-color string! "blue"
You can include functions in your object definition:
complex-account: make object! [
first-name:
last-name:
address:
phone:
none
email-address: does [
return to-email rejoin [
first-name "_" last-name "@website.com"
]
]
display: does [
print ""
print rejoin ["Name: " first-name " " last-name]
print rejoin ["Address: " address]
print rejoin ["Phone: " phone]
print rejoin ["Email: " email-address]
print ""
]
]
Note that the variable "email-address" is initially assigned to the result of a function (which simply builds a default email address from the object's first and last name variables). You can override that definition by assigning a specified email address value. Once you've done that, the email-address function no longer exists in that particular object - it is overwritten by the specified email value.
Here are some implementations of the above object. Notice the email-address value in each object:
user1: make complex-account []
user2: make complex-account [
first-name: "John"
last-name: "Smith"
phone: "555-4321"
]
user3: make complex-account [
first-name: "Bob"
last-name: "Jones"
address: "4321 Street Place Cityville, USA 54321"
phone: "555-1234"
email-address: "bob@mysite.net"
]
To print out all the data contained in each object:
user1/display user2/display user3/display
The display function prints out data contained in each object, and in each object the same variables refer to different values (the first two emails are created by the email-address function, and the third is assigned).
Here's a small game in which multiple character objects are created from a duplicated object template. Each character can store, alter, and print its own separately calculated position value based on one object prototype definition:
REBOL []
hidden-prize: random 15x15
character: make object! [
position: 0x0
move: does [
direction: ask "Move up, down, left, or right: "
switch/default direction [
"up" [position: position + -1x0]
"down" [position: position + 1x0]
"left" [position: position + 0x-1]
"right" [position: position + 0x1]
] [print newline print "THAT'S NOT A DIRECTION!"]
if position = hidden-prize [
print newline
print "You found the hidden prize. YOU WIN!"
print newline
halt
]
print rejoin [
newline
"You moved character " movement " " direction
". Character " movement " is now "
hidden-prize - position
" spaces away from the hidden prize. "
newline
]
]
]
character1: make character[]
character2: make character[position: 3x3]
character3: make character[position: 6x6]
character4: make character[position: 9x9]
character5: make character[position: 12x12]
loop 20 [
prin "^(1B)[J"
movement: ask "Which character do you want to move (1-5)? "
if find ["1" "2" "3" "4" "5"] movement [
do rejoin ["character" movement "/move"]
print rejoin [
newline
"The position of each character is now: "
newline newline
"CHARACTER ONE: " character1/position newline
"CHARACTER TWO: " character2/position newline
"CHARACTER THREE: " character3/position newline
"CHARACTER FOUR: " character4/position newline
"CHARACTER FIVE: " character5/position
]
ask "^/Press the [Enter] key to continue."
]
]
You could, for example, extend this concept to create a vast world of complex characters in an online multi-player game. All such character definitions could be built from one base character definition containing default configuration values.
16.1.1 Namespace Management
In this example the same words are defined two times in the same program:
var: 1234.56
bank: does [
print ""
print rejoin ["Your bank account balance is: $" var]
print ""
]
var: "Wabash"
bank: does [
print ""
print rejoin [
"Your favorite place is on the bank of the: " var]
print ""
]
bank
There's no way to access the bank account balance after the above code runs, because the "bank" and "var" words have been overwritten. In large coding projects, it's easy for multiple developers to unintentionally use the same variable names to refer to different pieces of code and/or data, which can lead to accidental deletion or alteration of values. That potential problem can be avoided by simply wrapping the above code into separate objects:
money: make object! [
var: 1234.56
bank: does [
print ""
print rejoin ["Your bank account balance is: $" var]
print ""
]
]
place: make object! [
var: "Wabash"
bank: does [
print ""
print rejoin [
"Your favorite place is on the bank of the: " var]
print ""
]
]
Now you can access the "bank" and "var" words in their appropriate object contexts:
money/bank
place/bank
money/var
place/var
The objects below make further use of functions and variables contained in the above objects. Because the new objects "deposit" and "travel" are made from the "money" and "place" objects, they inherit all the existing code contained in the above objects:
deposit: make money [
view layout [
button "Deposit $10" [
var: var + 10
bank
]
]
]
travel: make place [
view layout [
new-favorite: field 300 trim {
Type a new favorite river here, and press [Enter]} [
var: value
bank
]
]
]
Learning to use objects is important because much of REBOL is built using object structures. As you've seen earlier in the section about built-in help, the REBOL "system" object contains many important interpreter settings. In order to access all the values in the system object, it's essential to understand object notation:
get in system/components/graphics 'date
The same is true for GUI widget properties and many other features of REBOL.
For more information about objects, see:
http://rebol.com/docs/core23/rebolcore-10.html
http://en.wikibooks.org/wiki/REBOL_Programming/Language_Features/Objects
http://en.wikipedia.org/wiki/Prototype-based_programming
16.2 Ports - Fine Grained Access to Files, Email, Network and More
REBOL "ports" provide a single way to handle many types of data input and output. They enable access to a variety of data sources, and allow you to control them all in a consistent way, using standard REBOL series functions. You can open ports to POP email boxes, FTP directories, local text files, TCP network connections, keyboard input buffers, and more, and also use them to output data such as sounds and console interactions. Once a port is opened to a data source, the data contained in the port can be treated as a sequential list of items which can be traversed, arranged, searched, sorted, and otherwise organized/manipulated, all using series functions such as those covered earlier in this text (foreach, find, select, reverse, length?, head, next, back, last, tail, at, skip, extract, index?, insert, append, remove, change, poke, copy/part, clear, replace, join, intersect, difference, exclude, union, unique, empty?, write, save, etc.).
In some cases, there are other native ways to access data contained in a given port, typically with "read" and "write" functions. In such cases, port access simply provides finer control of the data (dealing with email and file data are examples of such cases). In other cases, ports provide the primary interface for accessing data in the given data source (TCP sockets and other network protocols are examples).
Ports are created using the "open" function, and are typically assigned a word label when created:
my-files: open ftp://user:pass@site.com/public_html/
After opening the above port, the files in the FTP directory can be traversed sequentially or by index, using series functions:
print first my-files
print length? my-files
print pick my-files ((length? my-files) - 7) ; 7th file from the end
; etc ...
To change the marked index position in a port, re-assign the port label to the new index position:
my-files: head my-files
print index? my-files
print first my-files
my-files: next my-files
print index? my-files
print first my-files
my-files: at my-files 10
print index? my-files
print first my-files
To close the connection to data contained in a port, use the "close" function:
close my-files
It is of course possible to read and write directly to/from the folder in the above examples without manually opening a port:
print read ftp://user:pass@site.com/public_html/
write ftp://user:pass@site.com/public_html/temp.txt (read %temp.txt)
The difference between opening a port to the above files, and simply reading/writing them is that the port connection offers more specific access to and control of individual files. The "get-modes" and "set-modes" functions can be used to set various properties of files:
my-file: open %temp.txt
set-modes port [
world-read: true
world-write: true
world-execute: true
]
close my-file
More benefits of port control are easy to understand when dealing with email accounts. All the email in a given account can be accessed by simply reading it:
print read pop://user:pass@site.com
If, for example, there are 10000 messages in the above email account, the above action could take a very long time to complete. Even to simply read one email from the above account, the entire account needs to be read:
print second read pop://user:pass@site.com
A much better solution is to open a port to the above data source:
my-email: open pop://user:pass@site.com
Once the above port is open, each of the individual emails in the given POP account can be accessed separately, without having to download any other emails in the account:
print second my-email ; no download of 10000 emails required
And you can jump around between messages in the account:
my-email: head my-email
print first my-email ; prints the 1st email in the box
my-email: next my-email
print first my-email ; prints the 2nd email in the box
my-email: at my-email 4
print first my-email ; prints the 5th email in the box
my-email: head my-email
print first my-email ; prints email #1 again
; etc...
You can also remove email messages from the account:
my-email: head my-email
remove my-email ; removes the 1st email
Internally, REBOL actually deals with most types of data sources as ports. The following line:
write/append %temp.txt "1234"
Is the same as:
temp: open %temp.txt
append temp "1234"
close temp
REBOL ports are objects. You can see all the properties of an open port, using the "probe" function:
temp: open %temp.txt
probe temp
close temp
From the very important example above, you can see that various useful properties of the port data can be accessed using a consistent syntax:
temp: open %temp.txt
print temp/date
print temp/path
print temp/size
close temp
The state/inBuffer and state/outBuffer are particularly important values in any port. Those items are where changes to data contained in the port are stored, until the port is closed or updated. Take a close look at this example:
; First, create a file:
write %temp.txt ""
; That file is now empty:
print read %temp.txt
; Open the above file as a port:
temp: open %temp.txt
; Append some text to it:
append temp "1234"
; Display the text to be saved to the file:
print temp/state/inBuffer
; The appended changes have NOT yet been saved to the file because the
; port has not yet been closed or updated:
print read %temp.txt
; Either "update" or "close" can be used to save the changes to the file:
update temp
; The "update" function has forced the appended data to be written to
; the file, but has NOT yet closed the port:
print read %temp.txt
; We can still navigate the port contents and add more data to it:
temp: head temp
insert temp "abcd"
; Display the text to be saved to the file:
print temp/state/inBuffer
; Those changes have not yet been saved to the file:
print read %temp.txt
; Closing the port will save changes to the file:
close temp
; Here are the saved changes:
print read %temp.txt
; And additional changes can no longer be made:
append temp "1q2w3e4r" ; (error)
Ports can be opened with a variety of refinements to help deal with data appropriately. "Help open" displays the following list:
/binary - Preserves contents exactly.
/string - Translates all line terminators.
/direct - Opens the port without buffering.
/seek - Opens port in seek mode without buffering.
/new - Creates a file. (Need not already exist.)
/read - Read only. Disables write operations.
/write - Write only. Disables read operations.
/no-wait - Returns immediately without waiting if no data.
/lines - Handles data as lines.
/with - Specifies alternate line termination. (Type: char string)
/allow - Specifies the protection attributes when created. (Type: block)
/mode - Block of above refinements. (Type: block)
/custom - Allows special refinements. (Type: block)
/skip - Skips a number of bytes. (Type: number)
Several of those options will be demonstrated in the following example applications.
16.3 Console and CGI Email Apps Using Ports
The following email program opens a port to a selected email account and allows the user to navigate through messages, read, send, delete, and reply to emails. It runs entirely at the command line - no VID GUI components or View graphics are required. You can store configuration information for as many email accounts as you'd like in the "accounts" block, and easily switch between them at any point in the program:
REBOL [Title: "Console Email"]
accounts: [
["pop.server" "smtp.server" "username" "password" you@site.com]
["pop.server2" "smtp.server2" "username" "password" you@site2.com]
["pop.server3" "smtp.server3" "username" "password" you@site3.com]
]
empty-lines: "^/"
loop 400 [append empty-lines "^/"] ; # of lines it takes to clear screen
cls: does [prin {^(1B)[J}]
a-line:{-----------------------------------------------------------------}
select-account: does [
cls
print a-line
forall accounts [
print rejoin ["^/" index? accounts ": " last first accounts]
]
print join "^/" a-line
selected: ask "^/Select an account #: "
if selected = "" [selected: 1]
t: pick accounts (to-integer selected)
system/schemes/pop/host: t/1
system/schemes/default/host: t/2
system/schemes/default/user: t/3
system/schemes/default/pass: t/4
system/user/email: t/5
]
send-email: func [/reply] [
cls
print rejoin [a-line "^/^/Send Email:^/^/" a-line]
either reply [
print join "^/^/Reply-to: " addr: form pretty/from
] [
addr: ask "^/^/Recipient Email Address: "
]
either reply [
print join "^/Subject: " subject: join "re: " form pretty/subject
] [
subject: ask "^/Email Subject: "
]
print {^/Body (when finished, type "end" on a seperate line):^/}
print join a-line "^/"
body: copy ""
get-body: does [
body-line: ask ""
if body-line = "end" [return]
body: rejoin [body "^/" body-line]
get-body
]
get-body
if reply [
rc: ask "^/Quote original email in your reply (Y/n)? "
if ((rc = "yes") or (rc = "y") or (rc = "")) [
body: rejoin [
body
"^/^/^/--- Quoting " form pretty/from ":^/"
form pretty/content
]
]
]
print rejoin ["^/" a-line "^/^/Sending..."]
send/subject to-email addr body subject
cls
print "Sent^/"
wait 1
]
read-email: does [
pretty: none
cls
print "One moment..."
; THE FOLLOWING LINE OPENS A PORT TO THE SELECTED EMAIL ACCOUNT:
mail: open to-url join "pop://" system/user/email
cls
while [not tail? mail] [
print "Reading...^/"
pretty: import-email (copy first mail)
either find pretty/subject "***SPAM***" [
print join "Spam found in message #" length? mail
mail: next mail
][
print empty-lines
cls
prin rejoin [
a-line
{^/The following message is #} length? mail { from: }
system/user/email {^/} a-line {^/^/}
{FROM: } pretty/from {^/}
{DATE: } pretty/date {^/}
{SUBJECT: } pretty/subject {^/^/} a-line
]
confirm: ask "^/^/Read Entire Message (Y/n): "
if ((confirm = "y") or (confirm = "yes") or (confirm = "")) [
print join {^/^/} pretty/content
]
print rejoin [
{^/} a-line {^/}
{^/[ENTER]: Go Forward (next email)^/}
{^/ "b": Go Backward (previous email)^/}
{^/ "r": Reply to current email^/}
{^/ "d": Delete current email^/}
{^/ "q": Quit this mail box^/}
{^/ Any #: Skip forward or backward this # of messages}
{^/^/} a-line {^/}
]
switch/default mail-command: ask "Enter Command: " [
"" [mail: next mail]
"b" [mail: back mail]
"r" [send-email/reply]
"d" [
remove mail
cls
print "Email deleted!^/"
wait 1
]
"q" [
close mail
cls
print"Mail box closed^/"
wait 1
break
]
] [mail: skip mail to-integer mail-command]
if (tail? mail) [mail: back mail]
]
]
]
; begin the program:
select-account
forever [
cls
print a-line
print rejoin [
{^/"r": Read Email^/}
{^/"s": Send Email^/}
{^/"c": Choose a different mail account^/}
{^/"q": Quit^/}
]
print a-line
response: ask "^/Select a menu choice: "
switch/default response [
"r" [read-email]
"s" [send-email]
"c" [select-account]
"q" [
cls
print "DONE!"
wait .5
quit
]
] [read-email]
]
Here is a CGI version of an email program that runs on a web server and operates in the absolute simplest browsers (this app was created to run in the bare bones experimental browser that came with the first Amazon Kindle book reader):
#!../rebol276 -cs
REBOL [Title: "Kindle Email"]
print {content-type: text/html^/^/}
print {<HTML><HEAD><TITLE>Kindle Email</TITLE></HEAD><BODY>}
read-cgi: func [/local data buffer][
switch system/options/cgi/request-method [
"POST" [
data: make string! 1020
buffer: make string! 16380
while [positive? read-io system/ports/input buffer 16380][
append data buffer
clear buffer
]
]
"GET" [data: system/options/cgi/query-string]
]
data
]
submitted: decode-cgi submitted-bin: read-cgi
if ((submitted/2 = none) or (submitted/4 = none)) [
print {
<STRONG>W A R N I N G - Private Server:</STRONG><BR><BR>
<FORM METHOD="post" ACTION="./kindle_email.cgi">
Username: <input type=text size="50" name="name"><BR><BR>
Password: <input type=text size="50" name="pass"><BR><BR>
<INPUT TYPE="SUBMIT" NAME="Submit" VALUE="submit">
</FORM>
</BODY></HTML>
}
quit
]
accounts: [
["pop.server1" "smtp.server1" "username1" "password1" you@site1.com]
["pop.server2" "smtp.server2" "username2" "password2" you@site2.com]
["pop.server3" "smtp.server3" "username3" "password3" you@site3.com]
]
myusername: "username" mypassword: "password"
username: submitted/2 password: submitted/4
either ((username = myusername) and (password = mypassword)) [][
print "Incorrect Username/Password."
print {</BODY></HTML>} quit
]
if submitted/6 = "read" [
account: pick accounts (to-integer submitted/8)
mail-content: read [
scheme: 'POP
host: account/1
port-id: 110
user: account/3
pass: account/4
]
mail-count: length? mail-content
for i 1 mail-count 1 [
single-message: import-email (pick mail-content i)
print rejoin [
i {) <a href="./kindle_email.cgi?}
{u=} myusername {&p=} mypassword
{&subroutine=displaymessage&themessage=}
; copy/part for URI length, at 3 is a serialization trick:
at (mold compress (copy/part single-message/content 5000)) 3
{">} single-message/subject
{</a> <a href="./kindle_email.cgi?}
{u=} myusername {&p=} mypassword
{&subroutine=delete&theaccount=} submitted/8
{&thesubject=} single-message/subject
{&thedate=} single-message/date
{&thefrom=} single-message/from {">delete</a>
<br> } single-message/from {<br>}
]
]
quit
]
if submitted/6 = "displaymessage" [
compressed-message: copy join "#{" submitted/8
print "<pre>" print decompress load compressed-message print "<pre>"
quit
]
if ((submitted/6 = "send") or (submitted/6 = "delete")) [
my-account: pick accounts (to-integer submitted/8)
system/schemes/pop/host: my-account/1
system/schemes/default/host: my-account/2
system/schemes/default/user: my-account/3
system/schemes/default/pass: my-account/4
system/user/email: my-account/5
]
if submitted/6 = "send" [
print "Sending..."
header: make system/standard/email [
To: to-email submitted/10
From: to-email my-account/5
Subject: submitted/12
]
send/header (to-email submitted/10) (trim submitted/14) header
print "<strong>Sent</strong>"
]
if submitted/6 = "delete" [
mail: open to-url join "pop://" system/user/email
while [not tail? mail] [
pretty: import-email (copy first mail)
either all [
pretty/subject = submitted/10
form pretty/date = submitted/12
form pretty/from = submitted/14
][
remove mail print "<strong>Deleted</strong>" wait 1
][
mail: next mail
]
]
]
print {<hr><h2>Read:</h2>}
for i 1 (length? accounts) 1 [
print rejoin [
i {) <a href="./kindle_email.cgi?}
{u=} myusername {&p=} mypassword {&subroutine=read&accountname=}
i {">} (first pick accounts i) {</a><br>}
]
]
print rejoin [
{<BR><HR>
<h2>Send:</h2>
<FORM METHOD="post" ACTION="./kindle_email.cgi">
<INPUT TYPE=hidden NAME="username" VALUE="} myusername {">
<INPUT TYPE=hidden NAME="password" VALUE="} mypassword {">
<INPUT TYPE=hidden NAME="subroutine" VALUE="send">
From Account #: <select NAME="account">}
]
for i 1 (length? accounts) 1 [prin rejoin [{<option>} i]]
print {
</option> </select><br><br>
To: <BR><input type=text size="35" name="to"><BR><BR>
Subject: <BR><input type=text size="35" name="subject"><BR><BR>
<TEXTAREA COLS="50" ROWS="18" NAME="contents"></TEXTAREA><BR><BR>
<INPUT TYPE="SUBMIT" NAME="Submit" VALUE="submit">
</FORM>
</BODY></HTML>
}
quit
16.4 Network Ports - Transferring Data and Files with HTTP
One important use of ports is for transferring data via network connections (TCP and UDP "sockets"). When writing a network application, you must choose a specific port number through which data is to be transferred. Potential ports range from 0 to 65535, but many of those numbers are reserved for specific types of applications (email programs use port 110, web servers use port 80 by default, etc.). To avoid conflicting with other established network applications, it's best to choose a port number between 49152 and 65535 for small scripts. A list of reserved port numbers is available here.
Network applications are typically made up of two or more separate programs, each running on different computers. Any computer connected to a network or to the Internet is assigned a specific "IP address", notated in the format xxx.xxx.xxx.xxx. The numbers are different for every machine on a network, but most home and small business machines are normally in the IP range "192.168.xxx.xxx". You can obtain the IP address of your local computer with the following REBOL code:
read join dns:// (read dns://)
"Server" programs open a chosen network port and wait for one or more "client" programs to open the same port and then insert data into it. The port opened by the server program is referred to in a client program by combining the IP address of the computer on which the server runs, along with the chosen port number, each separated by a colon symbol (i.e., 192.168.1.2:55555).
The following simple set of scripts demonstrates how to use REBOL ports to transfer one line of text from a client to a server program. This example is intended to run on a single computer, for demonstration, so the word "localhost" is used to represent the IP address of the server (that's a standard convention used to refer to any computer's own local IP address). If you want to run this on two separate computers connected via a local area network, you'll need to obtain the IP address of the server machine (use the code above), and replace the word "localhost" with that number:
Here's the SERVER program. Be sure to run it before starting the client, or you will receive an error:
; Open network port 55555, in line mode (this mode expects full lines
; of text delineated by newline characters):
server: open/lines tcp://:55555
; Wait for a connection to the above port:
wait server
; Assign a label to the first connection made to the above port:
connection: first server
; Get the data which has been inserted into the above port object:
data: first connection
; Display the inserted data:
alert rejoin ["Text received: " data]
; Close the server
close server
Here's the CLIENT. Run it in a separate instance of the REBOL interpreter, after the above program has been started:
; Open the port created by the server above (replace "localhost" with
; an IP address if running these scripts on separate machines):
server-port: open/lines tcp://localhost:55555
; Insert some text into the port:
insert server-port "Hello Mr. Watson."
; Close the port:
close server-port
Typically, servers will continuously wait for data to appear in a port, and repeatedly do something with that data. The scripts below extend the above example with forever loops to continuously send, receive, and display messages transferred from client(s) to the server. This type of loop forms the basis for most peer-to-peer and client-server network applications. Type "end" in the client program below to quit both the client and server.
Here's the server program (run it first):
server: open/lines tcp://:55555 ; Open a TCP network port.
print "Server started...^/"
connection: first wait server ; Label the first connection.
forever [
data: first connection ; Get a line of data.
print rejoin ["Text received: " data] ; Display it.
if find data "end" [
close server ; End the program if the
print "Server Closed" ; client user typed "end".
halt
]
]
Here's the client program. Run it only after the server program has been started, and in a separate instance of the REBOL interpreter (or on a separate computer):
server-port: open/lines tcp://localhost:55555 ; Open the server port.
forever [
user-text: ask "Enter some text to send: "
insert server-port user-text ; Transfer the data.
if user-text = "end" [
close server-port ; End the program if the
print "Client Closed" ; user typed "end".
halt
]
]
It's important to understand that REBOL servers like the one above can interact independently with more than one simultaneous client connection. The "connection" definition waits until a new client connects, and returns a port representing that first client connection. Once that occurs, "connection" refers to the port used to accept data transferred by the already connected client. If you want to add more simultaneous client connections during the forever loop, simply define another "first wait server". Try running the server below, then run two simultaneous instances of the above client:
server: open/lines tcp://:55555 ; Open a TCP network port.
print "Now start TWO clients..."
connection1: first wait server ; Label the first connection.
connection2: first wait server ; Label the second connection.
forever [
data1: first connection1 ; Get a line of client1 data
data2: first connection2 ; Get a line of client2 data
print rejoin ["Client1: " data1]
print rejoin ["Client2: " data2]
if find data1 "end" [
close server ; End the program if the
print "Server Closed" ; client user typed "end".
halt
]
]
Here's an example that demonstrates how to send data back and forth (both directions), between client and server. Again, run both programs in separate instances of the REBOL interpreter, and be sure to start the server first:
; Server:
print "Server started...^/"
port: first wait open/lines tcp://:55555
forever [
user-text: ask "Enter some text to send: "
insert port user-text
if user-text = "end" [close port print "^/Server Closed^/" halt]
wait port
print rejoin ["^/Client user typed: " first port "^/"]
]
; Client:
port: open/lines tcp://localhost:55555
print "Client started...^/"
forever [
user-text: ask "Enter some text to send: "
insert port user-text
if user-text = "end" [close port print "^/Client Closed^/" halt]
wait port
print rejoin ["^/Server user typed: " first port "^/"]
]
The following short script combines many of the techniques demonstrated so far. It can act as either server or client, and can send messages (one at a time), back and forth between the server and client:
do [
either find ask "Server or client? " "s" [
port: first wait open/lines tcp://:55555 ; server
] [
port: open/lines tcp://localhost:55555 ; client
]
forever [
insert port ask "Send: "
print join "Received: "first wait port
]
]
The following script is a complete GUI network instant message application. Unlike the FTP Chat Room presented earlier, the text in this application is sent directly between two computers, across a network socket connection (users of the FTP chat room never connect directly to one another - they simply connect to a universally available third party FTP server):
view layout [
btn "Set client/server" [
ip: request-text/title/default trim {
Server IP (leave EMPTY to run as SERVER):
} (to-string read join dns:// read dns://)
either ip = "" [
port: first wait open/lines tcp://:55555 z: true ; server
] [
port: open/lines rejoin [tcp:// ip ":55555"] z: true
]
]
r: area rate 4 feel [
engage: func [f a e] [
if a = 'time and value? 'z [
if error? try [x: first wait port] [quit]
r/text: rejoin [form x newline r/text] show r
]
]
]
f: field "Type message here..."
btn "Send" [insert port f/text]
]
Here's an even more compact version (probably the shortest instant messenger program you'll ever see!):
view layout [ across
q: btn "Serve"[focus g p: first wait open/lines tcp://:8 z: 1]text"OR"
k: btn "Connect"[focus g p: open/lines rejoin[tcp:// i/text ":8"]z: 1]
i: field form read join dns:// read dns:// return
r: area rate 4 feel [engage: func [f a e][if a = 'time and value? 'z [
if error? try [x: first wait p] [quit]
r/text: rejoin [x "^/" r/text] show r
]]] return
g: field "Type message here [ENTER]" [insert p value focus face]
]
And here's an extended version of the above script that uploads your chosen user name, WAN/LAN IP, and port numbers to an FTP server, so that that info can be shared with others online (which enables them to find and connect with you). Connecting as server uploads the user info and starts the application in server mode. Once that is done, others can click the "Servers" button to retrieve and manually enter your connection info (IP address and port), to connect as client. By using different port numbers and user names, multiple users can connect to other multiple users, anywhere on the Internet:
server-list: ftp://username:password@yoursite.com/public_html/im.txt ;edit
view layout [ across
q: btn "Serve" [
parse read http://guitarz.org/ip.cgi[thru<title>copy p to</title>]
parse p [thru "Your IP Address is: " copy pp to end]
write/append server-list rejoin [
b/text " " pp " " read join dns:// read dns://" " j/text "^/"
]
focus g p: first wait open/lines join tcp:// j/text z: 1
] text "OR"
k: btn "Connect" [
focus g p: open/lines rejoin [tcp:// i/text j/text] z: 1
]
b: field 85 "Username"
i: field 98 form read join dns:// read dns://
j: field 48 ":8080" return
r: area rate 4 feel [engage: func [f a e][if a = 'time and value? 'z [
if error? try [x: first wait p] [quit]
r/text: rejoin [x "^/" r/text] show r
]]] return
g: field "Type message here [ENTER]" [insert p value focus face]
tabs 181 tab btn "Servers" [print read server-list]
]
If you want to run scripts like these between computers connected to the Internet by broadband routers, you'll likely need to learn how to "forward" ports from your router to the IP address of the machine running your server program. In most situations where a router connects a local home/business network to the Internet, only the router device has an IP address which is visible on the Internet. The computers themselves are all assigned IP addresses that are only accessible within the local area network. Port forwarding allows you to send data coming to the IP address of the router (the IP which is visible on the Internet), on a unique port, to a specific computer inside the local area network. A full discussion of port forwarding is beyond the scope of this tutorial, but it's easy to learn how to do - just type "port forwarding" into Google. You'll need to learn how to forward ports on your particular brand and model of router.
With any client-server configuration, only the server machine needs to have an exposed IP address or an open router/firewall port. The client machine can be located behind a router or firewall, without any forwarded incoming ports.
Another option that enables network applications to work through routers is "VPN" software. Applications such as hamachi, comodo, and OpenVPN allow you to connect two separate LAN networks across the Internet, and treat all the machines as if they are connected locally (connect to any computer in the VPN using a local IP address, such as 192.168.1.xxx). VPN software also typically adds a layer of security to the data sent back and forth between the connected machines. The down side of VPN software is that data transmission can be slower than direct connection using port forwarding (the data travels through a third party server).
16.4.1 Peer-to-Peer Instant Messenger
The following text message example contains line by documentation of various useful coding techniques. For instructions, see the help documentation included in the code.
REBOL [Title: "Peer-to-Peer Instant Messenger"]
; The following line sets a flag variable, used to mark whether or not
; the two machines have already connected. It helps to more gracefully
; handle connection and shutdown actions throughout the script:
connected: false
; The code below traps the close button (just a variation of the routine
; used in the earlier listview example). It assures that all open ports
; are closed, and sends a message to the remote machine that the
; connection has been terminated. Notice that the lines in the disconnect
; message are sent in reverse order. When they're received by the other
; machine, they're printed out one at a time, each line on top of the
; previous - so it appears correctly when viewed on the other side:
insert-event-func closedown: func [face event] [
either event/type = 'close [
if connected [
insert port trim {
*************************************************
AND RECONNECT.
YOU MUST RESTART THE APPLICATION
TO CONTINUE WITH ANOTHER CHAT,
THE REMOTE PARTY HAS DISCONNECTED.
*************************************************
}
close port
if mode/text = "Server Mode" [close listen]
]
quit
] [event]
]
view/new center-face gui: layout [
across
at 5x2 ; this code positions the following items in the GUI
; The text below appears as a menu option in the upper
; left hand corner of the GUI. When it's clicked, the
; text contained in the "display" area is saved to a
; user selected file:
text bold "Save Chat" [
filename: to-file request-file/title/file/save trim {
Save file as:} "Save" %/c/chat.txt
write filename display/text
]
; The text below is another menu option. It displays
; the user's IP address when clicked. It relies on a
; public web server to find the external address.
; The "parse" command is used to extract the IP address
; from the page. Parse is covered in a separate
; dedicated section later in the tutorial.
text bold "Lookup IP" [
parse read http://guitarz.org/ip.cgi [
thru <title> copy my-ip to </title>
]
parse my-ip [
thru "Your IP Address is: " copy stripped-ip to end
]
alert to-string rejoin [
"External: " trim/all stripped-ip " "
"Internal: " read join dns:// read dns://
]
]
; The text below is a third menu option. It displays
; the help text when clicked.
text bold "Help" [
alert {
Enter the IP address and port number in the fields
provided. If you will listen for others to call you,
use the rotary button to select "Server Mode" (you
must have an exposed IP address and/or an open port
to accept an incoming chat). Select "Client Mode" if
you will connect to another's chat server (you can do
that even if you're behind an unconfigured firewall,
router, etc.). Click "Connect" to begin the chat.
To test the application on one machine, open two
instances of the chat application, leave the IP set
to "localhost" on both. Set one instance to run as
server, and the other as client, then click connect.
You can edit the chat text directly in the display
area, and you can save the text to a local file.
}
]
return
; Below are the widgets used to enter connection info.
; Notice the labels assigned to each item. Later, the
; text contained in these widgets is referred to as
; <label>/text. Take a good look at the action block
; for the rotary button too. Whenever it's clicked,
; it either hides or shows the other widgets. When in
; server mode, no connection IP address is needed - the
; application just waits for a connection on the given
; port. Hiding the IP address field spares the user some
; confusion.
lab1: h3 "IP Address:" IP: field "localhost" 102
lab2: h3 "Port:" portspec: field "9083" 50
mode: rotary 120 "Client Mode" "Server Mode" [
either value = "Client Mode" [
show lab1 show IP
][
hide lab1 hide IP
]
]
; Below is the connect button, and the large action block
; that does most of the work. When the button is clicked,
; it's first hidden, so that the user isn't tempted to
; open the port again (that would cause an error). Then,
; a TCP/IP port is opened - the type (server/client) is
; determined using an "either" construct. If an error
; occurs in either of the port opening operations, the
; error is trapped and the user is alerted with a message -
; that's more graceful and informative than letting the
; program crash with an error. Notice that the IP
; address and port info are gathered from the fields above.
; If the server mode is selected (i.e., if the "mode" button
; above isn't displaying the text "Client Mode"), then the
; the TCP ports are opened in listening mode - waiting
; for a client to connect. If the client mode is selected,
; an attempt is made to open a direct connection to the IP
; address and port selected.
cnnct: button red "Connect" [
hide cnnct
either mode/text = "Client Mode" [
if error? try [
port: open/direct/lines/no-wait to-url rejoin [
"tcp://" IP/text ":" portspec/text]
][alert "Server is not responding." return]
][
if error? try [
listen: open/direct/lines/no-wait to-url rejoin [
"tcp://:" portspec/text]
wait listen
port: first listen
][alert "Server is already running." return]
]
; After the ports have been opened, the text entry field
; is highlighted, and the connection flag is set to true.
; Focusing on the text entry field provides a nice visual
; cue to the user that the connection has been made, but
; it's not required.
focus entry
connected: true
; The forever loop below continuously waits for data to
; appear in the open network connection. Whenever data
; is inserted on the other side, it's copied and
; appended to the current text in the display area, and
; then the display area is updated to show the new text.
forever [
wait port
foreach msg any [copy port []] [
display/text: rejoin [
">>> "msg newline display/text]
]
show display
]
]
; Below are the display area and text entry fields. Notice
; the labels assigned to each. The "return"s just put each
; widget on a new line in the GUI (because the layout mode
; is set to "across" above).
return display: area "" 537x500
return entry: field 428 ; the numbers are pixel sizes
; The send button below does some more important work.
; First, it checks to see if the connection has been made
; (using the flag set above). If so, it inserts the text
; contained in the "entry" field above into the open TCP/IP
; port, to be picked up by the remote machine - if the
; connection has been made, the program on the other end
; is waiting to read any data inserted into that port.
; After sending the data across the network connection,
; the text is appended to the local current text display
; area, and the display is updated:
button "Send Text" [
if connected [
insert port entry/text focus entry
display/text: rejoin [
"<<< " entry/text newline display/text]
show display
]
]
]
show gui do-events ; these are required because the "/new"
; refinement is used above.
16.5 Transferring Binary Files Through TCP Network Sockets
These 2 scripts based on http://www.rebol.net/cookbook/recipes/0058.html, by Carl Sassenrath (edited and condensed here), demonstrate how to transfer binary files directly between any two networked computers (across a TCP socket connection), using ports. Sending binary files is different from sending text in that the length of the file must be transmitted before sending the file. That must be done so that the receiving code knows when the complete file has been transmitted. The sending script below appends the file length information, and the file name, to the data being sent. The receiving script searches for that information, then receives the specified amount of binary data and saves it to a file when complete:
REBOL [Title: "Server/Receiver"]
p: ":8000" ; port #
print "receiving"
data: copy wait client: first port: wait open/binary/no-wait join tcp:// p
info: load to-string copy/part data start: find data #""
remove/part data next start
while [info/2 > length? data] [append data copy client]
write/binary (to-file join "transferred-" (second split-path info/1)) data
insert client "done" wait client close client close port print "Done" halt
REBOL [Title: "Client/Sender"]
ip: "localhost" p: ":8000" ; IP address and port #
print "sending"
data: read/binary file: to-file request-file
server: open/binary/no-wait rejoin [tcp:// ip p]
insert data append remold [file length? data] #""
insert server data
wait server close server print "Done" halt
Here's a more compact example that demonstrates how to create and send an image from one computer to another:
; server/receiver - run first:
if error? try [port: first wait open/binary/no-wait tcp://:8] [quit]
mark: find file: copy wait port #""
length: to-integer to-string copy/part file mark
while [length > length? remove/part file next mark] [append file port]
view layout [image load file]
; client/sender - run after server (change IP address if using on 2 pcs):
save/png %image.png to-image layout [box blue "I traveled through ports!"]
port: open/binary/no-wait tcp://127.0.0.1:8 ; adjust this IP address
insert file: read/binary %image.png join l: length? file #""
insert port file
The following program is a walkie-talkie push-to-talk type of voice over IP application. It's extremely simple - it just records sound from mic to .wav file, then transfers the wave file to another IP (where the same program is running), for playback. Sender and receiver open in separate processes, and both run in forever loops to enable continuous communication back and forth. As it stands, this is a MS Windows only application. The code which handles the sound recording is discussed in more detail in the section of this tutorial about DLLs:
REBOL [Title: "Intercom (VOIP Messenger)"]
write %wt-receiver.r {
REBOL []
print join "Receiving at " read join dns:// read dns://
if error? try[c: first t: wait open/binary/no-wait tcp://:8000][quit]
s: open sound://
forever [
d: copy wait c
if error? try [i: load to-string copy/part d start: find d #""] [
print "^lclient closed" close t close c close s wait 1 quit
]
remove/part d next start
while [i/2 > length? d] [append d copy c]
write/binary (to-file join "t-" (second split-path i/1))
decompress to-binary d
insert s load %t-r.wav wait s
]
}
launch %wt-receiver.r
lib: load/library %winmm.dll
mciExecute: make routine! [c [string!] return: [logic!]] lib "mciExecute"
if (ip: ask "Connect to IP (none = localhost): ") = "" [ip: "localhost"]
if error? try [s: open/binary/no-wait rejoin [tcp:// ip ":8000"]] [quit]
mciExecute "open new type waveaudio alias buffer1 buffer 4"
forever [
x: ask "^lPress [ENTER] to start sending sound (or 'q' to quit): "
if find x "q" [close s free lib break]
; if (ask "^lPress [ENTER] to send sound ('q' to quit): ") = "q"[quit]
mciExecute "record buffer1"
ask "^l*** YOU ARE NOW RECORDING SOUND *** Press [ENTER] to send: "
mciExecute join "save buffer1 " to-local-file %r.wav
mciExecute "delete buffer1 from 0"
data: compress to-string read/binary %r.wav
insert data append remold [%r.wav length? data] #""
insert s data
]
Here's a more compact version of the above application, with hands-free operation enabled (several obfuscated versions of this script can be found at the end of this tutorial - likely the most compact VOIP programs you'll find anywhere):
REBOL [title: "VOIP"] do [write %ireceive.r {REBOL []
if error? try [port: first wait open/binary/no-wait tcp://:8] [quit]
wait 0 speakers: open sound://
forever [
if error? try [mark: find wav: copy wait port #""] [quit]
i: to-integer to-string copy/part wav mark
while [i > length? remove/part wav next mark] [append wav port]
insert speakers load to-binary decompress wav
]} launch %ireceive.r
lib: load/library %winmm.dll
mci: make routine! [c [string!] return: [logic!]] lib "mciExecute"
if (ip: ask "Connect to IP (none = localhost): ") = "" [ip: "localhost"]
if error? try [port: open/binary/no-wait rejoin [tcp:// ip ":8"]] [quit]
mci "open new type waveaudio alias wav"
forever [
mci "record wav" wait 2 mci "save wav r" mci "delete wav from 0"
insert wav: compress to-string read/binary %r join l: length? wav #""
if l > 4000 [insert port wav] ; squelch (don't send) if too quiet
]]
16.6 Transferring Data Through UDP Network Ports
"UDP" is a multicast network protocol used to transmit data to any listening program attached via a network port. Where TCP/IP programs require a server program to have a known IP address, to which all client programs connect, UDP programs simply broadcast messages to an open network port. Any program listening for UDP data on that port can receive the messages. The following program is a perfect example of UDP functionality. There's no need to connect to a central server to exchange messages. Any number of users can connect to a local network and broadcast messages to any/all others who are running this program. This enables a functional office "text intercom" system:
REBOL [Title: "UDP Group Chat Program"]
net-in: open udp://:9905 ; This is UDP, so NO known IP addresses required
net-out: open/lines udp://255.255.255.255:9905
set-modes net-out [broadcast: on]
svv/vid-face/color: white
name: request-text/title "Your name:"
prev-message: ""
gui: view/new layout [
a1: area wrap rejoin [name ", you are logged in."] across
f1: field
btn "Save Chat" [write request-file/only/save/file %chat.txt a1/text]
btn "?" [alert "Press [CTRL] + U to see who's online."]
at 0x0 key #"^M" [
if f1/text = "" [return]
insert net-out rejoin [name {, } now/time {: } f1/text]
]
at 0x0 key #"^u" [
insert net-out rejoin [name {, } now/time {: Who's online?}]
]
]
forever [
focus f1
received: wait [net-in]
if not viewed? gui [quit]
if find (message: copy received) "Who's online" [
insert net-out rejoin [name " is online."]
]
if message <> prev-message [
insert (at a1/text 1) message show a1
attempt [
insert s: open sound:// load %/c/windows/media/ding.wav
wait s close s
]
]
prev-message: copy message
]
The following set of programs are used in the author's music lesson business to alert teachers when their students have arrived for an appointment. UDP is used because any number of teachers can be notified by the central "server" program, and the server program doesn't need to know the IP addresses of any of the potential instructors' computers. It just transmits the messages to whoever is connected and listening:
REBOL [Title: "UDP Signin Client Alarm"]
if error? try [net-in: open udp://:9905] [
alert {
This program is already running. If you want to start a new
instance, please close the currently open program. If you have
any problems, close "rebol" in the task manager or just restart
your computer.
}
quit
]
svv/vid-face/color: white
name: request-list "Your name:" [
"alex" "brian" "chad" "chris" "david" "dorian" "doug" "emerald"
"jarrod" "josh" "kevin" "kyle" "lindsey" "mark" "nick" "peter"
"ryan_gaughan" "stef" "steve"
]
previous-signin: []
attempt [
insert s: open sound:// load %/c/windows/media/ding.wav
wait s close s
]
gui: view/new center-face layout [
a1: area 600x400 wrap rejoin [name ", you are logged in."]
across
btn "Save History" [
write request-file/only/save/file (to-file now/date) a1/text
]
btn "Quit" [quit]
; at 0x0 key #"^M" [if a1/text = "" [return]]
]
arrive-sound: load %/c/windows/media/ding.wav
forever [
received: wait [net-in]
if not viewed? gui [quit]
if find (message: copy received) name [
teacher: first parsed: parse copy message none
student: at (find/match copy message teacher) 2 ; erase newline
insert (at a1/text 1) rejoin [
now/time {, } now/date {: }
uppercase teacher ", your student has arrived: "
uppercase student
]
show a1
attempt [insert s: open sound:// arrive-sound wait s close s]
]
wait 1
]
This is the server required to run the program above:
REBOL [Title: "UDP Signin Server"]
net-out: open/lines udp://255.255.255.255:9905
set-modes net-out [broadcast: on]
svv/vid-face/color: white
previous-signin: []
write/append %last-signin.txt ""
gui: view/new center-face layout [
a1: area wrap "Server started" across
btn "Quit" [quit]
; at 0x0 key #"^M" [if a1/text = "" [return]]
]
forever [attempt [
if not viewed? gui [quit]
if previous-signin <> current-signin: load %last-signin.txt [
insert net-out rejoin [current-signin/1 " " current-signin/2]
previous-signin: current-signin
insert (at a1/text 1) rejoin [
"Last signed in student: " current-signin/2 newline
"Last signed in teacher: " current-signin/1 newline
]
show a1
write/append %alarm_history.txt rejoin [
now
{ student: } current-signin/2
{ teacher: } current-signin/1 newline
]
attempt [
insert s: open sound:// load %/c/windows/media/ding.wav
wait s close s
]
]
wait 1
]]
quit
{%last-signin.txt contents should be 2 strings "nick" "john smith"}
For more information on ports, see http://www.rebol.com/docs/core23/rebolcore-14.html, http://stackoverflow.com/questions/1291127/rebol-smallest-http-server-in-the-world-why-first-wait-listen-port, and http://www.rebol.net/docs/async-ports.html.
16.7 Parse (REBOL's Answer to Regular Expressions)
The "parse" function is used to import and convert organized chunks of external data into the block format that REBOL recognizes natively. It also provides a means of dissecting, searching, comparing, extracting, and acting upon organized information within unformatted text data (similar to the pattern matching functionality implemented by regular expressions in other languages).
The basic format for parse is:
parse <data> <matching rules>
Parse has several modes of use. The simplest mode just splits up text at common delimiters and converts those pieces into a REBOL block. To do this, just specify "none" as the matching rule. Common delimiters are spaces, commas, tabs, semicolons, and newlines. Here are some examples:
text1: "apple orange pear"
parsed-block1: parse text1 none
text2: "apple,orange,pear"
parsed-block2: parse text2 none
text3: "apple orange pear"
parsed-block3: parse text3 none
text4: "apple;orange;pear"
parsed-block4: parse text4 none
text5: "apple,orange pear"
parsed-block5: parse text5 none
text6: {"apple","orange","pear"}
parsed-block6: parse text6 none
text7: {
apple
orange
pear
}
parsed-block7: parse text7 none
To split files based on some character other than the common delimiters, you can specify the delimiter as a rule. Just put the delimiter in quotes:
text: "apple*orange*pear"
parsed-block: parse text "*"
text: "apple&orange&pear"
parsed-block: parse text "&"
text: "apple & orange&pear"
parsed-block: parse text "&"
You can also include mixed multiple characters to be used as delimiters:
text: "apple&orange*pear"
parsed-block: parse text "&*"
text: "apple&orange*pear"
parsed-block: parse text "*&" ; the order doesn't matter
Using the "splitting" mode of parse is a great way to get formatted tables of data into your REBOL programs. Splitting the text below by carriage returns, you run into a little problem:
text: { First Name
Last Name
Street Address
City, State, Zip}
parsed-block: parse text "^/"
; ^/ is the REBOL symbol for a carriage return
Spaces are included in the parsing rule by default (parse automatically splits at all empty space), so you get a block of data that's more broken up than intended:
["First" "Name" "Last" "Name" "Street" "Address" "City,"
"State," "Zip"]
You can use the "/all" refinement to eliminate spaces from the delimiter rule. The code below:
text: { First Name
Last Name
Street Address
City, State, Zip}
parsed-block: parse/all text "^/"
converts the given text to the following block:
[" First Name" " Last Name" " Street Address"
" City, State, Zip"]
Now you can trim the extra space from each of the strings:
foreach item parsed-block [trim item]
and you get the following parsed-block, as intended:
["First Name" "Last Name" "Street Address" "City, State, Zip"]
16.8 Using Parse to Load Speadsheet CSV Files and Other Structured Data
Parse is commonly used to convert spreadsheet data into REBOL blocks. In Excel, Open Office, and other spreadsheet programs, you can export all the columns of data in a worksheet by saving it as a CSV formatted ("comma separated value") .csv text file. People often put various bits of descriptive text, labels and column headers into spreadsheets to make them more readable:
; TITLE
DESCRIPTION
Header1
Category1 data data data Notes...
data data data
data data data
data data data
Header2
Category2 data data data Notes...
data data data
data data data
data data data
Header3
Category3 data data data Notes...
data data data
data data data
data data data
The following code turns the exported CSV spreadsheet data into a nice useable REBOL block, with group heading data added to each line:
; Read and parse the CSV formatted file:
filename: %filename.csv
data: copy []
lines: read/lines filename
foreach line lines [
append/only data parse/all line ","
]
; Add headers from sections of the spreadsheet to each line item:
info: copy ""
foreach line data [
either find "Header" line/1 [
info: line/1
][
append line info
]
]
; Remove the unwanted descriptive header lines:
remove-each line data [find "Header" line/1/1]
remove-each line data [
(line/3 = "TITLE") or (line/3 = "DESCRIPTION")
]
16.9 Using Parse's Pattern Matching Mode to Search Data
You can use parse to check whether any specific data exists within a given block. To do that, specify the rule (matching pattern) as the item you're searching for. Here's an example:
parse ["apple"] ["apple"]
parse ["apple" "orange"] ["apple" "orange"]
Both lines above evaluate to true because they match exactly. IMPORTANT: By default, as soon as parse comes across something that doesn't match, the entire expression evaluates to false, EVEN if the given rule IS found one or more times in the data. For example, the following is false:
parse ["apple" "orange"] ["apple"]
But that's just default behavior. You can control how parse responds to items that don't match. Adding the words below to a rule will return true if the given rule matches the data in the specified way:
- "any" - the rule matches the data zero or more times
- "some" - the rule matches the data one or more times
- "opt" - the rule matches the data zero or one time
- "one" - the rule matches the data exactly one time
- an integer - the rule matches the data the given number of times
- two integers - the rule matches the data a number of times included in the range between the two integers
The following examples are all true:
parse ["apple" "orange"] [any string!]
parse ["apple" "orange"] [some string!]
parse ["apple" "orange"] [1 2 string!]
You can create rules that include multiple match options - just separate the choices by a "|" character and enclose them in brackets. The following is true:
parse ["apple" "orange"] [any [string! | url! | number!]]
You can trigger actions to occur whenever a rule is matched. Just enclose the action(s) in parentheses:
parse ["apple" "orange"] [any [string!
(alert "The block contains a string.") | url! | number!]]
You can skip through data, ignoring chunks until you get to, or past a given condition. The word "to" ignores data UNTIL the condition is found. The word "thru" ignores data until JUST PAST the condition is found. The following is true:
parse [234.1 $50 http://rebol.com "apple"] [thru string!]
The real value of pattern matching is that you can search for and extract data from unformatted text, in an organized way. The word "copy" is used to assign a variable to matched data. For example, the following code downloads the raw HTML from the REBOL homepage, ignores everything except what's between the HTML title tags, and displays that text:
parse read http://rebol.com [
thru <title> copy parsed-text to </title> (alert parsed-text)
]
The following code extends the example above to provide the useful feature of displaying the external ip address of the local computer. It reads http://guitarz.org/ip.cgi, parses out the title text, and then parses that text again to return only the IP number. The local network address is also displayed, using the built in dns protocol in REBOL:
parse read http://guitarz.org/ip.cgi [
thru <title> copy my-ip to </title>
]
parse my-ip [
thru "Your IP Address is: " copy stripped-ip to end
]
alert to-string rejoin [
"External: " trim/all stripped-ip " "
"Internal: " read join dns:// read dns://
]
The following example downloads and parses the current (live) US Dollar exchange rates from http://x-rates.com. The user selects from a list of currencies to convert to, then performs and displays the conversion from USD to the selected currency. The first half of the script is the simple Calculator GUI example taken from the first part of the tutorial. All of the parsing occurs when the "Convert" button is clicked:
REBOL [title: "Currency Rate Conversion Calculator"]
view center-face layout [
origin 0 space 0x0 across
f: field 200x40 font-size 20
return
style btn btn 50x50 [append f/text face/text show f]
btn "1" btn "2" btn "3" btn " + " return
btn "4" btn "5" btn "6" btn " - " return
btn "7" btn "8" btn "9" btn " * " return
btn "0" btn "." btn " / " btn "=" [
attempt [f/text: form do f/text show f]
] return
btn 200x35 "Convert" [
x: copy []
html: read http://www.x-rates.com/table/?from=USD&amount=1.00
html: find html "src='/themes/bootstrap/images/xrates_sm_tm.png'"
parse html [
any [
thru {from=USD} copy link to {</a>} (append x link)
] to end
]
rates: copy []
foreach rate x [
parse rate [thru {to=} copy c to {'>}]
parse rate [thru {'>} copy v to end]
if not error? try [to-integer v] [append rates reduce [c v]]
]
currency: request-list "Select Currency:" extract rates 2
rate: to-decimal select rates currency
attempt [alert rejoin [currency ": " (rate * to-decimal f/text)]]
]
]
Here's a useful example that removes all comments from a given REBOL script (any part of a line that begins with a semicolon ";"):
code: read to-file request-file
parse/all code [any [
to #";" begin: thru newline ending: (
remove/part begin ((index? ending) - (index? begin))) :begin
]
]
editor code
For more about parse, see the following links:
http://www.codeconscious.com/rebol/parse-tutorial.html
http://www.rebol.com/docs/core23/rebolcore-15.html
http://en.wikibooks.org/wiki/REBOL_Programming/Language_Features/Parse
http://www.rebolforces.com/zine/rzine-1-06.html#sect4.
16.10 Responding to Special Events in a GUI - "Feel"
REBOL's simple GUI syntax makes it easy for widgets to respond to mouse clicks. As you've seen, you can simply put the block of code you want evaluated immediately after the widget that activates it:
view layout [btn "Click me" [alert "Thank you for the click :)"]]
But what if you want your GUI to respond to events other than a mouse click directly on a widget? What if, for example, you want the program to react whenever a user clicks anywhere on the GUI screen (in a paint program, for example), or if you want a widget to do something after a certain amount of time has passed, or if you want to capture clicks on the GUI close button so that the user can't accidentally shut down an important data screen. That's what the "feel" object and "insert-event-func" function are used for.
Here's an example of the basic feel syntax:
view layout [
text "Click, right-click, and drag the mouse over this text." feel [
engage: func [face action event] [
print action
print event/offset
]
]
]
The above code is often shortened using "f a e" to represent "face action event":
view layout [
text "Mouse me." feel [
engage: func [f a e] [
print a
print e/offset
]
]
]
You can respond to specific events as follows:
view layout [
text "Mouse me." feel [
engage: func [f a e] [
if a = 'up [print "You just released the mouse."]
]
]
]
This example demonstrates how to combine full screen mouse detection with normal mouse clicks on widgets. To do this, an invisible box the same size as the screen, with a feel event attached, is used for full screen detection. Then, other widgets are simply placed on top of it, starting over at the window origin:
print "Click anywhere in the window, then click the text."
view center-face layout [
size 400x200
box 400x200 feel [
engage: func [f a e] [
print a
print e/offset
]
]
origin
text "Click me" [print "Text clicked"] [print "Text right-clicked"]
box blue [print "Box clicked"]
]
You can also assign timer events to any widget, as follows:
view layout [
text "This text has a timer event attached." rate 00:00:00.5 feel [
engage: func [f a e] [
if a = 'time [print "1/2 second has passed."]
]
]
]
Here's a button with a time event attached (a rate of "0" means don't wait at all). Every 0 seconds, when the timer event is detected, the offset (position) of the button is updated. This creates animation:
view layout/size [
mover: btn rate 0 feel [
engage: func [f a e] [
if a = 'time [
mover/offset: mover/offset + 5x5
show mover
]
]
]
] 400x400
Here's a little shooting game that uses a timer event to automate the movement of GUI graphics around the screen, check for collisions, and control other game operations:
REBOL [title: "VID Shooter"]
score: 0 speed: 20 fire: false
do game: [
view center-face layout [
size 600x440
at 270x0 text join "Score: " score
at 280x440 x: box 2x20 yellow
at (as-pair 0 (random 300) + 30) y: btn 50x20 red "Enemy"
at 280x420 z: btn 50x20 blue "Player"
box 0x0 #"l" [z/offset: z/offset + 10x0 show z]
box 0x0 #"k" [z/offset: z/offset + -10x0 show z]
box 0x0 #" " [
if fire = false [
fire: true
x/offset: as-pair z/offset/1 440
]
]
box 0x0 rate speed feel [
engage: func [f a e] [
if a = 'time [
y/offset: y/offset + 5x0
if y/offset/1 > 600 [
y/offset: as-pair -10 ((random 300) + 30)
]
show y
if fire = true [x/offset: x/offset + 0x-20]
if x/offset/2 < 0 [
x/offset/2: 440
fire: false
]
show x
if within? x/offset y/offset 50x25 [
alert "Kablammmm!!!"
score: score + 1
speed: speed + 5
fire: false
unview
do game
]
]
]
]
]
]
By updating the offset of a widget every time it's clicked, you can enable drag-and-drop operations:
view layout/size [
text "Click and drag this text" feel [
; remember f="face", a="action", e="event":
engage: func [f a e] [
; first, record the coordinate at which the mouse is
; initially clicked:
if a = 'down [initial-position: e/offset]
; if the mouse is moved while holding down the button,
; move the position of the clicked widget the same amount
; (the difference between the initial clicked coordinate
; recorded above, and the new current coordinate determined
; whenever a mouse move event occurs):
if find [over away] a [
f/offset: f/offset + (e/offset - initial-position)
]
show f
]
]
] 600X440
Feel objects and event functions can be included right inside a style definition. The definition below allows you to easily create multiple GUI widgets that can be dragged around the screen. "movestyle" is defined as a block of code that's later passed to a widget's "feel" object, and is therefore included in the overall style definition (the remove and append functions have been added here to place the moved widget on top of other widgets in the GUI (i.e., to bring the dragged widget to the visual foreground)). You can add this "feel movestyle" code to any GUI widget to make it drag-able:
movestyle: [
engage: func [f a e] [
if a = 'down [
initial-position: e/offset
remove find f/parent-face/pane f
append f/parent-face/pane f
]
if find [over away] a [
f/offset: f/offset + (e/offset - initial-position)
]
show f
]
]
view layout/size [
style moveable-object box 20x20 feel movestyle
; "random 255.255.255" represents a different random
; color for each piece:
at random 600x400 moveable-object (random 255.255.255)
at random 600x400 moveable-object (random 255.255.255)
at random 600x400 moveable-object (random 255.255.255)
at random 600x400 moveable-object (random 255.255.255)
at random 600x400 moveable-object (random 255.255.255)
text "This text and all the boxes are movable" feel movestyle
] 600x440
The "detect" function inside a feel block is useful for constantly checking events. The following program constantly checks for mouse movements, and if the mouse is ever positioned over the button, the button is moved to a random position. This technique can be useful, for example, in video games controlled by mouse movement:
view center-face layout [
size 600x440
at 270x209 b: btn "Click Me!" feel [
detect: func [f e] [
; The following line checks for any mouse movement:
if e/type = 'move [
; This line checks if the mouse position is within the
; coordinates of the button (i.e., touching the button):
if (within? e/offset b/offset 59x22) [
; If so, move the button to a random position:
b/offset: b/offset + ((random 50x50) - (random 50x50))
; Check if the button has been moved off screen:
if not within? b/offset -59x-22 659x462 [
; If so, move back to the center of the window:
b/offset: 270x209
]
; Update the screen:
show b
]
]
; When using the detect function, always return the event:
e
]
]
]
To handle global events in a GUI such as resizing and closing, "insert-event-func" is useful. The following example checks for resize events:
insert-event-func [
either event/type = 'resize [
alert "I've been resized"
none ; return this value when you don't want to
; do anything else with the event.
][
event ; return this value if the specified event
; is not found
]
]
view/options layout [text "Resize this window."] [resize]
You can use that technique to adjust the window layout, and specifically reposition widgets when a screen is resized:
insert-event-func [
either event/type = 'resize [
stay-here/offset:
stay-here/parent-face/size - stay-here/size - 20x20
show stay-here
none ; return this value when you don't want to
; do anything else with the event.
][
event ; return this value if the specified event
; is not found
]
]
view/options layout [
stay-here: text "Resize this window."
] [resize]
To remove an installed event handler, use "remove-event-func". The following example captures three consecutive close events, and then removes the event handler, allowing you to close the GUI on the 4th try:
count: 1
evtfunc: insert-event-func [
either event/type = 'close [
if count = 3 [remove-event-func :evtfunc]
count: count + 1
none
][
event
]
]
view layout [text "Try to close this window 4 times."]
For more information about handling events see http://www.rebol.com/how-to/feel.html, http://www.codeconscious.com/rebol/view-notes.html, and http://www.rebol.com/docs/view-system.html.
16.11 2D Drawing, Graphics, and Animation
With REBOL's "view layout" ("VID") dialect you can easily build graphic user interfaces that include buttons, fields, text lists, images and other GUI widgets, but it's not meant to handle general purpose graphics or animation. For that purpose, REBOL includes a built-in "draw" dialect. Various drawing functions allow you to make lines, boxes, circles, arrows, and virtually any other shape. Fill patterns, color gradients, and effects of all sorts can be easily applied to drawings.
Implementing draw functions typically involves creating a 'view layout' GUI, with a box widget that's used as the viewing screen. "Effect" and "draw" functions are then added to the box definition, and a block is passed to the draw function which contains more functions that actually perform the drawing of various shapes and other graphic elements in the box. Each draw function takes an appropriate set of arguments for the type of shape created (coordinate values, size value, etc.). Here's a basic example of the draw format:
view layout [box 400x400 effect [draw [line 10x39 322x211]]]
; "line" is a draw function
Here's the exact same example indented and broken apart onto several lines:
view layout [
box 400x400 effect [
draw [
line 10x39 322x211
]
]
]
Any number of shape elements (functions) can be included in the draw block:
view layout [
box 400x400 black effect [
draw [
line 0x400 400x50
circle 250x250 100
box 100x20 300x380
curve 50x50 300x50 50x300 300x300
spline closed 3 20x20 200x70 150x200
polygon 20x20 200x70 150x200 50x300
]
]
]
Color can be added to graphics using the "pen" function. Shapes can be filled with color, with images, and with other graphic elements using the "fill-pen" function. The thickness of drawn lines is set with the "line-width" function:
view layout [
box 400x400 black effect [
draw [
pen red
line 0x400 400x50
pen white
box 100x20 300x380
fill-pen green
circle 250x250 100
pen blue
fill-pen orange
line-width 5
spline closed 3 20x20 200x70 150x200
polygon 20x20 200x70 150x200 50x300
]
]
]
Gradients and other effects can be easily applied to the elements:
view layout [
box 400x220 effect [
draw [
fill-pen 200.100.90
polygon 20x40 200x20 380x40 200x80
fill-pen 200.130.110
polygon 20x40 200x80 200x200 20x100
fill-pen 100.80.50
polygon 200x80 380x40 380x100 200x200
]
gradmul 180.180.210 60.60.90
]
]
Drawn shapes are automatically anti-aliased (lines are smoothed), but that default feature can be disabled:
view layout [
box 400x400 black effect [
draw [
; with default smoothing:
circle 150x150 100
; without smoothing:
anti-alias off
circle 250x250 100
]
]
]
16.11.1 Animation
Animations can be created with draw by changing the coordinates of image elements. The fundamental process is as follows:
- Assign a word label to the box in which the drawing takes place (the word "scrn" is used in the following examples).
- Create a new draw block in which the characteristics of the graphic elements (position, size, etc.) are changed.
- Assign the new block to "{yourlabel}/effect/draw" (i.e., "scrn/label/draw: [changed draw block]" in this case).
- Display the changes with a "show {yourlabel}" function (i.e., "show scrn" in this case).
Here's a basic example that moves a circle to a new position when the button is pressed:
view layout [
scrn: box 400x400 black effect [draw [circle 200x200 20]]
btn "Move" [
scrn/effect/draw: [circle 200x300 20] ; replace the block above
show scrn
]
]
Variables can be assigned to positions, sizes, and/or other characteristics of draw elements, and loops can be used to create smooth animations by adjusting those elements incrementally:
pos: 200x50
view layout [
scrn: box 400x400 black effect [draw [circle pos 20]]
btn "Move Smoothly" [
loop 50 [
; increment the "y" value of the coordinate:
pos/y: pos/y + 1
scrn/effect/draw: copy [circle pos 20]
show scrn
]
]
]
Animation coordinates (and other draw properties) can also be stored in blocks:
pos: 200x200
coords: [70x346 368x99 143x45 80x125 237x298 200x200]
view layout [
scrn: box 400x400 black effect [draw [circle pos 20]]
btn "Jump Around" [
foreach coord coords [
scrn/effect/draw: copy [circle coord 20]
show scrn
wait 1
]
]
]
Other data sources can also serve to control movement. In the next example, user data input moves the circle around the screen. Notice the use of the "feel" function to update the screen every 10th of a second ("rate 0:0:0.1"). Since feel is used to watch, wait for, and respond to window events, you'll likely need it in many situations where animation is used, such as in games:
pos: 200x200
view layout [
scrn: box 400x400 black rate 0:0:0.1 feel [
engage: func [face action event] [
if action = 'time [
scrn/effect/draw: copy []
append scrn/effect/draw [circle pos 20]
show scrn
]
]
] effect [ draw [] ]
across
btn "Up" [pos/y: pos/y - 10]
btn "Down" [pos/y: pos/y + 10]
btn "Right" [pos/x: pos/x + 10]
btn "Left" [pos/x: pos/x - 10]
]
Here's a very simple paint program that also uses the feel function. Whenever a mouse-down action is detected, the coordinate of the mouse event ("event/offset") is added to the draw block (i.e., a new dot is added to the screen wherever the mouse is clicked), and then the block is shown:
view layout [
scrn: box black 400x400 feel [
engage: func [face action event] [
if find [down over] action [
append scrn/effect/draw event/offset
show scrn
]
if action = 'up [append scrn/effect/draw 'line]
]
] effect [draw [line]]
]
A useful feature of draw is the ability to easily scale and distort images simply by indicating 4 coordinate points. The image will be altered to fit into the space marked by those four points:
view layout [
box 400x400 black effect [
draw [
image logo.gif 10x10 350x200 250x300 50x300
; "logo.gif" is built into the REBOL interpreter
]
]
]
Here's an example that incorporates the image scaling technique above with some animation. IMPORTANT: In the following example, the coordinate position calculations occur inside the draw block. Whenever such evaluations occur inside a draw block (i.e., when values are added or subtracted to a variable coordinate position, size, etc.), a "reduce" or "compose" function must be used to evaluate those values. Notice the tick mark (') next to the "image" function. Function words inside a reduced block need to be marked with that symbol to evaluate correctly:
pos: 300x300
view layout [
scrn: box pos black effect [
draw [image logo.gif 0x0 300x0 300x300 0x300]
]
btn "Animate" [
for point 1 140 1 [
scrn/effect/draw: copy reduce [
'image logo.gif
(pos - 300x300)
(1x1 + (as-pair 300 point))
(pos - (as-pair 1 point))
(pos - 300x0)
]
show scrn
]
for point 1 300 1 [
scrn/effect/draw: copy reduce [
'image logo.gif
(1x1 + (as-pair 1 point))
(pos - 0x300)
(pos - 0x0)
(pos - (as-pair point 1))
]
show scrn
]
; no "reduce" is required below, because no calculations
; occur in the draw block - they're just static coords:
scrn/effect/draw: copy [
image logo.gif 0x0 300x0 300x300 0x300
]
show scrn
]
]
Here's another example of a draw block which contains evaluated calculations, and therefore requires "reduce"d evaluation:
view layout [
scrn: box 400x400 black effect [draw [line 0x0 400x400]]
btn "Spin" [
startpoint: 0x0
endpoint: 400x400
loop 400 [
scrn/effect/draw: copy reduce [
'line
startpoint: startpoint + 0x1
endpoint: endpoint - 0x1
]
show scrn
]
]
]
The useful little paint program at http://rebol.org/cgi-bin/cgiwrap/rebol/view-script.r?script=paintplus.r consists of only 238 lines of code. Take a look at it to see how efficient REBOL's draw code is:
url: http://rebol.org/cgi-bin/cgiwrap/rebol/download-a-script.r?
script: "script-name=paintplus.r"
do rejoin [url script]
paint none []
The paint program above is actually a really useful tool. You can load images on which to paint, and save edits to be reloaded later:
paint load %myimage.png load %edits.txt
Take a look at the 100 draw examples script to see many more bits of useful draw code.
For more information about built-in shapes, functions, and capabilities of draw, see http://www.rebol.com/docs/draw-ref.html, http://www.rebol.com/docs/draw.html, http://translate.google.com/translate?hl=en&sl=fr&u=http://www.rebolfrance.info/org/articles/login11/login11.htm (translated by Google), http://www.rebolforces.com/zine/rzine-1-05.html, http://www.rebolforces.com/zine/rzine-1-06.html (updated code for these two tutorials is available at http://mail.rebol.net/maillist/msgs/39100.html). A nice, short tutorial demonstrating how to build multi-player, networked games with draw graphics is available at RebolFrance (translated by Google). Also be sure to see http://www.nwlink.com/~ecotope1/reb/easy-draw.r (a clickable rebsite version is available in the REBOL Desktop -> Docs -> Easy Draw).
16.12 3D Graphics with r3D
The "r3D" modeling engine by Andrew Hoadley is built entirely from native REBOL 2D draw functions. It demonstrates the significantly powerful potential of draw. It was demonstrated earlier in the tutorial with useful a 3D plotting script. The examples below show some more of what you can accomplish with r3D:
do http://www.rebol.net/demos/BF02D682713522AA/i-rebot.r
do http://www.rebol.net/demos/BF02D682713522AA/histogram.r
do http://www.rebol.net/demos/BF02D682713522AA/objective.r
The r3D engine is small. Here's the entire module in compressed, embeddable format (this is all just standard REBOL code compressed into a more compact format). To enable 3D graphics in your REBOL programs, just include this text in your code (paste it, or "do" it from a file). If you'd like to read and learn from the pure REBOL code that makes up this module, see the examples above (the r3D module is included in those examples as regular text code):
do to-string decompress 64#{
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
Rf8G28Udt8glAAA=
}
Here's a simple example that demonstrates the basic syntax and use of r3D. Be sure to do the code above before running this example:
Transx: Transy: Transz: 300.0 ; Set some camera
Lookatx: Lookaty: Lookatz: 100.0 ; positions to
; start with.
do update: does [ ; This "update" function is where
world: copy [] ; everything is defined.
append world reduce [ ; Add your 3D objects inside this "append".
reduce [cube-model (r3d-scale 100.0 150.0 125.0) red]
] ; A red 'cube' 100x150x125 pixels is added.
camera: r3d-position-object
reduce [Transx Transy Transz]
reduce [Lookatx Lookaty Lookatz]
[0.0 0.0 1.0]
RenderTriangles: render world camera r3d-perspective 250.0 400x360
probe RenderTriangles ; This line demonstrates what's going on
] ; under the hood. You can eliminate it.
view layout [
scrn: box 400x360 black effect [draw RenderTriangles] ; basic draw
across return
slider 60x16 [Transx: (value * 600 - 300.0) update show scrn]
slider 60x16 [Transy: (value * 600 - 300.0) update show scrn]
slider 60x16 [Transz: (value * 600) update show scrn]
slider 60x16 [Lookatx: (value * 400 - 200.0) update show scrn]
slider 60x16 [Lookaty: (value * 400 - 200.0) update show scrn]
slider 60x16 [Lookatz: (value * 200 ) update show scrn]
]
R3D works by rendering 3D images to native REBOL 2D draw functions, which are contained in the "RenderTriangles" block above. R3D provides basic shape structures and a simple language interface to create and view those images in a REBOL application. It automatically adjusts lighting and other characteristics of images as they're viewed from different perspectives. To see how the rendering of images is converted into simple REBOL draw functions, watch the output of the "probe RenderTriangles" line in the REBOL interpreter as you adjust the sliders above. It displays the list of draw commands used to create each image in the moving 3D world.
In the example above, slider widgets are used to adjust values in the animation. Those values could just as easily be controlled by loops or other forms of data input. In the example below, the values are adjusted by keystrokes assigned to empty text widgets (use the "asdfghqwerty" keys to move the cube):
Transx: Transy: Transz: 2.0
Lookatx: Lookaty: Lookatz: 1.0
do update: does [
world: copy []
append world reduce [
reduce [cube-model (r3d-scale 100.0 150.0 125.0) red]
]
Rendered: render world
r3d-position-object
reduce [Transx Transy Transz]
reduce [Lookatx Lookaty Lookatz]
[0.0 0.0 1.0]
r3d-perspective 360.0 400x360
]
view layout [
across
text "" #"a" [Transx: (Transx + 10) update show scrn]
text "" #"s" [Transx: (Transx - 10) update show scrn]
text "" #"d" [Transy: (Transy + 10) update show scrn]
text "" #"f" [Transy: (Transy - 10) update show scrn]
text "" #"g" [Transz: (Transz + 10) update show scrn]
text "" #"h" [Transz: (Transz - 10) update show scrn]
text "" #"q" [Lookatx: (Lookatx + 10) update show scrn]
text "" #"w" [Lookatx: (Lookatx - 10) update show scrn]
text "" #"e" [Lookaty: (Lookaty + 10) update show scrn]
text "" #"r" [Lookaty: (Lookaty - 10) update show scrn]
text "" #"t" [Lookatz: (Lookatz + 10) update show scrn]
text "" #"y" [Lookatz: (Lookatz - 10) update show scrn]
at 20x20
scrn: box 400x360 black effect [draw Rendered]
]
The r3D module can work with models saved in native .R3d format, and the "OFF" format (established by the GeomView program at http://www.geom.uiuc.edu/projects/visualization/. See http://local.wasp.uwa.edu.au/~pbourke/dataformats/oogl/#OFF for a description of the OFF file format). A number of OFF example objects are available at http://www.mpi-sb.mpg.de/~kettner/proj/obj3d/.
To understand how to create/import and manipulate more complex 3D shapes, examine the way objects are designed inside the "update" function in each of Andrew's three examples. Here's a simplified variation of Andrew's objective.r example that loads .off models from the hard drive. Be sure to do the r3D module code above before running this example, and then try downloading and loading some of the example .off files at the web site above:
RenderTriangles: []
view layout [
scrn: box 400x360 black effect [draw RenderTriangles]
across return
slider 60x16 [Transx: (value * 600 - 300.0) update show scrn]
slider 60x16 [Transy: (value * 600 - 300.0) update show scrn]
slider 60x16 [Transz: (value * 600) update show scrn]
slider 60x16 [Lookatx: (value * 400 - 200.0) update show scrn]
slider 60x16 [Lookaty: (value * 400 - 200.0) update show scrn]
slider 60x16 [Lookatz: (value * 200 ) update show scrn]
return btn "Load Model" [
model: r3d-load-OFF load to-file request-file
modelsize: 1.0
if model/3 [modelsize: model/3]
if modelsize < 1.0 [ modelsize: 1.0 ]
defaultScale: 200.0 / modelsize
objectScaleX: objectScaleY: objectScaleZ: defaultscale
objectRotateX: objectRotateY: objectRotateZ: 0.0
objectTranslateX: objectTranslateY: objectTranslateZ: 0.0
Transx: Transy: Transz: 300.0
Lookatx: Lookaty: Lookatz: 200.0
modelWorld: r3d-compose-m4 reduce [
r3d-scale objectScaleX objectScaleY objectScaleZ
r3d-translate
objectTranslateX objectTranslateY objectTranslateZ
r3d-rotatex objectRotateX
r3d-rotatey objectRotateY
r3d-rotatez objectRotateZ
]
r3d-object: reduce [model modelWorld red]
do update: does [
world: copy []
append world reduce [r3d-object]
camera: r3d-position-object
reduce [Transx Transy Transz]
reduce [Lookatx Lookaty Lookatz]
[0.0 0.0 1.0]
RenderTriangles:
render world camera r3d-perspective 250.0 400x360
]
update show scrn
]
]
Like most REBOL solutions, r3D is a brilliantly simple, compact, and powerful design that doesn't require any external toolkits. It's pure REBOL, and it's really amazing!
16.13 Several 3D Scripts Using Raw REBOL Draw Dialect
The following short script is a compacted version of Gregory Pecheret's "ebuc-cube" (from http://www.rebol.net/demos/download.html). It demonstrates some simple 3d techniques using only native REBOL draw functions (no 3rd party library required). It's relatively easy to understand, manipulate, and use to create your own basic 3D graphics:
z: 10 h: z * 12 j: negate h c: as-pair z * 5 z * 5 l: z * 4 w: z * 20
img: to-image layout [box effect [draw [pen logo.gif circle c l]]]
q: make object! [x: 0 y: 0 z: 0]
cube: [[h h j] [h h h] [h j j] [h j h] [j h j] [j h h] [j j j] [j j h]]
view center-face layout [
f: box 400x400 rate 0 feel [engage: func [f a e] [
b: copy [] q/x: q/x + 5 q/y: q/y + 8 q/z: q/z + 3
repeat n 8 [
p: reduce pick cube n ; point
zx: (p/1 * cosine q/z) - (p/2 * sine q/z) - p/1
zy: (p/1 * sine q/z) + (p/2 * cosine q/z) - p/2
yx: (p/1 + zx * cosine q/y) - (p/3 * sine q/y) - p/1 - zx
yz: (p/1 + zx * sine q/y) + (p/3 * cosine q/y) - p/3
xy: (p/2 + zy * cosine q/x) - (p/3 + yz * sine q/x) - p/2 - zy
append b as-pair (p/1 + yx + zx + w) (p/2 + zy + xy + w)
]
f/effect: [draw [
fill-pen 255.0.0.100 polygon b/6 b/2 b/4 b/8
image img b/6 b/5 b/1 b/2
fill-pen 255.159.215.100 polygon b/2 b/1 b/3 b/4
fill-pen 54.232.255.100 polygon b/1 b/5 b/7 b/3
fill-pen 0.0.255.100 polygon b/5 b/6 b/8 b/7
fill-pen 248.255.54.100 polygon b/8 b/4 b/3 b/7
]]
show f
]]
]
Here's a version that reshapes and moves the 3D cube in, out and around the screen:
g: 12 i: 5 h: i * g j: negate h w: 0 v2: v1: 1 ; sizes/positions
img: to-image layout [box 200.200.200.50 center logo.gif]
q: make object! [x: 0 y: 0 z: 0]
cube: [[h h j] [h h h] [h j j] [h j h] [j h j] [j h h] [j j j] [j j h]]
view center-face layout/tight [
f: box 500x450 rate 0 feel [engage: func [f a e] [
b: copy [] q/x: q/x + 3 q/y: q/y + 3 ; q/z: q/z + 3 ; spinning
repeat n 8 [
if w > 500 [v1: 0] ; w: xy pos v1: xy direction
if w < 0 [v1: 1]
either v1 = 1 [w: w + 1] [w: w - 1]
if j > (g * i * 2) [v2: 0] ; j: z pos (size) v2: z direction
if j < g [v2: 1]
either v2 = 1 [h: h - 1] [h: h + 1] j: negate h
p: reduce pick cube n ; point
zx: p/1 * cosine q/z - (p/2 * sine q/z) - p/1
zy: p/1 * sine q/z + (p/2 * cosine q/z) - p/2
yx: (p/1 + zx * cosine q/y) - (p/3 * sine q/y) - p/1 - zx
yz: (p/1 + zx * sine q/y) + (p/3 * cosine q/y) - p/3
xy: (p/2 + zy * cosine q/x) - (p/3 + yz * sine q/x) - p/2 - zy
append b as-pair (p/1 + yx + zx + w) (p/2 + zy + xy + w)
]
f/effect: [draw [
image img b/6 b/5 b/1 b/2
fill-pen 255.0.0.50 polygon b/6 b/2 b/4 b/8
fill-pen 255.159.215.50 polygon b/2 b/1 b/3 b/4
fill-pen 54.232.255.50 polygon b/1 b/5 b/7 b/3
fill-pen 0.0.255.50 polygon b/5 b/6 b/8 b/7
fill-pen 248.255.54.50 polygon b/8 b/4 b/3 b/7
]]
show f
]]
]
And here's a little 3D game, with sound, based on the above code:
REBOL [title: "Little 3D Game"]
beep-sound: load to-binary decompress 64#{
eJwBUQKu/VJJRkZJAgAAV0FWRWZtdCAQAAAAAQABABErAAARKwAAAQAIAGRhdGEl
AgAA0d3f1cGadFQ+T2Z9jn1lSjM8T2uNsM/j7Midc05PWGh4eXVrXE5DQEZumsTn
4M2yk3hiVU9fcX+GcFU8KkNmj7rR3+HYroJbPUpfdoqAbldBP0ZWbpW62OvRrohk
WlleaHB2dW9bRzo1WYWy3OHbyrKObVNCVGp/jXpgRC48Vnievtfm6MCUaUVLWW1/
fXNkUkdCRlN7ps3r3cSkgm1fWFhmdH2AaVA6LElwnMja4dzNpHtXPUxje45/aVA5
PUtif6TG3uvMpHtXU1lkcnd2cGVURT0+ZJC84+HUvaGCZ1NIWm6AinVaQCtAX4Wu
yt3k37aJYEBKXXOHf3FdSEJET2KJsdPr1reUcGJbW2FsdXl2YUs5MFF7qdPe3tO+
mHNUP1Bnfo59ZEkyPFFukbTR5OvGm3BMTVlpent1aVpMQ0FJcZ3I6uHMsJB2YlZR
YXJ/hW5UOypEaJK90+Dg1qyBWjxKYHeLgG1WPz9HWXKYvNnr0KyFYVhZX2pydnVu
Wkc7N1yHtN3h2sivjGxTRFZrgI15X0MtPVh7osHZ5ua+kmdES1tvgn5zY1BGQ0hW
fqjO69vBoX9rXllaaHV9fmhPOi1Lcp/K2+DayaF4Vj1NY3uNfmhONjxLZIKnyODr
yqJ4VFFYZHN3dm5iUUM9QGaTv+Th0rqdf2VTSltvgIl0WT4rQGCIssze5N60iF8/
Sl10h39vW0ZBRFFljLPU69W1kG1gWlxiYHkWb1ECAAA=
}
alert {
Try to click the bouncing REBOLs as many times as possible in
30 seconds. The speed increases with each click!
}
do game: [
speaker: open sound://
g: 12 i: 5 h: i * g j: negate h x: y: z: w: sc: 0 v2: v1: 1 o: now
img1: to-image layout [backcolor brown box red center logo.gif]
img2: to-image layout [backcolor aqua box yellow center logo.gif]
img3: to-image layout [backcolor green box tan center logo.gif]
cube: [[h h j][h h h][h j j][h j h][j h j][j h h][j j j][j j h]]
view center-face layout/tight [
f: box white 550x550 rate 15 feel [engage: func [f a e] [
if a = 'time [
b: copy [] x: x + 3 y: y + 3 ; z: z + 3
repeat n 8 [
if w > 500 [v1: 0] if w < 50 [v1: 1]
either v1 = 1 [w: w + 1] [w: w - 1]
if j > (g * i * 1.4) [v2: 0] if j < 1 [v2: 1]
either v2 = 1 [h: h - 1] [h: h + 1] j: negate h
p: reduce pick cube n
zx: p/1 * cosine z - (p/2 * sine z) - p/1
zy: p/1 * sine z + (p/2 * cosine z) - p/2
yx: (p/1 + zx * cosine y) - (p/3 * sine y) - p/1 - zx
yz: (p/1 + zx * sine y) + (p/3 * cosine y) - p/3
xy: (p/2 + zy * cosine x) - (p/3 + yz * sine x) - p/2 - zy
append b as-pair (p/1 + yx + zx + w) (p/2 + zy + xy + w)
]
f/effect: [draw [
image img1 b/6 b/2 b/4 b/8
image img2 b/6 b/5 b/1 b/2
image img3 b/1 b/5 b/7 b/3
]]
show f
if now/time - o/time > :00:20 [
close speaker
either true = request [
join "Time's Up! Final Score: " sc "Again" "Quit"
] [do game] [quit]
]
]
if a = 'down [
xblock: copy [] yblock: copy []
repeat n 8 [
append xblock first pick b n
append yblock second pick b n
]
if all [
e/offset/1 >= first minimum-of xblock
e/offset/1 <= first maximum-of xblock
e/offset/2 >= first minimum-of yblock
e/offset/2 <= first maximum-of yblock
][
insert speaker beep-sound wait speaker
sc: sc + 1
t1/text: join "Score: " sc
show t1
if (modulo sc 3) = 0 [f/rate: f/rate + 1]
show f
]
]
]]
at 200x0 t1: text brown "Click the bouncing REBOLs!"
]
]
16.14 Sprite Sheets
"Sprite sheets" are large images that consist of many smaller images, each intended to be used independently. Here's an example that demonstrates how to crop and use individual images from sprite sheets (in this case, random images from a sheet of smileys):
REBOL [title: "Random Smileys 'Sprite Sheet'"] code: [
rows: 4 cols: 3 x: 64 y: 64 random/seed now
update-pic: does [
z: as-pair (random rows) * x - x (random cols) * y - y
img/effect: [crop z] show img
]
view layout [
backdrop white
img: image 61x61 pic #"^M" [update-pic]
]]
pic: load to-binary decompress 64#{
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
AJECvNrcU4oqWZJr/9l+BsVcgFoAAA==
}
do code
16.15 Multitasking
"Threads" are a feature of modern operating systems that allow multiple pieces of code to run concurrently, without waiting for the others to complete. Without threads, individual portions of code must be evaluated in consecutive order. Unfortunately, REBOL does not implement a formal mechanism for threading at the OS level, but does contain built-in support for asynchronous network port and services activity. See http://www.rebol.net/docs/async-ports.html, http://www.rebol.net/docs/async-examples.html, http://www.rebol.net/rebservices/services-start.html, and http://www.rebol.net/rebservices/quick-start.html for more information.
The following technique provides an alternate way to evaluate other types of code in a multitasking manner:
- Assign a rate of 0 to a GUI item in a 'view layout' block.
- Assign a "feel" detection to that item, and put the actions you want performed simultaneously inside the block that gets evaluated every time a 'time event occurs.
- Stop and start the evaluation of concurrently active portions of code by assigning a rate of "none" or 0, respectively, to the associated GUI item.
The following is an example of a webcam viewer which creates a video stream by repeatedly downloading and displaying images from a given webcam URL. To create a moving video effect, the process of downloading each image must run without stopping (i.e., in some sort of unending "forever" loop). But for a user to control the stop/start of the video flow (by clicking a button, for example), the interpreter must be able to check for user events that occur outside the forever loop. By running the repeated download using the technique outlined above, the program can continue to respond to other events while continuously looping the download code:
webcam-url: http://209.165.153.2/axis-cgi/jpg/image.cgi
view layout [
btn "Start Video" [
webcam/rate: 0
webcam/image: load webcam-url
show webcam
]
btn "Stop Video" [webcam/rate: none show webcam]
return
webcam: image load webcam-url 320x240 rate 0 feel [
engage: func [face action event][
if action = 'time [
face/image: load webcam-url show face
]
]
]
]
Here's an example in which two webcam video updates are treated as separate processes. Both can be stopped and started as needed:
webcam-url: http://209.165.153.2/axis-cgi/jpg/image.cgi
view layout [
across
btn "Start Camera 1" [
webcam/rate: 0
webcam/image: load webcam-url
show webcam
]
btn "Stop Camera 1" [webcam/rate: none show webcam]
btn "Start Camera 2" [
webcam2/rate: 0
webcam2/image: load webcam-url
show webcam2
]
btn "Stop Camera 2" [webcam2/rate: none show webcam2]
return
webcam: image load webcam-url 320x240 rate 0 feel [
engage: func [face action event][
if action = 'time [
face/image: load webcam-url show face
]
]
]
webcam2: image load webcam-url 320x240 rate 0 feel [
engage: func [face action event][
if action = 'time [
face/image: load webcam-url show face
]
]
]
]
Unfortunately, this technique is not asynchronous. Each piece of event code is actually executed consecutively, in an alternating pattern, instead of simultaneously. Although the effect is similar (even indistinguishable) in many cases, the evaluation of code is not concurrent. For example, the following example adds a time display to the webcam viewer. You'll see that the clock is not updated every second. That's because the image download code and the clock code run alternately. The image download must be completed before the clock's 'time action can be evaluated. Try stopping the video to see the difference:
webcam-url: http://209.165.153.2/axis-cgi/jpg/image.cgi
view layout [
btn "Start Video" [
webcam/rate: 0
webcam/image: load webcam-url
show webcam
]
btn "Stop Video" [webcam/rate: none show webcam]
return
webcam: image load webcam-url 320x240 rate 0 feel [
engage: func [face action event][
if action = 'time [
face/image: load webcam-url show face
]
]
]
clock: field to-string now/time/precise rate 0 feel [
engage: func [face action event][
if action = 'time [
face/text: to-string now/time/precise show face
]
]
]
]
One solution to achieving truly asynchronous activity is to simply write the code for one process into a separate file and run it in a separate REBOL interpreter process using the "launch" function:
write %async.r {
REBOL []
view layout [
clock: field to-string now/time/precise rate 0 feel [
engage: func [face action event][
if action = 'time [
face/text: to-string now/time/precise show face
]
]
]
]
}
launch %async.r
; REBOL will NOT wait for the evaluation of code in async.r
; to complete before going on:
webcam-url: http://209.165.153.2/axis-cgi/jpg/image.cgi
view layout [
btn "Start Video" [
webcam/rate: 0
webcam/image: load webcam-url
show webcam
]
btn "Stop Video" [webcam/rate: none show webcam]
return
webcam: image load webcam-url 320x240 rate 0 feel [
engage: func [face action event][
if action = 'time [
face/image: load webcam-url show face
]
]
]
]
The technique above simply creates two totally separate REBOL programs from within a single code file. If such programs need to interact, share data, or respond to interactive activity states, they can communicate via tcp network port, or by reading/writing data via a shared storage device.
16.16 Using DLLs and Shared Code Files in REBOL
"Dll"s in Windows, "So" files in Linux, and "Dylib" on Macs are libraries of functions that can be shared among different programming languages. Shared code libraries are used to extend the capabilities of a language with new functions. They allow you to accomplish goals which aren't possible (or which are otherwise complicated) using the native functions built into the language. Most of the executable code, and all the potential capabilities, of most operating systems is contained in such files. Third party code libraries are also available to make easy work of complex tasks such as multimedia programming, 3d game programming, specialized hardware control, etc. To use Dlls and shared code files in REBOL, you'll need to download version 2.76 or later of the REBOL interpreter (rebview.exe). If you're using any of the beta versions from http://www.rebol.net/builds/, use either rebview.exe or rebcmdview.exe to run the examples in this section.
Using the format below, you can access and use the functions contained in most DLLs, as if they're native REBOL functions:
lib: load/library %TheNameOfYour.DLL
; "TheFunctionNameInsideTheDll" is loaded from the Dll and converted
; into a new REBOL function called "your-rebol-function-name":
your-rebol-function-name: make routine! [
return-value: [data-type!]
first-parameter [data-type!]
another-parameter [data-type!]
more-parameters [and-their-data-types!]
...
] lib "TheFunctionNameInsideTheDll"
; When the new REBOL function is used, it actually runs the function
; inside the Dll:
your-rebol-function-name parameter1 parameter2 ...
free lib
The first line opens access to the functions contained in the specified Dll. The following lines convert the function contained in the Dll to a format that can be used in REBOL. To make the conversion, a REBOL function is labeled and defined (i.e, "your-rebol-function-name" above), and a block containing the labels and types of parameters used and values returned from the function must be provided ("[return: [integer!]]" and "first-parameter [data-type!] another-parameter [data-type!] more-parameters [and-their-data-types!]" above). The name of the function, as labeled in the Dll, must also be provided immediately after the parameter block ("TheFunctionNameInsideTheDll" above). The second to last line above actually executes the new REBOL function, using any appropriate parameters you choose. When you're done using functions from the Dll, the last line is used to free up the Dll so that it's closed by the operating system.
Here are some examples:
REBOL []
; The "kernel32.dll" is a standard dll in all Windows installations:
lib: load/library %kernel32.dll
; The "beep" function is contained in the kernel32.dll library.
; We'll create a new REBOL function called "play-sound" that
; actually executes the "beep" function in kernel32.dll. The
; "beep" function takes two integer parameters (pitch and
; duration values), and returns an integer value:
play-sound: make routine! [
return: [integer!] pitch [integer!] duration [integer!]
] lib "Beep"
; (Beep returns a value of zero if the function does not complete
; successfully. Otherwise it returns a nonzero number).
; Now we can use the "play-sound" function AS IF IT'S A NATIVE
; REBOL FUNCTION:
for hertz 37 3987 50 [
print rejoin ["The pitch is now " hertz " hertz."]
play-sound hertz 50
]
free lib
halt
The following example demonstrates how to record sounds (with the microphone attached to your computer) using the Windows MCI API. When complete, the recorded sound is played back using a native REBOL sound port:
; Various mci functions are included in the winmm.dll library.
; We'll create a new REBOL function called "mciExecute" that
; allows us to run MCI functions in winmm.dll. This function
; function takes one string parameter (a text string written
; in MCI function syntax), and returns an integer value (true
; if the function is successful, false if it fails):
lib: load/library %winmm.dll
mciExecute: make routine! [
command [string!]
return: [logic!]
] lib "mciExecute"
; Get a file name from the user, which will be used to save the
; recorded sound:
file: to-local-file to-file request-file/save/title/file "Save as:" {
} %rebol-recording.wav
; Open an MCI buffer and begin the recording:
mciExecute "open new type waveaudio alias buffer1 buffer 6"
mciExecute "record buffer1"
ask "RECORDING STARTED (press [ENTER] when done)...^/"
; Stop recording and save the sound to the wave file selected above:
mciExecute "stop buffer1"
mciExecute join "save buffer1 " file
; Close the DLL:
free lib
print "Recording complete. Here's how it sounds:^/"
; Play back the sound:
insert port: open sound:// load to-rebol-file file wait port close port
print "DONE.^/"
halt
The next example demonstrates how to play AVI video files, again using the Windows API "mciExecute" from winmm.dll. A demo video is downloaded from the Internet and played two times - once with default settings, and a second time at a given location on screen at twice the original recorded speed. The video codec in the demo video is MS-CRAM (Microsoft Video 1), and the audio format is PCM. For more information about mciExecute commands, Google "multimedia command strings" and see http://msdn.microsoft.com/en-us/library/dd743572(VS.85).aspx:
; These lines open the winmm.dll and define the "mciExecute" function
; in REBOL:
lib: load/library %winmm.dll
mciExecute: make routine! [c [string!] return: [logic!]] lib "mciExecute"
; These lines download a demo video:
if not exists? %test.avi [
flash "Downloading test video..."
write/binary %test.avi read/binary http://re-bol.com/test.avi
unview
]
video: to-local-file %test.avi
; The lines run the mciExecute function with the commands needed to
; play the video:
mciExecute rejoin ["OPEN " video " TYPE AVIVIDEO ALIAS thevideo"]
mciExecute "PLAY thevideo WAIT"
mciExecute "CLOSE thevideo"
mciExecute rejoin ["OPEN " video " TYPE AVIVIDEO ALIAS thevideo"]
mciExecute "PUT thevideo WINDOW AT 200 200 0 0" ; at 200x200
mciExecute "SET thevideo SPEED 2000" ; play twice a fast
mciExecute "PLAY thevideo WAIT"
mciExecute "CLOSE thevideo"
; These lines clean up:
free lib
quit
The next example uses the "dictionary.dll" from http://www.reelmedia.org/pureproject/archive411/dll/Dictionary.zip to perform a spell check on text entered at the REBOL command line. There are two functions in the dll that are required to perform a spell check - "Dictionary_Load" and "Dictionary_Check":
REBOL []
check-me: ask "Enter a word to be spell-checked: "
lib: load/library %Dictionary.dll
; Two new REBOL functions are created, which actually run the
; Dictionary_Load and Dictionary_Check functions in the DLL:
load-dic: make routine! [
a [string!]
return: [none]
] lib "Dictionary_Load"
check-word: make routine! [
a [string!]
b [integer!]
return: [integer!]
] lib "Dictionary_Check"
; This line runs the Dictionary_Load function from the DLL:
load-dic ""
; This line runs the Dictionary_Check function in the DLL, on
; whatever text was entered into the "check-me" variable above:
response: check-word check-me 0
; The Dictionary_Check function returns 0 if there are no errors:
either response = 0 [
print "No spelling errors found."
] [
print "That word is not in the dictionary."
]
free lib
halt
The following example plays an mp3 sound file using the Dll at http://musiclessonz.com/mp3.dll. Of course, that Dll could be compressed and embedded in the code to eliminate the necessity of downloading the file:
REBOL []
write/binary %mp3.dll read/binary http://musiclessonz.com/mp3.dll
lib: load/library %mp3.dll
; the "playfile" function is loaded from the Dll, and converted
; to a new REBOL "play-mp3" function:
play-mp3: make routine! [a [string!] return: [none]] lib "playfile"
; Then an mp3 file name is requested from the user, which is played
; by the "playfile" function in the Dll:
file: to-local-file to-string request-file
play-mp3 file
print "Done playing, Press [Esc] to quit this program: "
free lib
The next example uses the "AU3_MouseMove" function from the Dll version of AutoIt, to move the mouse around the screen. AutoIt contains a wide variety of functions to programatically push buttons, type text, select menu items, choose items from lists, control the mouse, etc. in any existing program window, as if those actions had been performed by a user clicking and typing on screen. Learning the other functions in the AutoIt language can be very helpful in customizing and automating existing Windows applications:
REBOL []
if not exists? %AutoItDLL.dll [
write/binary %AutoItDLL.dll
read/binary http://musiclessonz.com/rebol_tutorial/AutoItDLL.dll
]
lib: load/library %AutoItDLL.dll
move-mouse: make routine! [
return: [integer!] x [integer!] y [integer!] z [integer!]
] lib "AUTOIT_MouseMove"
print "Press the [Enter] key to see your mouse move around the screen."
print "It will move to the top corner, and then down diagonally to"
ask "position 200x200: "
for position 0 200 5 [
move-mouse position position 10
; "10" refers to the speed of the mouse movement
]
free lib
print "^/Done.^/"
halt
This example uses Dll functions from the native Windows API to eliminate the default 'REBOL - ' text at the top of the GUI window:
REBOL []
; First, load the necessary dll:
user32.dll: load/library %user32.dll
; Then define the Windows API functions you'll need:
get-focus: make routine! [return: [int]] user32.dll "GetFocus"
set-caption: make routine! [
hwnd [int]
a [string!]
return: [int]
] user32.dll "SetWindowTextA"
; Next, create your GUI - be sure to use 'view/new', so that it doesn't
; appear immediately (start the GUI later with 'do-events', after you've
; changed the title bar below):
view/new center-face layout [
backcolor white
text bold "Notice that there's no 'Rebol - ' in the title bar above."
text "New title text:"
across
f: field "Tada!"
btn "Change Title" [
; These functions change the text in the title bar:
hwnd: get-focus
set-caption hwnd f/text
]
btn "Exit" [
; Be sure to close the dll when you're done:
free user32.dll
quit
]
]
; Once you've created your GUI, run the Dll functions to replace the
; default text in the title bar:
hwnd: get-focus
set-caption hwnd "My Title"
; Finally, start your GUI:
do-events
Here's a slightly more versatile version of the above script. You can add it to the top of any existing GUI script, and it will remove the default "REBOL -" text from all GUI title bars, including alerts and requestors:
title-text: "My Program"
if system/version/4 = 3 [
user32.dll: load/library %user32.dll
get-tb-focus: make routine! [return: [int]] user32.dll "GetFocus"
set-caption: make routine! [
hwnd [int]
a [string!]
return: [int]
] user32.dll "SetWindowTextA"
show-old: :show
show: func [face] [
show-old [face]
hwnd: get-tb-focus
set-caption hwnd title-text
]
]
The following application demonstrates how to use the Windows API to view video from a local web cam, to save snapshots in BMP format, and to change the REBOL GUI window title:
REBOL []
; First, open the Dlls that contain the Windows API functions we want
; to use (to view webcam video, and to change window titles):
avicap32.dll: load/library %avicap32.dll
user32.dll: load/library %user32.dll
; Create REBOL function prototypes required to change window titles:
; (These functions are found in user32.dll, built in to Windows.)
get-focus: make routine! [return: [int]] user32.dll "GetFocus"
set-caption: make routine! [
hwnd [int] a [string!] return: [int]
] user32.dll "SetWindowTextA"
; Create REBOL function prototypes required to view the webcam:
; (also built in to Windows)
find-window-by-class: make routine! [
ClassName [string!] WindowName [integer!] return: [integer!]
] user32.dll "FindWindowA"
sendmessage: make routine! [
hWnd [integer!] val1 [integer!] val2 [integer!] val3 [integer!]
return: [integer!]
] user32.dll "SendMessageA"
sendmessage-file: make routine! [
hWnd [integer!] val1 [integer!] val2 [integer!] val3 [string!]
return: [integer!]
] user32.dll "SendMessageA"
cap: make routine! [
cap [string!] child-val1 [integer!] val2 [integer!] val3 [integer!]
width [integer!] height [integer!] handle [integer!]
val4 [integer!] return: [integer!]
] avicap32.dll "capCreateCaptureWindowA"
; Create the REBOL GUI window:
view/new center-face layout/tight [
image 320x240
across
btn "Take Snapshot" [
; Run the dll functions that take a snapshot:
sendmessage cap-result 1085 0 0
sendmessage-file cap-result 1049 0 "scrshot.bmp"
]
btn "Exit" [
; Run the dll functions that stop the video:
sendmessage cap-result 1205 0 0
sendmessage cap-result 1035 0 0
free user32.dll
quit
]
]
; Run the Dll functions that reset our REBOL GUI window title:
; (eliminates "REBOL - " in the title bar)
hwnd-set-title: get-focus
set-caption hwnd-set-title "Web Camera"
; Run the Dll functions that show the video:
hwnd: find-window-by-class "REBOLWind" 0
cap-result: cap "cap" 1342177280 0 0 320 240 hwnd 0
sendmessage cap-result 1034 0 0
sendmessage cap-result 1077 1 0
sendmessage cap-result 1075 1 0
sendmessage cap-result 1074 1 0
sendmessage cap-result 1076 1 0
; start the GUI:
do-events
For more information about DLLs and the Windows API, see:
http://rebol.com/docs/library.html
http://en.wikipedia.org/wiki/Dynamic_Link_Library
http://www.math.grin.edu/~shirema1/docs/DLLsinREBOL.html
http://www.borland.com/devsupport/borlandcpp/patches/BC52HLP1.ZIP
http://www.allapi.net/downloads/apiguide/agsetup.exe
http://www.activevb.de/rubriken/apiviewer/index-apiviewereng.html
http://msdn.microsoft.com/library/
Remember, whenever you use any Dll or code created by another programmer, be absolutely sure to check, and follow, the licensing terms by which it's distributed.
16.17 A Multiple Network Security Camera App Using The Window's Webcam DLL
This application uses several of the DLL routines from the previous section to demonstrate how to build a security camera monitoring application. This application relies on the common ability of IP cameras to upload a stream of images to an FTP server. This application simply provides an interface to monitor, store, and review frames from up to 24 cameras, to effectively save, browse, search each video stream. Video from a local web cam can be used as a source to test the program.
REBOL [title: "Camera Manager"]
svv/vid-face/color: white
tt: "Camera Manager"
do set-title: {user32.dll: load/library %user32.dll
gf: make routine![return:[int]]user32.dll"GetFocus"
sc: make routine![hw[int]a[string!]return:[int]]user32.dll"SetWindowTextA"
so: :show show: func[face][so[face]hw: gf sc hw tt]}
make-dir %./history/
write %view1cam.r rejoin [{
REBOL []
cam: first parse (first system/options/args) "."
tt: uppercase form cam } set-title {
blank-image: to-image layout/tight [box black 640x480]
file: to-file first system/options/args
gui: view/new center-face layout [
across
i1: image (blank-image)
return
btn "Stop" [flag: false]
btn "Start" [flag: true]
box 0x0 []
]
flag: true
forever [
wait .15 if not viewed? gui [quit] show gui if flag = true [
attempt [i1/image: (load file) show i1]
]
]
}]
write %viewhist.r rejoin [{
REBOL []
svv/vid-face/color: 230.230.230
change-dir %./history/
cam: to-file first system/options/args
tt: uppercase form cam } set-title {
files: read %./
if cam <> %all [
remove-each file files [
cam-in-file: to-file (first parse (last parse file "_") ".")
cam <> cam-in-file
]
]
last-file: length? files
counter: 0
blank-image: to-image layout/tight [box black 640x480]
update-screen: does [
attempt [
i1/image: (load cf: pick files counter) show i1
t1/text: rejoin [
"Current file: " form cf { (} counter {/} last-file {)}
]
show t1
]
]
gui: view center-face layout [
across
i1: image (blank-image)
return
btn "Reverse" feel [engage: func [f a e] [
if find [down over away] a [
counter: counter - 1
if counter < 1 [counter: 1]
update-screen
]
]]
btn "Forward" feel [engage: func [f a e] [
if find [down over away] a [
counter: counter + 1
if counter > last-file [counter: last-file]
update-screen
]
]]
btn "Select" [
selected-file: to-file request-list "Select An Image:" files
if selected-file = %none [return]
counter: index? find files selected-file
update-screen
]
btn "Save" [
save/png request-file/only/save/file (pick files counter)
i1/image
]
t1: text bold 370 {
Select image, or scroll forward/back (arrow keys work)
}
key keycode [left] [
counter: counter - 1
if counter < 1 [counter: 1]
update-screen
]
key keycode [right] [
counter: counter + 1
if counter > last-file [counter: last-file]
update-screen
]
key keycode [up] [
counter: counter - 20
if counter < 1 [counter: 1]
update-screen
]
key keycode [down] [
counter: counter + 20
if counter > last-file [counter: last-file]
update-screen
]
]
}]
write %localcam.r {
REBOL [title: "Local Camera Test Streamer"]
avicap32.dll: load/library %avicap32.dll
user32.dll: load/library %user32.dll
find-window-by-class: make routine! [
ClassName [string!] WindowName [integer!] return: [integer!]
] user32.dll "FindWindowA"
sendmessage: make routine! [
hWnd [integer!] val1 [integer!] val2 [integer!] val3 [integer!]
return: [integer!]
] user32.dll "SendMessageA"
sendmessage-file: make routine! [
hWnd [integer!] val1 [integer!] val2 [integer!] val3 [string!]
return: [integer!]
] user32.dll "SendMessageA"
cap: make routine! [
cap [string!] child-val1 [integer!] val2 [integer!]
val3 [integer!] width [integer!] height [integer!]
handle [integer!] val4 [integer!] return: [integer!]
] avicap32.dll "capCreateCaptureWindowA"
gui: view/new center-face layout/tight [
at -10x-10 b1: box 0x0
pic1: image 320x240
]
hwnd: find-window-by-class "REBOLWind" 0
cap-result: cap "cap" 1342177280 0 0 320 240 hwnd 0
sendmessage cap-result 1034 0 0
sendmessage cap-result 1077 1 0
sendmessage cap-result 1075 1 0
sendmessage cap-result 1074 1 0
sendmessage cap-result 1076 1 0
counter: 1 flag: true
forever [
wait .05
if not viewed? gui [
sendmessage cap-result 1205 0 0
sendmessage cap-result 1035 0 0
free user32.dll
quit
]
show b1
if flag = true [
sendmessage cap-result 1085 0 0
filename: rejoin ["cam" counter ".png"]
sendmessage-file cap-result 1049 0 filename
counter: counter + 1
if counter > 24 [counter: 1]
]
]
}
blank-image: to-image layout/tight [box black 160x120]
gui: view/new center-face layout/tight [
across
btn "Stop" [flag: false]
btn "Start" [flag: true]
btn "Test With Local Camera" [launch %localcam.r]
text "View History: "
drop-down data [
"cam1" "cam2" "cam3" "cam4" "cam5" "cam6" "cam7" "cam8" "cam9"
"cam10" "cam11" "cam12" "cam13" "cam14" "cam15" "cam16" "cam17"
"cam18" "cam19" "cam20" "cam21" "cam22" "cam23" "cam24" "all"
] [call/show join "rebol -s %viewhist.r " value]
t1: text blue "Updating Camera: 000"
return
i1: image (blank-image) [call/show "rebol -s %view1cam.r cam1.png"]
i2: image (blank-image) [call/show "rebol -s %view1cam.r cam2.png"]
i3: image (blank-image) [call/show "rebol -s %view1cam.r cam3.png"]
i4: image (blank-image) [call/show "rebol -s %view1cam.r cam4.png"]
i5: image (blank-image) [call/show "rebol -s %view1cam.r cam5.png"]
i6: image (blank-image) [call/show "rebol -s %view1cam.r cam6.png"]
return
i7: image (blank-image) [call/show "rebol -s %view1cam.r cam7.png"]
i8: image (blank-image) [call/show "rebol -s %view1cam.r cam8.png"]
i9: image (blank-image) [call/show "rebol -s %view1cam.r cam9.png"]
i10: image (blank-image) [call/show "rebol -s %view1cam.r cam10.png"]
i11: image (blank-image) [call/show "rebol -s %view1cam.r cam11.png"]
i12: image (blank-image) [call/show "rebol -s %view1cam.r cam12.png"]
return
i13: image (blank-image) [call/show "rebol -s %view1cam.r cam13.png"]
i14: image (blank-image) [call/show "rebol -s %view1cam.r cam14.png"]
i15: image (blank-image) [call/show "rebol -s %view1cam.r cam15.png"]
i16: image (blank-image) [call/show "rebol -s %view1cam.r cam16.png"]
i17: image (blank-image) [call/show "rebol -s %view1cam.r cam17.png"]
i18: image (blank-image) [call/show "rebol -s %view1cam.r cam18.png"]
return
i19: image (blank-image) [call/show "rebol -s %view1cam.r cam19.png"]
i20: image (blank-image) [call/show "rebol -s %view1cam.r cam20.png"]
i21: image (blank-image) [call/show "rebol -s %view1cam.r cam21.png"]
i22: image (blank-image) [call/show "rebol -s %view1cam.r cam22.png"]
i23: image (blank-image) [call/show "rebol -s %view1cam.r cam23.png"]
i24: image (blank-image) [call/show "rebol -s %view1cam.r cam24.png"]
]
previous-files: read %./
remove-each file previous-files [%.png = suffix? file]
flag: true
forever [
wait .05 show i1 if flag = true [
current-files: read %./
new-files: exclude current-files previous-files
foreach file new-files [
if not viewed? gui [quit] show i1 if flag = true [
do process-image: [
loaded-file: load file
if error? try [
rename file to-file rejoin [
%./history/
now/year "-" now/month "-" now/day
"_" replace/all form now/time ":" "-"
"_" file
]
] [
; print "error deleting"
do process-image
]
]
camnum: replace replace form copy file "cam" "" ".png" ""
t1/text: join "Updating Camera: " camnum show t1
do rejoin [
"i" camnum "/image: " loaded-file " show i" camnum
]
wait .05
]
]
]
]
16.18 REBOL as a Browser Plugin
REBOL interpreters exist not only for an enormous variety of operating systems, but also as plugins for several popular browsers (Internet Explorer and many Mozilla variations, including Opera). That means that you can embed the REBOL interpreter directly into a web page, and have complete, complex REBOL programs run right inside pages of your web site (in a way similar to Flash and Java, and useful like Javascript code). This provides a nice alternative to CGI programming for any type of application that runs appropriately in the stand-alone view.exe interpreter (i.e., for games, multimedia applications, and rich graphic/GUI applications of all types). Since the browser plugin runs typical REBOL code in the same way as the downloadable view.exe interpreter, you can run code directly on your web pages, without making any changes.
To use the plugin on a web page, just include the necessary object code on your page, as in the following example. Be sure to change the URL of the script you want to run, the size of the window you want to create, and other parameters as needed. You can download the rebolb7.cab file, upload it to your own site, and run it from there, if you prefer (be aware that there are several different versions: "http://www.rebol.com/plugin/rebolb5.cab#Version=0,5,0,0", "http://www.rebol.com/plugin/rebolb6.cab#Version=0,6,0,0", and "http://www.rebol.com/plugin/rebolb7.cab#Version=1,0,0,0" - use version 7 unless you have a specific reason not to). If you use your own copy, be sure to change the URL of the cab file in the following example:
<HTML><HEAD><TITLE>REBOL Plugin Test</TITLE></HEAD><BODY><CENTER>
<OBJECT ID="REBOL" CLASSID="CLSID:9DDFB297-9ED8-421d-B2AC-372A0F36E6C5"
CODEBASE="http://www.rebol.com/plugin/rebolb7.cab#Version=1,0,0,0"
WIDTH="500" HEIGHT="400">
<PARAM NAME="LaunchURL" VALUE="http://yoursite.com/test_plugin.r">
<PARAM NAME="BgColor" VALUE="#FFFFFF">
<PARAM NAME="Version" VALUE="#FFFFFF">
<PARAM NAME="BgColor" VALUE="#FFFFFF">
</OBJECT>
</CENTER></BODY></HTML>
Here's an example script that could be run at the "http://yoursite.com/test_plugin.r" link referenced in the code above. You can replace this with any valid REBOL code, and have it run directly in your browser:
REBOL []
view layout [
size 500x400
btn "Click me" [alert "Plugin is working!"]
]
You can see the above example running at http://re-bol.com/test_plugin.html. The above script must be run in Internet Explorer on MS Windows. In order to use the plugin in Mozilla based browsers (Firefox, Opera, etc.), the following code must be pasted into your HTML page:
<HTML><HEAD><TITLE>REBOL Plugin Test</TITLE></HEAD><BODY><CENTER>
<OBJECT ID="REBOL_IE" CLASSID="CLSID:9DDFB297-9ED8-421d-B2AC-372A0F36E6C5"
CODEBASE="http://re-bol.com/rebolb7.cab#Version=1,0,0,0"
WIDTH="500" HEIGHT="400" BORDER="1" ALT="REBOL/Plugin">
<PARAM NAME="bgcolor" value="#ffffff">
<PARAM NAME="version" value="1.2.0">
<PARAM NAME="LaunchURL" value="http://re-bol.com/test_plugin.r">
<embed name="REBOL_Moz" type="application/x-rebol-plugin-v1"
WIDTH="500" HEIGHT="400" BORDER="1" ALT="REBOL/Plugin"
bgcolor="#ffffff"
version="1.2.0"
LaunchURL="http://re-bol.com/test_plugin.r"
>
</embed>
</OBJECT>
<script language="javascript" type="text/javascript">
var plugin_name = "REBOL/Plugin for Mozilla";
function install_rebol_plugin_mozilla() {
if (navigator.userAgent.toLowerCase().indexOf("msie") == -1) {
if (InstallTrigger) {
xpi={'REBOL/Plugin for Mozilla':'http://re-bol.com/mozb2.xpi'};
InstallTrigger.install(xpi, installation_complete);
}
}
}
function installation_complete(url, status) {
if (status == 0) location.reload();
}
function is_mozilla_plugin_installed() {
if (window.navigator.plugins) {
for (var i = 0; i < window.navigator.plugins.length; i++) {
if (window.navigator.plugins[i].name == plugin_name)
return true;
}
}
return false;
}
if (!is_mozilla_plugin_installed()) install_rebol_plugin_mozilla();
</script>
</CENTER></BODY></HTML>
You can see the above script working at http://re-bol.com/test_plugin_mozilla.html.
For more information about the REBOL plugins, see http://www.rebol.com/plugin/install.html and http://home.comcast.net/~rebolore/plugin-guide.pdf.
16.19 Using Databases
Databases manage all the difficult details of searching, sorting, and otherwise manipulating large amounts of data, quickly and safely in a multiuser environment. MySQL is a free, open source database system used in many web sites and software projects. ODBC is a common interface that allows programmers to connect to many other types of databases. The most recent releases of REBOL, along with all commercial versions, have built-in native access to MySQL, ODBC, and other database formats. You can also download a free MySQL protocol module that runs in every free version of REBOL, from http://softinnov.org/rebol/mysql.shtml. A free module for the postgre database system is also available at that site.
To explore database concepts and techniques in this section, we'll use the open source MySQL module above because it provides access to a powerful and popular database solution which works even in old and unusual versions of REBOL.
Most web hosting accounts come with MySQL already installed. See your web host account instructions to learn how to access it (you need a web address, database name, user name, and password). To get a free, simple-to-install web server package for Windows that includes MySQL, go to http://www.uniformserver.com. That program enables you to easily install a web server with MySQL pre-configured on your local computer. It's useful if you don't have access to a web server on the Internet, or if you want to create multiuser applications which use MySQL on a local network.
To use the REBOL MySQL module above, unpack the compressed "rip" file available at the link above. This step only needs to be done the first time you use the package on a given computer:
do mysql-107.rip
Every time you access a MySQL database, you need to "do" the module that was unpacked in the step above:
do %mysql-r107/mysql-protocol.r
; At the time of this writing, 1.07 was the most current version of
; the mysql module. Update the numbers in the previous two lines
; of code to reflect the current version number you've downloaded.
Next, enter the database info (location, username, and password), as in the instructions at http://softinnov.org/rebol/mysql-usage.html:
db: open mysql://username:password@yourwebsite.com/yourdatabasename
In MySQL and other databases, data is stored in "tables". Tables are made up of columns of related information. A "Contacts" table, for example, may contain name, address, phone, and birthday columns. Each entry in the database can be thought of as a row containing info in each of those column fields:
name address phone birthday
---- ------- -------- --------
John Smith 123 Toleen Lane 555-1234 1972-02-01
Paul Thompson 234 Georgetown Place 555-2345 1972-02-01
Jim Persee 345 Portman Pike 555-3456 1929-07-02
George Jones 456 Topforge Court 1989-12-23
Tim Paulson 555-5678 2001-05-16
"SQL" statements let you work with data stored in the table. Some SQL statements are used to create, destroy, and fill columns with data:
CREATE TABLE table_name ; create a new table of information
DROP TABLE table_name ; delete a table
INSERT INTO table_name VALUES (value1, value2,....)
INSERT INTO Contacts
VALUES ('Billy Powell', '5 Binlow Dr.', '555-6789', '1968-04-19')
INSERT INTO Contacts (name, phone)
VALUES ('Robert Ingram', '555-7890')
The SELECT statement is used to retrieve information from columns in a given table:
SELECT column_name(s) FROM table_name
SELECT * FROM Contacts
SELECT name,address FROM Contacts
SELECT DISTINCT birthday FROM Contacts ; returns no duplicate entries
; To perform searches, use WHERE. Enclose search text in single
; quotes and use the following operators:
; =, <>, >, <, >=, <=, BETWEEN, LIKE (use "%" for wildcards)
SELECT * FROM Contacts WHERE name='John Smith'
SELECT * FROM Contacts WHERE name LIKE 'J%'
SELECT * FROM Contacts WHERE birthday LIKE '%72%' OR phone LIKE '%34'
SELECT * FROM Contacts
WHERE birthday NOT BETWEEN '1900-01-01' AND '2010-01-01'
; IN lets you specify a list of data to match within a column:
SELECT * FROM Contacts WHERE phone IN ('555-1234','555-2345')
SELECT * FROM Contacts ORDER BY name ; sort results alphabetically
SELECT name, birthday FROM Contacts ORDER BY birthday, name DESC
Other SQL statements:
UPDATE Contacts SET address = '643 Pine Valley Rd.'
WHERE name = 'Robert Ingram' ; alter or add to existing data
DELETE FROM Contacts WHERE name = 'John Smith'
DELETE * FROM Contacts
ALTER TABLE - change the column structure of a table
CREATE INDEX - create a search key
DROP INDEX - delete a search key
To integrate SQL statements in your REBOL code, enclose them as follows:
insert db {SQL command}
To retrieve the result set created by an inserted command, use:
copy db
You can use the data results of any query just as you would any other data contained in REBOL blocks. To retrieve only the first result of any command, for example, use:
first db
When you're done using the database, close the connection:
close db
Here's a complete example that opens a database connection, creates a new "Contacts" table, inserts data into the table, makes some changes to the table, and then retrieves and prints all the contents of the table, and closes the connection:
REBOL []
do %mysql-protocol.r
db: open mysql://root:root@localhost/Contacts
; insert db {drop table Contacts} ; erase the old table if it exists
insert db {create table Contacts (
name varchar(100),
address text,
phone varchar(12),
birthday date
)}
insert db {INSERT into Contacts VALUES
('John Doe', '1 Street Lane', '555-9876', '1967-10-10'),
('John Smith', '123 Toleen Lane', '555-1234', '1972-02-01'),
('Paul Thompson', '234 Georgetown Pl.', '555-2345', '1972-02-01'),
('Jim Persee', '345 Portman Pike', '555-3456', '1929-07-02'),
('George Jones', '456 Topforge Court', '', '1989-12-23'),
('Tim Paulson', '', '555-5678', '2001-05-16')
}
insert db "DELETE from Contacts WHERE birthday = '1967-10-10'"
insert db "SELECT * from Contacts"
results: copy db
probe results
close db
halt
Here's a shorter coding format that can be used to work with database tables quickly and easily:
read join mysql://user:pass@host/DB? "SELECT * from DB"
For example:
foreach row read rejoin [mysql://root:root@localhost/Contacts?
"SELECT * from Contacts"] [print row]
Here's a GUI example:
results: read rejoin [
mysql://root:root@localhost/Contacts? "SELECT * from Contacts"]
view layout [
text-list 100x400 data results [
string: rejoin [
"NAME: " value/1 newline
"ADDRESS: " value/2 newline
"PHONE: " value/3 newline
"BIRTHDAY: " value/4
]
view/new layout [
area string
]
]
]
For a more detailed explanation about how to set up MYSQL, how to us the SQL language syntax, and other related topics, see http://musiclessonz.com/rebol_tutorial.html#section-27.
16.19.1 SQLite
Here's an example that demonstrates how to use the popular SQLite DBMS, using sqlite.r by Ashley Trutter:
REBOL [title: "SQLITE Example"]
unless exists? %sqlite3.dll [
write/binary %sqlite3.dll read/binary http://re-bol.com/sqlite3.dll
]
unless exists? %sqlite.r [
write %sqlite.r read http://re-bol.com/sqlite.r
]
do %sqlite.r
db: connect/create %contacts.db
SQL "create table contacts (name, address, phone, birthday)"
SQL {insert into contacts values
('"John Doe"', '"1 Street Lane"', '"555-9876"', '"1967-10-10"')
}
data: [
"John Smith" "123 Toleen Lane" "555-1234" "1972-02-01"
"Paul Thompson" "234 Georgetown Pl." "555-2345" "1972-02-01"
"Jim Persee" "345 Portman Pike" "555-3456" "1929-07-02"
"George Jones" "456 Topforge Court" "" "1989-12-23"
"Tim Paulson" "" "555-5678" "2001-05-16"
]
SQL "begin"
foreach [name address phone bd] data [
SQL reduce [
"insert into contacts values (?, ?, ?, ?)" name address phone bd
]
]
SQL "commit"
SQL ["DELETE from Contacts WHERE birthday = ?" "1967-10-10"]
results: SQL "select * from contacts"
probe results
disconnect db
halt
To use SQLite in REBOL, see http://www.dobeash.com/sqlite.html.
16.19.2 Other DBMSs
To use ODBC in REBOL, see http://www.rebol.com/docs/database.html.
For a useful open source SQL database system created entirely in native REBOL, see http://www.dobeash.com/rebdb.html.
The following additional database management systems ("DBMS"s) are also available for REBOL:
http://www.tretbase.com/forum/index.php
http://www.fys.ku.dk/~niclasen/nicomdb/
http://www.rebol.net/cookbook/recipes/0012.html
http://www.cs.unm.edu/~whip/
http://www.garret.ru/dybase.html
http://www.rebol.org/view-script.r?script=sql-protocol.r
http://www.rebol.org/view-script.r?script=db.r
http://www.rebol.org/view-script.r?script=couchdb3.r
Be sure to search rebol.org for more information and code related to databases.
16.20 Menus
One oddity about Rebol's GUI dialect is that it doesn't incorporate a native way to create standard menus. Users typically click buttons or text choices directly in REBOL GUIs to make selections. The "request-list" function and the GUI "choice" widget are short and simple substitutes which provide menu-like functionality.
The example below demonstrates how to stylize the choice widget to create normal looking menus. Use "editor decompress #{compressed code}" to examine how it works. Otherwise, just follow the menu format in the example, fill in your own GUI code, and paste the compressed chunk into your own script:
REBOL [title: "Choice Button Menu Example"]
menu: [[
"File" []
"_________________________^/" []
"File Option 1" [alert "File Option 1"]
"File Option 2" [alert "File Option 2"]
"File Option 3" [alert "File Option 3"]
"_________________________^/" []
"About" [do-face b1 1]
][
"Edit" []
"_________________________^/" []
"Edit Option 1" [alert "Edit Option 1"]
"Edit Option 2" [alert "Edit Option 2"]
"Edit Option 3" [alert "Edit Option 3"]
][
"Preferences" []
"_________________________^/" []
"Preferences Option 1" [alert "Preferences Option 1"]
"Preferences Option 2" [alert "Preferences Option 2"]
"_________________________^/" []
"Preferences Option 3" [alert "Preferences Option 3"]
]]
gui: [
a1: area wrap with [colors: [254.254.254 248.248.248]]
b1: btn "About" [alert "These menus are just a choice button widgets"]
]
do decompress #{
789C5553DBAE9B30107CE72B567929E911C12149A5F2D20F41A87260093E3536
B24D427A947FEFDA26093542D87B19CFEC2E9D36C89B1E0618504D502540AB5B
8C0252EEC875D842352989D64227544BF6CD6FBF36500965D138E89107B3CF86
4D5D2775D248534237A906AA8E3758C74FEE7076647F6E6DBE07B0BDBE05539D
781659A3A5A6E4E270DA154746EF8962AED7FC2ADA2C643E034E87DDF22616DD
34968B006C2F5882D20ABD18451756D6DDE5CB667BDEEADBF314D02CC5B01D3D
44DE438CDCF0D2EB6BD1E71F6756BF230DB6538350BDD942B127B205912D4E5E
FD197D11331F5082E4773D39B84C2259D78BCE5069232E4241C1E623AB133E8E
A8164FA07146A96F71A76760F39E854350030375ACE9B5202AFB9F6C2E1848EC
1CAC58DD84EB219426A42D97B19905B85560CA6D36726160453CB7E22FE63371
DB866CDE184D23B046013B523FC29644CFD9A8A93A2C3138224D8E203EEAE2FA
5FEBE11A54094E6767A99B3F54C84F4D50D1E3D717A53DE04926000261BFDC41
73CB1D8794A6C7F026CACD1F74D91714DB15945FAD26F512FF8FBA7239F9B64B
1386EE95503FC236F6DF6BA7114BD31FF0FD25A313C63A1805310F8AC4163E68
0A637916F591F34704889DF3FFCCA0E09BBD5B8743ACE5BBD1834AEA7FEAF212
1C87030000
}
view center-face layout gui
The example below demonstrates a useful menu widget by Cyphre (shown earlier in the presentation section of this tutorial), which makes it easy to add nice looking menus to your GUIs. This example also includes Cyphre's Tab Panel widget, which is a great way to maximize screen real estate in programs with large GUIs. The code demonstrated here is the author's preferred way to add menus to REBOL programs:
REBOL [title: "Cyphre's Menu and Tab Panel Example"]
; Menu Widget:
do load decompress #{
789CB51B6B73DAB8F67B7E85BA3B770237430CE4D12EDD6EC6109A92266D499A
B629E3CE1810C6606C6A9BE074BBFFFD9E23C9B66CCB84A477C384C8B2747474
5E3A0FE5EF5118D516D45DB5C8C29C53E20D6774143E23839D6F357B615A3468
119F8E5723CABABED55EE260329C5B646D875342C71665FD8E678EC9F1E1EF7F
DB9FDAEFAFD6F5B76796A7C3CFBBEB9B69F7C68256BB87CFD71DFD161F3EFFF0
8EBE6047FB6CDCFE78D3D5F58BB3F7930FE1D5CE7C0DBD9DF6ECFAF5F93B787D
7CDED775AB77A9EB1F5C0D5EE8C7F07AFC11BEDE2F11ECB103E35F37F7BE7CFF
EADE20C09DE6DCE9F63F5D1DBA67172F34ED85A6FFB8D1FB7A27B27A33FFD38B
BDABAFE7DF17E1B43B5B4DA7B06AD7BABE3A773AEFDAD7FE7BC07074D5DDB1E7
776BC4D25B2EDB00BBFBEE1656F7ACC08AACAF67572BBDD9A6BDC6E7B6ADEB6F
BAD6FCC5E91B5B3F8F2EBFF6FBD142BFA6D6E71D58EDEAF8C7CDF2EB25ECA0D3
D1DFBEE95E754EEFDF74FA7AB7FBEEA67376DFD1BF9FDDF6FBFA69FBEEF6D68F
AE7A17BADEEB0DDB9DE69DFEFEEDCE17EB6BEFB6F7767D7EEE0125EF69AF1FE8
EDB61E5993FAE5349AF5828B4B86E17A31E97FBC9E863F7AFACDFBFACCBA38EF
996DA0FBCE6D70F5E58D7DE67EE94CFBFDD7A3D3C6C58B8B2BFDB2DB7D7D7C7A
AD03B5FBB7FAC5DC72BF1EDE6B07EDBECE5873F3E9FDD5DBA3CE6DAFF7EA1F89
D9A3291DCD413CFCF9969CD6D9578F73FAB47BFAFCF8E2DFE1F44D3DE6F4F97C
4FDFEBAE7AB0F41A609DBE99CED7FDB6A5B7EDFECAB4AE0F7B20017A74DBB7EC
B707CF3F009C7EFFEDF4F38ED75D74901A97D7AF7FD43B9FDAC8ACB3CBCE9ED5
EDB65FB70F6F75ABDFEF358EEE74EB3540EEEA7BF3E0CB878F079ED6EE3292ED
E46966C06F10DE3BF60FAA2DCC20A4BED01BAE6313131489290EEFFD561B798E
E7B788EBB954F44C3C3714DA88CD64643276E898A379D2E94D26018509F5A89E
F4055373ECAD33500DF117155640C7A6043D009C5BA41135F2EB359E37F71BC7
2FF61B4707DF6A39706BCF1F838948A18CCDD0240397AE356CC18E6D3F08894B
A39098BE15A42D2399623ACBA92981F856C3E9ACB7305F1A04066A42827BA0F1
421BAE6C678C2B52F22769366AE7A65B6BD6EB8719A839C8CDA323522324E9C9
8C34A4A70C62793C8C1C41BC498617410838B5083029E5376C8A711824A236C9
304960164F9E50EAC492004D69333EE5DC9AACDC11190019E069CF90374B41C8
40F896A6ED9F90898603C84034B4A895B4C81E9F0C2D79CFF1D078603C4C1E13
4CBD35991428912388EDDA612B2BC42D62BAF764C0C48B6D3F850A2C75BD90F0
57032180CD83E3FDE6C1C17EB3719C8E5C9A2E6037F2960029ED659445B5497B
3C9F9A2350389B0C0DC2C553DA84ED06D40F4968DA0E419082DCE688E6642704
D6B7889DE9E3CA243130A337857E3835F93BE9503DFC63BF513FDE6FFCF13C47
098EBB1B1666ACA73688393302C5D1B1B8A0DD301DDB725B64D7A193303B7269
FAA618894D80E99BCB13344F4E003E806F5B364C6C46F5EC342E1087D1214883
45C35AC84408D7B271C5CCD8D82C813E64FAB9551812FCC9625F2AEC02E01DF5
5381378967E44730E9311D986A82D4C2C628500F39A9312DCCF571F55300E1B6
5A0315A321CC4184156354C0346F19DA9E1B2858BF6152B85872ABA10629E858
59DBAE202AE873153850A9470DF25FA19F553065F5E870DB353927033AF2DC31
0940EE4FD4AB732563D695D9AB7887822E894CEE7285CDC2803EB28BD231D1D8
41B6CB0D7D7610EB33B6C55B9C29654CE1164931B130B6B8E244136ABBB4C1DB
9968A9B292E2527C47B91992B62AA6E4ACA51A0DEA5AA6456531A70A090DC083
008B661674842998B776952FF2241547139A5BC50B0500159756CB92B5401555
3AA81E0C16C99BE715945963DB1DD3088413FEAA5E8324536722AC066F0F5468
E2671BAD8E1905F82C574B0D740EC8A9996BF35E2956ADD2D342FE240AFECBBA
2C84447DEAC89FB2132823289B7549880C3B53F2C63D4356738426A1852702B0
2B4B27D079F6B67476A981798C7979BC71119C2E5A41C4A7743CC8346C50B89C
77365D935D90135CA9A63CB232847CE078933F78368E1C2FA0357A077B41FF9A
3D96EF84CF015FC61ED393C47C2003180483B75FF1270D1F1E82B5F4BD110D82
9AB70A1128C74405F90138F8B3721D00C59578B080839C84F68212148D3B5471
D198D37BC2AC5730F23DC7A93936F84302E5F07E097230454C986A3E843EFEB0
990F8C7B900CDEB236A6211D852DE0E153362F1C71C11B92CE7E5004721018F9
64B128210DF723C980A3B8C51AC6D698288542DAD2368BFD0243CADF95BD3118
D56ADC8E174EDFF8B336EDB0FCDC509E9ACC53564E508129F6E57BB2CFF293DC
16A74888274623AAC3895129F1C6C12456AB85D0EC5BEDE543364F0C38AED7A3
669A504093A5F1A093B0539687E5B17DE6FD6C621232C47F172B27B485E5636D
49DA26B643333665E8CC334A0516973957D04F1A393165ECE087184B49719460
A4927859B40C96AA81AF8470311AEC7510FAECAF961E7C136F38C3497C804FC3
95EFB22DB3C050E107A003844A9E49E70C672067312A22E152879F887D8B7E1E
69C638705459582D7248299ECC0B49CEEFF8EC640F041E44EF24CE56404BF42D
CD702A3A97A2C55D23DE74BC91E9B0E65D92E0F55760C4590BED329F0381E842
38D7E355DC0A5643DE630727AC319ACE79C35E58ECEFD0F44547B8607FC1F2B3
BF3402591CF377AE07E6920F172C8D43C9241309188340C95CC1E76C5200D8C0
3616B305741942DC847004B605C76BE0C1793440C0102F7B2EA960536BE0C304
1CB49F04BD4614C2219066FE8C543818EC010055783FB79706093D42DD315F98
E076EC30E16192DB40DA275282BB0CB2181B29E15BE4203A48C99BA65114C1F0
D843E72476C2B82E9818976C769D779937B28B001F31CBC41457925254052DBF
860E07F92884A84C3D2E982AF7BCE03C6F4CC0629E2A3172D4013700F52A753B
8DE4D478446E67DBAC4E2E7768A4CA9718F0ACDDFFEDF237C277AE2529B08CD0
81AEC7B2561766881D29A9962BD4834AF95DAE278951455A0C7641D1CDA14349
0535BE45427F45AB06A3D380E5F976E1B4421B205E8136EDD6E21E768E560D31
1CB50C8C0419B0D2D33318892704FE45FC9FC5C376C180900A5A91643536335C
A0D5B45DEB191FF73BEB46B75274C7F3C144910A33300900AEFB0916C21211BE
6134093F992D90314DED81412AD2A1C3625926B43CAB08946F908A8864719DAA
34384E59A1809C24CBB22E84CF90348C814853E26C8E471604235AE674147C67
843C89896A89A3125A598783BD0A3D5EF293B5466887E553EA92547738DC1683
4B2713748F4129C08B4A8A08E09B3051355E368E23396F9BF568004D9040ED1E
629383FC0EA4CC2CB7EC04F896D169F2127A588D0A9F732E15FAC580557C1CC9
9B15B86379536BBC64E2CEAB214390217349FE634D2216E2EF2F5D2B373BDEEE
C85B2CC117073F3B0AC1E813166B8283432A65468347A8CDA323A3CA13DCE835
958E96B2E3D5BCB7581EDE97E606D4E97289090119A439DB20DCCE218599A886
1B3807E4D7F27975C2EB8A63EAD80BB057BE9A710058BD6DA91EB5EDEE13574B
AA6E091396E6A9C18F06A7B774DF19F73B9F9FE1C6281F156448554A146EB213
B2E06306B6A047B19F5724C2450E4975E2DDF73CF42A73DA90398B44A089669C
0C061299659283319226955720BC7C098208F649B3B5E653EA12982B83238599
1730331008E51444442251B31819C11191A90E31A9111A1D47EA92C92C0820D3
796D4CE9920CC6BEB9164715BA4360E2175695D4F741C3F7EB464E81F2A0C462
780A2AC2EF0C6C0CF184B16A9680572C80BE65410FCAB51A04741C0903B14C82
03C1D6C79666088F20C88C848B25FE7AF815E0976D902C150B68C3A0B2F89A1F
6A35FA7D653A2719416AC822AACC673C2ABFFFA40C7F31CA57E6FCE532154FBE
A36A663CDB3809FA0A5E025BD4099AF2792D3EAD300742928A0A12F4CFD2AA73
EE4D5C3F75B96CB08045890FC12F803303ADCB5BD9FCCE2B0E78F8E1F484235A
257FBE2249975B65647193DD6FCCAC7307AE74048421C9415D0107074C638DF9
C2E2C89D79109E0C7621F0406292DD4CEE1A3D6003F1C97B5CD98F4F594ED30C
057166A52319756AA5D4512D414A324A55456745D127EC4CD6A1243CBC050AB3
9D95E4FF4AD3829BC98E32A3F1B43F999448221B97C4C9998D0D54BD6011D5DD
6A188AF594F42A2D5DA1D92AABA115B44E594C2D8CDA504A2D8C55153F846F28
46F3334B7889E8472A0AADEA19B19F5A26CF42602A251B65374614EF44910B5E
436CC7E3F624F89732B0E08F6385AB4AFE22F50D69676081642294B44410F994
60414C3DB55993463063A95C406B8AE23BDBF231D90CC756C249A8B261726C60
6D7ECBA9049B8D3B4DB8868800B2B68CB91DF3ED2FF2105F1E5824DE285BA526
AF524B567900403AA7C5FD8274261279E3EC8DE51625F03DEE76D41E02BCF12D
1792ADF6C7252E4563C3E027D432CAB7AF7205124A3C412CCB5040B5E47BE492
C4BF379A3C38DB376A7A2AFD4F117C99E319A1378781E7AC42CAE4E0D1A4FE05
8A72992B5FF297CE8F47B24B5C87C983528EDED2C9D85CB7FA3F5E6099C49536
F022CA2E9EB03B26E25E49DEE530480A41EDBCB15B1D83CDA3B62FD32519371E
3DE7E6C57724A452D79DE9AC681C28452C32F5E154264A874DE5A2257EADCAB3
D9E0EA32D7B6D4DB85D80FFDDDE28D8D2D02CE38B0F35CE75E89166C92DF81D6
3077A118C0D3CF6C448953C4CA354A09CEE5A6B39F48DC39DDDD63E96783D1BB
126189870B2344A31313738CD047067B301206D57082610CA04FE970E3E72789
5AB9B21082C0B250E90CAC16A9B550949054CAA85415520119C92F14D7DAD574
5CC48A9248E12F887D2E1988B9F6C7E602C14AF9E1681516135F49EA2BAE0FA6
9FF20CE813EFB666925752112549812B2EDC6E4838F2DC5C116F6EEB9B8AD414
922E5FBC493FE2F0B96FF174791EF55C82262DC6AA39086B15B0CE7172B3FD79
98ABA6EFC3E153CE52F67E6BA696D239CEDD0DF87AF5FDE7F00B1FE27BEC5CF8
A3C0E64285E25FA474194DE3320FC6DFAEA1222DB7A18CBE3CA9A04CCCB14A13
D697E25889D79AF68863825F961FAD28A8F0407412B00B0149448A28629A9B9D
1808B25E128C4A858A5D9E5E4DF5261B84A605906DEEB6F0DB9095B20CAEB44C
55B67D80774148784D32D7C9CA5DB93E5E8FCC750A4615FA45ED30D39B629231
F0F11E25C3CEBBC41D92F412C30EBBE4C13204995B1EE9058FF45E867C3323BD
1921DFCDE0D1C84E725067FE312577BBE0A1A23262A03552BB943AED99FFA7B0
852B5B6902F7D81278B735E3D8DAB21F2B7BEF7238920BA3EEF3C3EFE597B082
ACA8316A33B194FC0F4EDCEB9C891B73123B8BC7D62C1358A4898F38807E8040
79F1CE5F8A5253AAD26477BA18373653EE49BB44B5891369E93EE37F084A7659
93B0D222BC8C7CA8DC1378B5CCE66E41B81A99A54D01B8042696F69F80E28B52
24C4202067732BD6487628BE646727D7B20587528E714B85025B2DAAB6505CE9
F69674D5077ACCF13873A72BBDFD1697FB714A72A52D003F4943CBA2C141A2A1
B30D5E5D6C99F9D04301C9244363202209FC77431B040804940CD96F930C307B
8BB782C12732E21B663C8F5D82914872B36B38325AB011E325D6BFBD0989FF97
78E79FFF0120649CE6573C0000
}
; Tab Panel Widget:
do load decompress #{
789CBD586973E2B816FDCEAF50577FE86428C7989824506F2695349DADB37496
4E4FA09C2A63CBE0C6D8C4368DC9BCF9EFEF5E495E65B2CCAB9AA4005BBAD2DD
8E8EAEF45714AF3CF799AA33338A6948868D47253647CADCF4A9D7238E6951B2
74E309EBE05D6648CD1E999953CABB79CFA312384E44E31E6925ED9668A2F698
0A517CCC441F1577666297179836890365E4FA66B82236B582D93CA4514476F4
8F7F35E8D973D8FF76F8DC69FEB8D56767E387ABBED63D39383B884FC79713EB
E4B8D38F2EEF9ED4A3969BCCFBBF76AE7E9E2E4757ABB079D3D28FADC99F8DDB
6F0FA77BB3FBF1D3D199F7703C0906B3BEDEBCE99EFE6A1F8EBFF6EFC70F27EE
FDF2E2707C39681F5EDF256A337A1ADCEB874FB7CF897DF2E5E2A9D171EFBA4D
4775FC78767D6D1E4D6EA6D6F5D765D4D793E3D9E0AC3B4816E777E7CFA74F17
9FA7AD9B2FC9F27C104C8F0E7FBA17DD4B6B7BFAE3B6D1FC71393BD8BB9AF447
30CF73D3695D7C3B595EDEDE3C58BB5F4EA3CF5DB77564FF79D78CC0BC643038
39F8BAFB5DBB3EBF98B9FDD1FDFC9BAD37BE3E07CBF3D3D5C21B3C75CD8BE6F7
B3B63538BE9C3E9CF8FABDF7BDD554E943AB75A21FDF6996EA6DDF5F7DFEDE3A
38D8D54E0F9AD303F8FBBDF17716F40812DD237AA293ACC90ABC20EC113FF0A9
6833C4AF4311003CCDF058C85DA3F4F3A884D40ECD258065E15B64E8109350A3
200E30000001B61C750ED8F1630561A38E82D0A6E17E4910B4AA0814953A0EB5
004B08872002E0D024A6BECD6CDF7054748428446B6D12E600BE6E94A767ED91
AA6D1A85F98D3A6525EF8B1110225553C6A169BBA00780AE9176A7BDC53F3A69
EBFA565BDFDE6A6FEFBDC72CA31478235D67754B8C27B0038ABB6996023F4E25
E1B1104D9159ADDDDA129F0A0E34AD92F1F5AB5546493A0998F266DC507FCC16
7D012744F502CBF448E43965C844403AD60444CA19B383A55F690259CFE955C0
55789685D594C55464B91E89A8071926A227620F4E602D229E06D7B769B24F1C
F84D65945190B0C1A016463B151DAE039E0D51D6B46237F071587956E335ABD4
BA900BD949B064B3A15CA9CFA805B1F1D2425D9300173B7F279F98AE72BCDF11
6D474DF78499EBE3982012DFCDCA0C76E0C495A12200E868BA684A1282544A61
0593DF902D09409C50D4558FB4DB521777013B5B521F2EB9D4D091675A531908
68433DD5F129EAE84EB5299D9321268B0CE7D4271BC5286C82739EA7D4B4839F
B820BB408C2DD88535A30AB35A3E0B8305C44AEBEC6D693B5DF874C876755CF9
DD581FBF8C97EAE227C7568A1F0063A71AE6F706B166C914FDAEF39720A7831D
8CD2351DD8927F8023E1977F5E89C98B4BAF48ED9C373323855B3D12870BDE52
A01EA4A31EB1CDD864CF0864781D99D31E31C33058463D22A257D9C83826C0D9
8F436C3320313B5B6D0DF6A64E879BB104BD28202C441D64E8D3A5CAB5396E18
C5C4876D17348DA3FC2975C90F84E97C54E686637A00AA5C927FBBBECB20375F
91D94ABCA5AA33C7E4FD2E2F2AF524470F9205EE3C850D2DA6F348CC3FCCC33F
33936C9BCA85EB2386FF7C3B28CF62E4560A0B916786D2BE6B14048B2572FA0A
254C757B2D0DC92320DE7213380ACA66C1A2F08398E4CC372CC0464BA5B84340
FB0B0C6A668AD050988BA79F238B29C2868A2E16373214E15B4EDC98A6120BE4
11C80AF9657A0B1223563C73455C3B210EB32E9ECD59260AD49136959323529B
271BE60087F28255184367F378B52FEC2EAC3DD854C016A9192012CCA8C41DA6
0F21057C71BBE1812D8B0F063E7237A23874FDF107D6802E0D47B05F4E3F90FF
9260F413F8049FC4986C32EEF2279610836C485C042EE0907D3E217CF5C8984F
2FF335C8328D4561F80A166BE4A151CD0F62552A65F3A1AF741FECE3B891444A
0C04E1AF1158F8E8279F41EAAE752255CA025DA733CB0278B05E234ABC4D2182
841D13EA94E5D8038AE0894B9B306A061AC10F194DA2ED25DBDB6FD109168680
1AD3F5186F67CB0E57417D6E11DAD80B4A5E9B2EAF61320E2A170B355E426F8F
540B2B116DDCAF7161B5B014635C8F4D0AC37C8187418F5AA0BAF41578888843
04AB343A6CB1F4C806FE6CCA9E3E2AC5FE9A6E991BDF1F711622B613088F7051
41DDE999515C0EE127B45D9A8E510F9258136F24E6A61B6629D4B228BD3C2791
AA844DE08768EACEDF502CA4A501E7119C2D63CD824CAA34DFDBCA19CE02C1FA
36D83CBF112D81A3729328DA0E948505695828305C4DC81F8443A202A4628899
9B69D9C139282DEEE0A83B86A8BC8848D30A8328AA348A81DB499B4473445C95
B1983EA2ED26DA2ED910956E7B937814F2149A31253B35D067DB51E54099FE3B
FC52EB8DA74511232493093581B2F30305475A9D0AAC364B5250DC626D5B697D
6524AF72CAEF2F42B36A9A844501323CD265B23522AF2FBDEAFB9A24416E27FF
284BFF384788D37D5EABBE3751B5E156FEFF704B50A8B3EEDF499451A60385E8
AD44694B65025FE26B2E205E3EDBD4DFFD08C190724EE2B70FC4618CD324A5D3
A438D463C8F7457FF5CA2E3D6AA64F901026A816CB06E9A28FAB6682EBC89785
5B722F33BE7A7512A45727B083BFE9EA846354A14F0BD34322C16A1B7FE45A75
E2DA9438B51E48875B474D2959EAB20332C26B104715672EDCB91CB0AC7A6355
719D252271E33581023FC4A583949BF42AA2974AD4F5BF78DFCA6A222CCA3476
67E4517F1C4F8AB42BEE71B212B5519C3FBB421348CB2F8AA1469124D3E9544D
45B4AAFC7EB2A24BED6185F6895D3C56EC6681E0D19722F1E2222A0808FD42AE
DCDCCE9B5352AD9F242DF8D39BF1F202AF8C29121C96803E1D2343D7788D3775
6AB266B86033AC6DD8D628F1617D8E44D4AA550C141EFF917995973065A7A088
D1BB50C4D45CE2554CABB36AED18F94E614D8C15A87813FC7A43452753A4D130
FEFE1F50628086731B0000
}
insert-event-func [
either event/type = 'resize [
mn/size/1: system/view/screen-face/pane/1/size/1
my-tabs/size: system/view/screen-face/pane/1/size - 15x30
show [mn my-tabs] none
] [event]
]
view/options center-face layout [
across space 0x0 origin 0x0
mn: menu with [
size: 470x20
data: compose/deep [
" File " [
"Open" # "Ctrl+O" [request-file]
"Save" # "Ctrl+S" [request-file/save]
bar
"Exit" [quit]
]
" Options " [
"Preferences" sub [
"Colors" [alert form request-color]
"Settings" [request-text/title "Enter new setting:"]
]
"About" [alert "Menu Widget by Cyphre"]
]
]
]
below
at 10x25 my-tabs: tab-panel data [
"Fields" [
h1 "Tab Panel by Cyphre" field field area area btn "Ok"
]
"Data List" [
t1: text-list 400x430 data system/locale/months [alert value]
]
]
] [resize]
For full blown menus with all the bells and whistles, animated icons, appropriate look-and-feel for various operating systems, and every possible display option, a module has been created to easily provide that capability: http://www.rebol.org/library/scripts/menu-system.r. Here's a minimal example demonstrating it's use:
do-thru http://www.rebol.org/library/scripts/menu-system.r
menu-data: [edit: item "Menu" menu [item "Item1" item "Item2"]]
simple-style: [item style action [alert item/body/text]]
view center-face layout/size [
at 2x2 menu-bar menu menu-data menu-style simple-style
] 400x500
Here's a typical example that demonstrates the basic syntax for common menu layouts:
REBOL []
; You can download the menu-system.r script to your hard drive:
if not exists? %menu-system.r [write %menu-system.r (
read http://www.rebol.org/library/scripts/menu-system.r)]
; If you're packaging your program into an .exe file, be sure to
; include the menu-system.r script in your package:
do %menu-system.r
; Here's how to create a menu layout:
; The "menu-data" block contains all the top level menus.
; Items in each of those menus go into separate "menu" blocks.
; Submenus are simply items with their own additional "menu" blocks.
; Use "---" for separator lines:
menu-data: [
file: item "File" menu [item "Open" item "Save" item "Quit"]
edit: item "Edit" menu [
item "Item 1"
item "Item 2" <ctrl-q>
---
item "Submenu..." menu [
item "Submenu Item 1"
item "Submenu Item 2"
item "Submenu Item 3" menu [
item "Sub-Submenu Item 1"
item "Sub-Submenu Item 2"
]
]
---
item "Item 3"
]
icons: item "Icons" menu [
item "Icon Item 1" icons [help.gif stop.gif]
item "Icon Item 2" icons [info.gif exclamation.gif]
]
]
; Each menu selection can now run any code you want.
; Just use the "switch" structure below:
basic-style: [item style action [
switch item/body/text [
; put any code you want, in each block:
case "Open" [
the-file: request-file
alert rejoin ["You opened: " the-file]
]
case "Save" [
the-file: request-file/save
alert rejoin ["You saved to: " the-file]
]
case "Quit" [
if (request/confirm "Really Quit?") = true [quit]
]
case "Item 1" [alert "Item 1 selected"]
case "Item 2" [alert "Item 2 selected"]
case "Item 3" [alert "Item 3 selected"]
case "Submenu Item 1" [alert "Submenu Item 1 selected"]
case "Submenu Item 2" [alert "Submenu Item 2 selected"]
case "Submenu Item 3" [alert "Submenu Item 3 selected"]
case "Sub-Submenu Item 1" [alert "Sub-Submenu Item 1 selected"]
case "Sub-Submenu Item 2" [alert "Sub-Submenu Item 2 selected"]
case "Icon Item 1" [alert "Icon Item 1 selected"]
case "Icon Item 2" [alert "Icon Item 2 selected"]
]
]]
; The following lines need to be added to eliminate a potential problem
; closing down:
evt-close: func [face event] [either event/type = 'close [quit] [event]]
insert-event-func :evt-close
; Now put the menu in your GUI, as follows:
view center-face layout [
size 400x500
; use this stock code:
at 2x2 menu-bar menu menu-data menu-style basic-style
]
The demo at http://www.rebol.org/library/scripts/menu-system-demo.r shows off many more advanced features of the module.
Below is an intermediate example with explanations of the most important features. It also includes some stock code to display menus with a standard MS Windows style (OS specific appearance). The menu module has been compressed and embedded directly into the script, using "compress read http://www.rebol.org/library/scripts/menu-system.r" (so that the module does not need to be downloaded or included as a separate file):
REBOL [Title: "Menu Example"]
; The following line imports the compressed menu module.
do decompress #{
789CED7DED761B3792E8EFEBA740343F24AD4D5352EC24C3331E1F59A213258E
E548B233191EDE735A64536A9B6433ECA64566BD8F71DFF7A2AAF0D9F8E82645
27D9DD602672B31B28140A8542A150285C745F9CBF62BD078CA78BECE6B62C3A
8CFD27FE847492CF567378CDF606FBECE8E0E0293BB99D6745992553D69D16E9
58653D1E8F19662DD83C2DD2F9C774F8F881FA7A910E79A97976BD28B37CCA92
E9902D8A94655356E48BF920C537D7D93499AFD8289F4F8A47EC2E2B6F593EC7
7FF345C914AC493ECC46D92001488F58324FD92C9D4FB2B24C876C36CF3F6643
FE50DE2625FF937268E3717E974D6FD8209F0E3328546858507A92961D8DE9FF
AB225BB07C24B11CE4439E7F5194BC8D65C2B1871A92EBFC237C12A45290789A
E66536481FF16C59C1C61C28C032F08056DB48F29A07E3249BA4F3C7319478D5
06B5244ABCF5C30547D38315DB165A8C5A6D821BE683C5249D9689ECDB36EFB6
9C679AB34952A6F32C1917BA67B05F01BAD922ABB1AFD30C0B43A6693249013B
78D68DB9CDC7439E619AEB4C9C266561B5923788E0E7F38223B262D729301D6F
5ACED2E990BF4D81BF386293BC4C19118FC3E09033CEBD16AC11CF44E42AF251
79076C23F9B298A503E0470E2003769D03274E89278B02DBA6205D7D7776C92E
CF5F5EFD7C7CD165FCF9CDC5F9BBB3D3EE297BF10BFFD86527E76F7EB938FBF6
BB2BF6DDF9ABD3EEC5253B7E7DCADFBEBEBA387BF1F6EAFCE252C1DA39BEE410
7630C3F1EB5F58F75F6F2EBA9797ECFC829DFDF8E6D51907CA6BB9387E7D75D6
BD7CC4CE5E9FBC7A7B7AF6FADB478C0362AFCFAF74035F9DFD7876C5F35F9D3F
422CDCF2ECFC25FBB17B71F21DFF79FCE2ECD5D9D52F58F1CBB3ABD750E94B5E
AB1605ECCDF1C5D5D9C9DB57C717ECCDDB8B37E7975D060D3E3DBB3C79757CF6
63F7F431C787E3C0BAEFBAAFAFD8E577C7AF5E55DAAFA09DFFFCBA7B01AD32A9
C05E7439D6C72F5E75B16A68FFE9D945F7E40A1AAA9F4E386D39C2AF1E697097
6FBA2767F0A6FBAF2E6FE6F1C52F8F04F0CBEE4F6F796EFE919D1EFF78FC2D6F
F55E53AAF17E3C797BD1FD115AC34975F9F6C5E5D5D9D5DBAB2EFBF6FCFC143B
E5B27BF1EEEC840315E9F4F8EA18ABE6653919D57B4E86F38B5F000A341049FD
88FDFC5D97BFBF00AA21198EA17D979C1C275766360E8D53C7C0513780BDEE7E
FBEAECDBEEEB932E643B07703F9F5D76F779CF9C5D42060E1BE8F8F331AFFC2D
36037AE42DEF3BDD172F6D167E841DC8CE5EB2E3D3771CD8A92CC5BBFCF24CF0
09D2E3E43B41D4C73BAC2E6175FFF5A0FFE0C1A05CB6B86459F059094673BA2C
F95CF5C0CA57DC26C3FCAE954D929BB483934A0FE69F92E19BBE98DB20E1EB0E
2B5645994EDAF90C655D9BC468EB3A29B44C0B67E9B0AF9EA86CA2CE719E0CF9
EBBFFDE783ECDD8BF38BBB831FBEBDC98F797A7DF9F6B6FBF6863FBD809FC73F
9D1CFF02FF8EBE69FFFD161E5E4CBE7F7571F0D371FBEEB47DFCE6E1CD838F49
7A051F4EFEF5E2ECE77FFDC89F0AF8FDAA7BD73D9ECCEEB0F48B2F2FBEBF7AFB
F6BBC39F7E7A7BF2DB2FDFAEDE24C9F827FEE1ECF5F70F2EBA2FDFA6AFE7C383
5FBBE777EF8FDFBE181E9F9E9E7D7F7AF64BF2EF9FDFBCFF7E74F9EFC5717E74
FD53B6BAFEE1877FBF9AFCFAF6B78B41F7F0EADFE70FDF7ED91D3D78F3F4EFA7
776F57DF9DDCFDF0EDAF372FF2C1CB7CF1F0EED5CDEDEDACFCE1DDE9FBF7DFFE
38FB707EFDD5ABF4F8CDCDC9AFBF7DDF3DBC7C77F9E4E3FBEF6F2E0E8BE3F70F
7EFEF7F7B7C7CB9BF7DF1497072F3ECE9E941F3EBE990C96CBB47BD25EFDB67A
F26A79F8F1F8F8E555FBFCB7BF1F5FBE6FBFBC3DCDAEFFF5DD3552A83B7EF9E0
EAC3E5E2A7C9C9C983FF6AD41FD8AB7697E0AF3EF1C92029789F8F16D301EB7D
4CC68B14DEA405E78B820BF0019F8956B3F479FB2E9F0F59C7CC40C581E55A45
F65BFA5C02D9B948CBC51C2649364A06E96E817918E481D9E46079C0391CBEB0
5E7EFD3E1D945FF4597B9C0F9231E5E1F82EC625AF5F8F2A78DFC1326D7854EF
D59B0E3B3CE069897F6B878F7F3C8911C0EBEE60852D441BAAF0D567E141C504
55154024758536AFCF5F775B45324AE92B02E323349F978345B9432F590FFFF9
82CFDED3F48BBE394293E98AF512AE48521603481F08DB7750981665321DA4AD
7C2451D0D3A3ECA7AB8BB75D968DF8945F66E58A65A8CFCDD35F17D99C4FF25C
AB290AB6078893CA729715E9FEE31D054714939D29B016E57AC0375F08BC8C76
40134CF58151E9E7029AF5691799E519BB01993595F5ED025F5AF946F9623A7C
CE461957CE9050950202A3BEC04D9575A8E6658DA6090B4CD3BBD61014E0B13B
2AA66C31CDA65C911CF36E1B72A52C9D30CAFB78C7ECEB49F2219503FB6396DE
F13F43F8AF4563675DBC3061C33B04FA9A8FB90F5F704EE2150864FB9BC04C87
20E307F9389F77583A1AF16E94FFF2AA801B9AC1B1D96134A2B908071B676DBB
A7D3742C5A8143127E57F8091267DF7972277B809A28204B89038DEF7B8A4282
6F1DD135ED19D768A765CB120756E691CC299A1E000AC9CAD861B36CF0A15A18
B9E21FFFC47FDB30C1B7930157D38953ABFFD9F37D90B6D8D135A4530F1C1F44
8E6887E8A41F3905AAC4E2ED063D3F9B3EA7EF6D415FBE286AD1E3736A0CB604
C59D4B18DD4A408733799195FA253EF9CB60765E2BACBA7C991025EB6DDF18F6
86FC3619C5CABF73CABFE0A8850A1AE866350969A1273E41ADDE2CC9E65F68DC
883B2D44B8202F531A66BA093D374B47FD44BEEA1DB1C33ED5FACCCB4D95C254
CD4355F8A02F7A0EDFB30A8FD9AC00F9841CA8BEE66B47BEDE2BB3B468231D31
5BD1EE2058174AC695D9B6D01E235020DB63CC56034A0EB5FB81BACE87ABF688
ABD91D16C30AB23D866C11088A4E7108ED1A3A213C2E9A920618413617C287B4
D2243F049E2DD0240940F77C14405D8B009CD5A030387F83683A725E57014036
7F619B858385EB1A8279EAF9CEC81681658289C00A83A9CAEC695EB274322B57
CF231C43C32124AF91F72466837C32CB8BB43D4CD35960E603111B991449A1DD
4EDA4B8A1648547660A00A734F7BC55A6CAF48C19467EAE7B5441034DD676D76
B41FD509F79A82F242E93B6FFB81E90B92F543B02D6ABE9E91E915405C094C06
1FFC6A9FCB8638DB4237B626C9FC8398CB08CEBAA9022F99CFF3BB4DE11913B9
42CE525CF4744BC07BE56236AEACAC1452505AB1B5BB4EB1F3B9030D12AC8F43
3AA55E9069306C779E0CB33C32381A0C2F93045C9F48A76C0F5BBBCF1744E371
CB7C31C8E68371CABE5E7EC3BE74190E49CA7CAC1869C0E036057D617B0D8866
311B33CEA669EB2E1B96B7EC887E0C92192B7E5D80091A7FBFCF613380AB82E3
2850C8CBBEE434F96A7978C80E8F964FB59EE5FBCF5E2F6A36DE9CF5B07813DE
2B16D72ED3DD5B02477966968F5737F9342EFAA4D8355A835277C9A52EFBC679
BD0269CA3F1DD648D41858DE5521B04F3E07D887B5F2FFFE44A859DCE1F24872
590C9535D2CE3B002AD69D7B435CF364E56EC16EF38F7C714533165FFDD84C6D
2C442AF345712BE5F9E6A962A0B15EE19ACFBF60EBC227D912C043B4840F1A34
959BB62B488126B5A7E9DD3D90F7266504CA26937498719A8E57B473067866D3
9BFBAF2FFF4A0D53DFB163A0C0DD661552566F0B1E28ED7A31EE482130B509F3
A769369CE5B3166E617BA78E7ECFB51A89DD651800FED902B815C7120E12A891
CFC0D3A11CF26BB5BB5F5305FEBA6715768BA382659C261FD32D08575B26BD02
A81EE9BA98A27C4D87C2EE027E0583319FC65D99E5015BD130EE87677F7BAC6F
2149BB15B2BBE403E9FED854F5AEEF9F418C69D1370FDCC022CE51F90DF36E51
D5B502EAF906AA39171AF968A40C6B6A45105BE6DA393BAC9C2FFC16655F8A82
2D3A86810F467C7B964CC3B047F93C4D06B7ACC8AEC7E0C182206A146F29256B
113689292BD805C45A5E3B6D35C922C2025E5F9DA2EACD3C5FCCF466910024FA
123F468105A4910737AB1347C9B8A8EF456469B3B8BBD8673ECD4FB572FDB597
C36E6A0E89AC5F0DBD4EFEAA0C38AFE685032F19A08F131ADE6D761543729E73
0CF89CC47B89D73E2FC1D348F09E8D4D608216153C63BB1C024D794ABAD81364
3F2AC05DEA8EC876B2F6BA7DED75B24BF048E7892D0E25E6FC03C26596E0AC0E
963FDCA9296EB351793F8D073AD3943A882CBCD4CDE6AFBD8D835CE62C00BF9B
12C69D1B54817A3A6423BFB54F30D6BDE831CC8D49A80F1E88C36865196D2BB1
DD221D8F02EBA5980110F8BFA2CD78F6B1F1C7753207F8E606A0FA6D307DB198
C1C24A7D521F404F11DBB98E30B0D0B037D3B04DB4CBE5B57908AB256E37693E
22DB7A6817D46AA0264868CB524A1E2CDB501930048B516F4CC6422E216377B9
9A3A6D95790B57A935659AEF78421AE58345D19EE62D3504FC23CC1D073652D6
66604C67F9DC88621D23C28E234592095C3B6A752893E0A845C729AE0AD6ED19
43AA33C343AA54DA9CA2BCB513703DB65B1BD7ED36E982462DF66B1B1EB902E9
F7B1BAD880EB4D26751A4388369BA4ED2FE97FFF353B4963EFD25D73143672AF
EAC5B1CF1EB2BD83E521FB0F9129FB2DDD6FD4D0BA65BEF41BF9AC1845ECF790
C06B0B80783C1985CB56C23B852FD6154EE8B1156DB7E9C5051AE130990FDBE4
C165CF43E49965BB66A90553DF193BB019DCDCB72A9EA09A6DC2DB76FA98D076
FCB6F003C74EDEC7E8F5209C4CD4EAE8CF4001AF47DD9F03354C8A587F342281
A495EDED908D5466AD2DDF370D9332D9668772C1B855FE206B1A78447518380C
74D88794FF11BB9BEB57046628F043C7A3571B4EC7CEEC414892BCD4EEB6AC87
361EC2722D932835D702B5A1499512116E7BF0B00B62F01C0AC9FE5A1F035A48
B2E04479BDC8C6436BAA549F775EC037E530F98895F94D8AAA091E6B03854CE9
85300D1686FEE6B167EF24A8D2C0971DE38023CCBF958C70CC0DDFC303429AA7
4536044BFA545781C718C8295D3AAAEFC8021C2B9E7F5A66A32C9DEB221C046F
204DC9827E6611F83CCFF0E00595118674AB556DA21569287CA9EF1AB295AED3
61D63A09DFD3040858DBEF499000062E400DCB303ED8C54D3989E348BAF0BBEB
7461B0E818C04D43869B5F398057F24B0767772743F884BA55086380AF88E1B5
C9CBF55C35897228647C3994B365C7AE36ECC6A9FD297D45BC6E89A603580053
E921D9A9345FBEF7345F7A41FA8AF8D1B0BD238D6E190AE73BDF6E0B328BE1FC
EAB568B9E4D2C508C770310B57CC66B8A57A8B39FEA8AA94A047B0945B974113
6F29CB55D42A5A35B80597E1243087191CF07517D04A66920013D9B42F80CA48
87100CC9B743C784313F9745E23B40F1CB22E5F7EF5F7DB88B0F01DB66571447
96890061A3C02114AC0F5575CCAE99B4DCC3AF8F1E1F7EF5CDE3C3A75F8668D8
2C5167EAF30DC8D8F73401D0211476B83C723A58A02F5611A6AE1EB0DA2A23AD
CB30B0FCDD1D27AB7C51B66843A4CA27AFF0235959CC89D39E0FFD3317BEAD9C
2BA0E37C3859EA4E12B37FCFE4BA2215C06C84F389CFC805D4EEED0ACE619FD8
EE7582FFB45AAD3EDB8BD0D94A519356C71E51F694594DF67E6807B666784B2B
AF6BCC685404952825340BA5D2A24AECAED3CD640E828A94F196719DCC3E3162
11EAB47C56C2AF16A9327BC17A31B7702614C737CB9C5129FCE947D9EFE3063D
BB4B26BE608590CC2EC2ECD2B2429C4A68586A8B2F6DA1DF109978DF4905ABA6
FF20ADDB8790FC8484BEDC2DC6C8BBCD49290A0031C5E35FE4C4E4AFC7FB32BC
C50B498F2F88BC31BD898D2C485AC5219B1296DD801774BD65D2AC525075A84E
3E9C25B29B56DFDB15FB911A0FA1057C725EF56BB0F3EF746E4E9ADE2EA8F030
7FC0BF45DF40489E93FE24E4D927705BC6DFB3A4BC857F3743D95A55C0F97859
5F140A24C2834E1E47A4AB990865D6C320090DCB8876F71A66276AB0DE306F5A
81D4011B648F7F15478D10DEF318A11B1037DE4DE4FEDE000CA4BD08ACFDF8D7
3F1CFE7DBA234A4221C22379EAC5927970731340FB7EA92D04011E1103498040
FCA2408A002912EE270AAC65FB5AA2609371BD81F8587F6CAF293C7E2751103F
70584DF16EBA9F2830607986AAF9F50F87BF4D51609130200ACC4E6235B131B4
6A141007E691C8C34DF46761173484C02E2EC43E9165D718F3750A95F0FCA3CD
788245A09E894125D81F0C88EB5155DA34054137558276A5D9B44163FFB0B62A
2BAB1A807B08693F0C26D26234791ACD6DD8BC8AA93268A51436A2E0F79AAE52
751986CA908D52EE5285BEB3CDD9028C99F72112194B8336D91891B0EA358824
ECB221936C8C4858D5E64402746B26830DB9CD26E6E7E7B8F08C700F6EB01BF1
F93922DC08BF9CC0A67136F8CC5DD86820C7C7F11FDD818D86597C94DDABFBC2
BAFB2E9AE17FD7C94B458EAA99C58C2D20DFC64F8060D81E42C09FEE6504EA89
53479FC481813ABA24B3993C45D1A62867BB9345B948C6F5A76DCCA2782484CA
3714ECE6C9927C345AB3D1587FD41E0769371FB11E98BBE9F88EB5EA1B67C202
DE788CE843427526A938DE950EABAD7C379F42A15D38EE850F2BAE437FC2D35F
EC5383D2A31114C27346F000BE579F1A1C3BEAAF4316D1917229B97D298EE66A
08526C1B191B682BE4E930CAE66A05BE7122270C7099127B6DE423415BA7AAB2
C0618C4AFA4B63D72D361B3ACC8AE47A8C9C9A4EE1A9914A2AF88F803C63124A
C45864A6863B03A1E334FA496D268A09ED41ED10C20CC9F0FDA2103BB7EE16FF
317E65D24B496CF062DC120A273E17BEC6655ECA48A860EF5E4CD221BB5E395B
BE543EB6B9AB1998A2F5E03B30A56342A51813A8569828088F6A8F8481119210
063D010C7A0218F4C461D003C2A0471521B5C199CA5052F57734021D8D4147A3
D05138740C24ECD091F2C42B524610D0420D43F1D1213B2014D208C88300FB72
118FA7BA22D9EC435DAEDB4E8BE27E70DCAC4FA05208A4D5E373773FCE063772
0E1A1AC76C7DD39241C6C3AF96875FB9390CEA4E92A5F19B1E45FCDBDAE05966
40BDC3469DEDA262F42EA2A2198EA9905E1D2F7341F620D789B3126ADF8EF5BE
5A3E610F8D58C6F88DCB551553370226ECD6A45A5F73A87A9D3864CD48A99313
99A59AAAB155ADC2EE17F78D1E77D8475A16E0A36017412BB96BE9A138FF2408
1E72658DB2ADF382661431D28C00701D37AB44133E928AC15B924D1693160642
A68D6D2F91D48068AFCC3A0C88EEFCE32121695F0203333610B4D6DB32418B20
B63121EDC12080923829B4A2AE35280AA18C0CA4318491CB46D4921810270E92
871791C6514CCC5E4020767B2231F3C2F2537A16F5D8BDBCB6EB6B234F8E90BC
D6624E341A73DB51D015F8EDF3454500EB32347CAB2835A7BB3938CDDE7CC844
70C6CA1468026810EF18492163487399E2D168DA4B43A7682FA1359236F84BB5
157F491AE10F2D8DF0A7F60AFC932818C66831085191459AEACB8E4915DE20E2
9ECFA78468C9B1F40B38359E971DB357C2A303321A1DE69DA45446B3335D1E32
23B679B10B9141B54C907B5955F054D37406BB0027BD411A7F6B8DB2162CC1BF
32D49C9F0246610B9860F750614B8E43610B182F6C9077CD89B9D9D8AF889575
457804850AEFC7917DEA10B5716A82747C263065ADEE0A6DB837BAE3CB268557
6661359756253ACDA851FA9B12A663CB1F3545D8AA50DF95C6F2CC112DB84DF8
F843BB3C46EE5348E84205E11A899AF38EBA59045F3AD72B28BF54AF3BBCB08D
0A8BAAD13D5666F2A69499D18952A28A97ED14E321AF976C4DED126FE782771D
89E8E1D1C1F2F09B3EC37C151A19F4305D40232708ACD6ABEFE2ADF76C157DAB
9EAE62D1F355025EF08415F39DB1A242B15356CC77CE4A15734E5A550C1E9C80
557B83E1826C7ACC228ECA61B6C27456E76258D70FD3FC5A925C9CD0984BC682
6F22B6EBCA642F2AC327684E42A57DED8E53CE49E4719BA2953E9F1355DBF0D4
07C00A82385A5039C1E58FA6AEFD275622964AAFF79471A6EA7DC39EF6112E18
D4A0520BC326F1BB03516363D162DDCC91E8B12A602A22240431A8F1D89C7620
386CB0548B7DC94B0582B4064B3DDCA8941743CF590F56E12A8013BA89E3FF40
060AB6431C871C062F9E07E2E8205673349DA29B3FE5C5B7C6AD25F6B3E627E0
AFC750DA137E0FABFE5CC90C2B62C614C34AAD306262A801A69EEB478C202A9C
C5CBCC7B26850E27C568E22D42A153B8D8C9C7543F8268DADDEE3C867A493218
A4EAB455E8829F36E5124D51906E52C2B683873FE869304E93B978C6CBBEC4F3
9F2862C0DA49F3C6321DDC271CA7D5073B976959C02564181D106605DC6B28D8
5EF6387D8C752D4AFE136F3115ABACDD42DEAC8741382D789E73C8BAAE2B3561
E6C0CE2C9FDA1170C073132E7E01FF4D9A2C335ECD4D3A87F9F20D7C848262A9
974C5777B7299F2C304E92B857B59CA7691525445D4CBE045BCFA83B27F051B4
72E83650E3E7B97F06D1D0BC5E352BDADE9D8405FCD5E7FEE157657F8A17C204
E83E370882871A087BF8EB292448D5B890D3146923A192B827098F9E7C66DC3C
BE10E27596F315EBF1CA647FD15917DF1C093371E5804DB59C2D4C5C18428530
8EDAA8F35B6E71EB97405C5CF3719B262AC610142FDAF314C35011059D7A8739
D9213AE6602093431DCA728755C8B036709C7A49F501D0A818D54B043C6FE40F
3A755C96C9E03655D10680B23866D2255CC13CBD41B255224F6100CAAA7A5B49
7AF0426ECFB865FE91ABF4DDEAF885C7215EAC6B45C5AA8EDECA5D7A7EDC7E04
BCF2B98E42C4A7FD39EFDD95D8D04E87C640C66EC7FD4771D59CB8F519DF8B6B
832B446A5FA760C8623B67487A11D44BBC448D9C2EF485B5D6027B3887C3CAF0
2C258F1BD72B1941F8B10A4C7A69839C71799112D01DFC11868967D01C98D43E
71D79B6A1FDB1BA6A364312ECD4064EDC4BE10194E363B4B979F61B170BD6277
B7D900FB749E8E00EB5C459AC093DA31D909CB2CD55B05FB98CDC139C710A6CE
ACA2C40E8D4E0C96698E268AA28ADF604462F8E43EE58B08BEEA11C040F043C1
339EF9B622CB1079D74E459C6217C7106795E2C031AAA1F202486223F8DBC7B6
55841F7E70EB14ECA5EA9CE2DD9D9E3A81A39C3A89CDE0AFB74EFC60D7D916DC
14B20EF62C49AB0F4BC668D777FB4A38136CD64F84BC0B1952B58FAE133E5369
7A19B486AFEB92DB20DB56A8166A49EC36243FC96484936774ADAD12B85E8C22
F7F820116A5DD74C1B9FBC85541BC6372B2AAC9AC1C235DE1618BB0131EF80B0
AB5C91CA5FC53D88AC0EF29F20F6DCE0DA180A75B4D41801418216478AF2F8A2
E738C4097E09D1BBA6878528EFC8320DF0ECC84252A00B3D4CBC65E1B33090FE
EADC353BB7492857E18758150A6CE775FB78A7B9CCA94EAF162550584742F39B
F21E52A030E608A3209CD090732D6279364D89EF02B79EA615CD1A43AE2A65CA
7458CB4A9F82DD64552C181F407FEEA57183056DDD7AB14E07ABD8CF3C2B2D9D
462A30785DF8050F1CB05A95C68CADE1E879121B77E4B4420C23273E8D55ADB1
C1832169C0E6845DE663141D2255C4251999D15137E38BDFCF5A62868373AC25
A8F88B85AF752F538869221C62C56EF38C452474F13F97D06410B23677344A40
E80E7B07CDBA2F9D2DBB4288CE348CC2A4FE9F26FC22E4135828027A68E7D28F
26F520FD7A83A41C54EC6FF730D5DCCF50F359CC34B4655C6FA4F9CB38F3A730
CE8406825F5D263BA7A32CC7D7E0B689860608762FB48219024AEC3A783471D1
875E48F40D60358124BAD30B09BF21520D20C9F5BA1F5221B3D541F21DB278A0
7FBA920678E23A99DB3B65C6DE19EF072D91E8C212E3ABFE666BB855326D96DC
D9610FC4043EE5D3F16ABF66B6A8C2136D35833A4A9906AF6B670A264DD1FD10
9B0B50B2FB906052F0CB6A24284F8A4DA79FC738FF97C4FF4BE263FA5398E371
67138EDE0536CACA5B383685220AF707BF901400162FE1220FB87A9AA9BD3B31
E4387D8AE92E9F7126B3713A819B55866C95965FEC0444A74CBFCF0C4642627B
F398056F0BB399056F0B739A0D6FE399AD492C605F52B39EE91554B910069C3A
F0335C8FD24AEE12D3FA5C9D0E71856FCC83E6955C95C50EDE469AF88471C5BD
41DCD0D20357A8F86C83B68CB1BCDB4BBEABF81CE1D75051E997E62F4A5F8365
653C677F59339AB3674A33E7B66C48ABC4E796F71F3A1C55B7FB21BA7D367D2E
BC9104A9CCCB6A9449CE3EBCE19F5465C52D58CA35A8DDBF15A13D98CCB6300D
C61D6BC1CB3D8D43D2E1EFD5BA087DB36C3B527943A51112DC8F645FF716A64D
40ECD5212C7CCF827778A1E9AD87CBEEC58CA13285BE67784E635CB6F00B3CC0
4597E840D61A67702745A32BCEFC1472A9E47F835105FCC00D6F166A8185BC81
281DC86B84ACB6CDDB174046ADF53A3BDDFF18CC1CD9D0DA9C4A067719E7BB11
93ED2E17D8CE09DDE72C82BBEFC17A008619BAFDEF8A58DEBB4AF12CF66BEE80
B7857245CDD912CE12A6BCBE6F0B2970371B72015DAEA81FD19B213464550E83
7914A2768E6DE0DD34393E6AE13BDD02DB8A8B29DE9928DDEF7FFF24D7996A1B
DFF6949DA66BA266C20B1222D2C99E7D0A488B29FACECB2EEFE98E37F08DEDD9
6523333BEBD957555627ABE83C8477316E5B60BC03A0855EA13C0275906EE8C6
350E2C5F9040359242A48A1657BF928DA4EACD8392943482C54DADD1C9769C26
1FD76524234520CFF96C20BCC42B7B7B565EC15ACE461B36A794B765EA4B369B
2B25D834753EE915FC12BD88753E12915C78AF55BAC4B8ACBE7A9DA0B8ACB863
92F9BE8C563BC23419A8B7E810DE66294A31A1283867BA00C35DB13AA47589D7
F91F56911FFD07FCE49ADD1AE8D7F334F9D026631CB3C7B6499B8E230322C224
D63CF430F6B58F5C8F85ADC266065F1BD591E2CA66B1B7DDD1A3B8160590DF82
ECAD756D4EE4D60C4254785A021FB37C51F81B23393B5D96F384E5A07956DB67
FDC07CC2A051D50C881F9342699631D915AEC2A2A7765B43462A527204AFFABC
20F691515747F508A2F124FCE375FB412C5D8FE50A458872206BC7D39B0694A9
184861281667504F3556A121D9FD28DC2BF1AFC7090FDF6F27795607A67CC3BE
7372DCDD66E3D08226BC04B661C36CA7492A877AD3358FDD057E2CB1796E2744
D6E7C6D465D81BF02D8EE2107E7E3948EDB4FB3CD4326F8AC946187A41A982E3
F2F7912856C7F5D7171CEBCA0A2BFD2538FED70B0E7346FB33CA0D1CA75B901B
516B1EC9A6B5958BBB7932633B3FC3DFA4944A6399CFF0566E1F5F193AA150A5
632B9550CF7828197605DF40DF88E227D33D353DB7373CE442FAF642DA572F24
44EF63D815ECB6D68CF079D9203CB36F830FFEBB747D659A0C31C51A5D6F77F9
E036991BFD6E957299006E20A7FD55BC54180AB33D4EB3D7E7AFBBFBA6C30FA4
0ABB60E61EFCB557DEDAA9BA6AAA249F6C4198680F89AC0DBAD7E51C116B04E4
016D15C323B9FA56A0B82EE0A1B9CCEF696EF7496DCC2A6CD82832BD4598D19F
BF342F4DF373E1AF8B64FC9C2D3851E6B87F83BDA67FDA203CD787540D553239
63E33EC24AB07EC87C019F3758E4C7A686DF67C52F950CA76157176FBB58235C
4D3A1EAF644E5728DB56C66A7B41AB8C6C59627BAB3184D4EF7FB003B75FEB52
03B856AC222674B4C1389BC957D5E4A51DD8905B60A0B6776D4D527870E16DEA
DB20201ECC5A201E1A6DB4DB40BBE2F44ED7720B7B9E6B23FACF67AC252A42B7
091FD06DA0FE8F676CCFE900910FE08B57FB7D97F0A2664FD5B658371637D416
0EE0798825110D2308A5F34E354293250249461473DFF1572E811B0C938E8FC5
1B40D27B1E5670A02A7F3BBC19ECDF2091A9DB6A892C49E310A6E5F47EA43190
ABB6316A08346ECCD6B8C5ED992AA02061BC3CE3725F0C1E66A38841DE0D0020
CC1A4CA146FD7A74BC2F43B8E220D066DDEEDA11E3C2F46386191A51B01127EA
F1850E187E3A5238A62059637E2EB863FA4C0008DB011AD3A728D399FF1247BD
3050A2226CDB909C80F9F584249409CFA746AB0C850656BE8D56B7D66CB542B5
61AB71C6F4B75A11B0766DE5AB0A9295CBDCE2E65A418515D54FDA4B0DADC6E0
5BA1DC6DD9DEF54AF02D7ABC82FBF084AF1652AEFD26937C312DF982089C2C93
F9AA6E7166AB8D2AA7DC9CB71139CDC17F165B0495F225C2F42635EF48F0B8FE
0ED371993090220CBA38B4E6C36C143918F21AC2078B194C12B665CA7B3B7C2A
FB01D7CDB00E12E816B705D7FD325515D74DCCC46B0DD3CA00A98E0DD190CF3A
C22181575B83A18FE4FD4790BCF58EB955FA362D20E6A0DAEC2D66CE629B19F9
D712385BEDBFCD6415A406FDE70B26452AA4D7DD874BB0B05B9F95DC997692CC
5A1FD255DCE23401B3CC9075DF755F5FB57FE8FE127790161EA3F88F362EF14A
426206EB27174B339768367D80D86C5CB67AE4089646112342D6802F276EA9F1
7F6FF3494A7FD011155FE3135899F87F7D7454451F4F3BF5AB187AD4158B9076
F44624892F7A23D9CC50C0D3550FFEE6CA388EFE0BFFC4E68C6184F3DEE6A094
64E44DD1D362CBC4EB32EABC88847ED99D630CDF672C7E27A13499119A51BF46
A78584379C0999B795B7574DBC18F412AA38472AABAD7A5F0BA3ADE9A67DA0C8
6B3B68D093E92EE1F97BE1701EEBD8A1FFB6C3A4771B519A0528B0F7B79DFFFB
E38E9173BFA6536AACEB9084AD573B92C6B343AAF61BF39B79CDD4E05E240B2A
0C3381FE2E1FE161E76755C1567A222D06C98C8257A89EF076C4EE381D954C67
DC0F76D92E04AF2866C920555DA6D94EBB83FBF1D945A966E2D3ABF22CB2AB67
F3025204B0949D8D019BAE5131C008731D8CC58E286D6D4500A3445F07B0D85F
AB05ACE78B75003720054C3F6B612CF7976A3A4F1FEB0A323D4663174E96B65F
A56E81D887222450C07BD1432881FB4ED7DDFF6C7266EC816C3C9C8A368E88C1
AB96081C2DF39A07A2E193FAE01E988193BF7466C67F6298144C3C1AA74FDC30
ABA4473B90A7CD2CC38875B44240081886E8AC726087AD9225229D9D7C55F162
7D6C68535054D17ECCFC5530ABB8D170972BFC830FAD326FE1DCE06417AEFFB5
6622B92CB7286B7B5275C478519F6DF7CD8E9485213DD847C9B5D3EFAA3FFB17
D95225C63989CB5FFE7FD2DB50A8E1633FA07A43F27F71CE11FC795574F3849C
C52E145ADE7AE5B055E58A0FB82503BA2A72F7065DBD01996CAF8B688C73FFAD
1B58444464EBB9027F344E6E78AEDE284BC7C30AA224199947F64152D124521D
2AC2FC6E23436757E92C29DD3E8BB70617E635D1FC199DC43B7261E2D882AD1F
683FECB027EC0F4B36FF92B78245EF7B5D76B6E564D3EE36E163A383F7ABF03E
A65D1BB2CD098B1CEE3FFC6131EF29E29F0C6EB81D98B2CDEA8E08160A546A32
AB773F0212DE20D911505BF8CBAFF08BB3D73D3EB1A4785A71769B4C1663F6E5
5113D3BBE8A1A64807D1AD698DC43298C1EE91A06A35D61166482639C4146245
5DDA115E56095BB186155D9E35EBB5359BA39A458322D62C82680FFF70DB20AD
BBA08444E3727DFAF6ABFFB91A32DDEE63CE4CEAB3BE10890C52182651C47701
34AA57292B2DC4BAB108CAF242C6DDE04C7899552E2EAA46B6D3B72599971611
F6955D09CC651CAC37541C6A99487212B6EF696ACB09C832CEE0179AB498BEE7
C8A21F15366632156F5C4C68EA74AE68BDF1DCAE14767622E93A89C066E46321
87443240D217A79B55411514C229485F8205C1D4904D6F3A6E41F12558E3D0C0
D4AE714898060AD2D6B0AF207C89145431339C8274C99448EEC208FCCCC8EE5A
2190F12558505D04EF1494D7D8070AD2FD53BE1AE94BB046E37AE80EBBDFB4AD
E1A966DC1F9E71E7F256F04378706517D2CA2494FAE22794BCEA8B686C16945F
021446B83C7FE2AF11BE846B54E59C1AE14B888B7CA3457DC197A125EE3D0488
77E6F4C88BDECD3C196620CA0E97877092A3843BEAC71F7D07563C52E36879D4
4446980B1E70CB1C26F3611B4FD0230D7A74992E0726A77F4241A933BBD7102D
2A80912D55BE7AD2448460439B888BE051E08A74902A088E091F518F9E3E7DFC
D513FEFF3E0BE61184F7ECDEBA52C5AA50900B9FE14F00822B5E22F9D6101B91
A1D6A0365B0884590547754FDE5F7CB43CE0D37176C37B8A4CEC1FE5AF016D35
487249425D8F213634FDE5B45FE19FBE5A4E44C8660B9B3F2182B6500B238852
AA97CFB39B8CE3F1940FB74932C7E76C3A4405077C3BC00AFE1CEA8B90434BC2
CF5DDB268284695162D017F91886E1E1DF8FE03F63B830C5EC56E6DD0CE50E33
7FF4E3766B6B3828DF03A1A6DB6B77081A4CC110BD41838DD8D6F8790F9AB98F
EA7D49D1348B72354ED1CE0662C4D0A9D9304BC0B0F638A490EF9C52065AE240
D41D84553C624959CEB36BB8EFED11AE0F08C1425C2D5A0975B5F3421EE7B0A2
2F1776D4C8D0A282A3714C66277C0DCD13D52032462D89B04DB3DC88938999F8
E84853B39DF8DB68E8B1994F85074F448078D1ECC7DE9B5331278515D75ABDEA
3EC39827AE4B67B16BE48DEBD12D6A884868F44C77FE86927F21A4977BD42271
65AC89A1B9A0A92C525C9ECD042FB09EB8A5020122F97E7F33587555A703D955
DAE189626714095C1AABF5025F21A14F790BD192CC251E1F1F9C5569AC6DA5DD
90FCA67CBA8765577077EEBDDF13F3D10A09788C7859440EFDC4CAC56C0C8BFA
BDA095C24B2BBC7849018B5A46A826AEEAC8CAE3861442082E418966F57F712F
A9FDC4FFDB954B59A3FD2AFAED2746411BD7A080D27EF156455E9880AE83114E
54063ABB38BB7C22130BFF578AAEBDF0C6A774A2A4239B0487C03C13741644EC
C81991CFB4112B966F1C09EB94EFD3DA6DEEA16BC39083E23F7771F0F47D1C39
CAC64802CCB156BF88C1FAB938931063BD719E0C9BE4A706484E363E6C87A985
6127C24492A2E3AC6CC967C558DBE02BBF362A933F34A9B09F115C71B736EBED
E1EFFD90E3804B02BB5A24081DF06B280B8B46A36F2B83AFD998B3567742015D
5FAC0893D49FB16D4ABB763A7D9D16E2F2CA685E7D73AA78182BCDAA70B3BF47
C8EF836B2C10BD70954D6AFD6EC545D4FD1B4DEBB570A3B19AF51B2D9681C146
23D8F51B0D284504F8069C1024CC16B8C16D846AC8FABD1744740B3DE847D41D
83887ADC8F774B5DA0074E7CDCFCC11DA0593DCEE95B247F632DF18F50120DC3
8C97205A49F4835A4716883D1A4D0AB12BDB5C004A83EE3A6228169B0996771D
EB0D5F0CA4D3E84D66C2CE439124D416A8BDEC34EE488C5D8BE8B12E599F1E58
996209331977232A4FA6637C57A87B29B85681260CEBD2167A198DE2E0ACDAD7
5BB6D7AEC05D9A88D6683A570818722BADA51766A0FBE8F06898CF6077C9BF78
2E0DB8CBA64338A3E31CE0B309472628610EDA66DA793347D3E0755A30BA168D
F32BA7E6D4B857651D7889BAEDE0FEB8D961FF772ED2A29C670315AF08BE6A42
4E922599C8CCCB16E028880670CE9B055D00E74304DD2DAE9D4CD2619694E978
55DB66153B5C41B7050859DBE8DE61CB9487264DC453381509B35ED57E677CED
B027CB2D38C04902C9D07E925E4409A475DFE275856A277C476A4548C9B3F41D
7D9A58FC3E5C1EB0FFD020DB4BF6904F298747216CC11FBA85C790C99485C7F5
EE91AAF0F0E0DD3D92473ACBBD1DD8B8D83C697872BBA2480739C403E772EA79
456C3585A74F4F2E3B951ADA4B9F74956DC157C48B66FB3CDD6E9530223798AC
60BD0958CF5CD632C34899014FEE951EB23D4157986090279F1B04D8DF00DE91
60F050CBDAABFDCA54F2D01CE6D5E3A4FF5423B65D55B68DC106B1203ABE8821
1DB64A0BB754252E8659C7FD53AB86A87513696B1322FAC0F8C9EABB76C7A4A2
43C1695E43C02A4FEAAA98B9A3571D01F17838460D0139ABC31375EA29B6EFC0
E52FBCFDA3672F376C0B53AE6D3A3252A78A8D37A64F9F3970CD3026EB403562
8CF43D1B2CA6B03266210DC91E706106F2A88EA59836C385D64EC9763539E792
866DC0847657AF6492156C060F6620F82BFB54F83C09A560DD64B799BC2C69B3
BF5979AF280922A9C057674E99BCA1152A0B9D502C5C940A89113B451709C5D5
71AB33A03C14E1C04C02EB284A5283681FCA2F6D76E40A6882C727097680CF9E
0C91C5B0A929F42BC3548F4F63C4FAEE141B593D21F8E49FA6DACA7FAFC7ECA6
4621478BAFE94BA3E94BBB5B44BD2D0F13DB048903B65BD132DB18A06BBF4299
954599558532EB4A2CB3BF6D38266EBED3539F031B0392CB2BF0CE02EECED59E
6945B5E6899BDD9DDAAD498897A98BA8D64C070B6A028E12618F19A93E1B63CE
582FF2596D96CF16B3B6200A5E77888B1DB1D65D2BB937FCA8AAE46D427C15DD
13F10BE4B15CDB76D2C8CEE44B644F29577044AE3D498AD20A6D8E8BC86BB8C6
134F47603CDB4A7050FBC09BF92974620E00DA39A55BBFE39B48A7049CD70D0E
AEA2A38CA7289D3C810398A34DC8E524E4929B74ABD72CED5C828B94B830B5C4
9B3DC78BB4607BE9321D804B181BE4C354DEA5BA5B488F64BC9CCDE104E7DAE1
3E732F1AE60A1799A4F08867462629B86ED007116BD7AE5D27F053A03674B1B2
0FD57AA25E4172A277F47B0177D68E3C3C4BB645BAAC065A277C41A0991DF8C7
292DA6A274322B57CF290C72E0D25C48C39C5D83575DC7A435DB05C6F1E6C7C1
EB8FD91B084C8CBE938BEB36D8AA343658DF5A7670AF7FE5E1D7DF3C3EFCE6E0
F1E1DF0FAB1195D5360FF9C94E93091F4B3B57C96D3E497698B0211D4A0F70F2
8395F210C36290DD25EC0F6B9C9D330F5219A750552CEAEAF004060571E21000
A52BA51E1787F2FC10753F522F99DF14FAC9EFA3DE122E750041488886102A87
ABA770B19A8B25ED1A5070644FA8996D0B1F41C55075F2C0F1A05CD2ED8DA855
CB200DEEF0924790DDFCE2627B9FCA8DAE24E4D5097DD2D7FE8AFC97F05774CA
61B00BDA46923302DEEA5DE840DFD28A4BB116F063283A919EA582A353380402
4A7E18C10914894BB871F9C231127E3C7BF84E9E9FC7EFA21FFA61E34BF043C4
594665513B6F1A0972B8896FA242125D82F79CAB28ED420AC0D13DF2076EA444
504916267535093540C590879F7541D8AB8976C5744F372A84D78F33DC15246A
C5B785CD244F19C8FADADE1350BE241DF2EBDCF11B2B6CDA6FFF89F2DB4711AA
1CF61B818AFBC9C9E44A0CDE781C8D4EBC82CD2A31195111976672397DE1BC08
F7C5BBF1E0AB69838106297C0F86998CF156CEB3E94D6CABDB4C883F283EB19D
6E33C56DB5B8F98EB3DF1A5BEF0E3E345D0AEA8B56A1CC6A0E82AB2A1D669E93
683E9C94AA639E076E52D093E2D48AF34BF0E33A11C782CA5CCBB76B6326EB22
85C08D0926C0B6DC111C32E0276144C6D28651392A44E5048DE050F2A8A73649
96BA907F0622EA516EC1546185FB26DC2F16E5CC1A8C30A7724FD3C87B9F0E73
B369425B3B8074699A26B533E7D990268B7199C9D857F0EC212D7830CBB522AE
A9AFC71F425DC03B6F96E151B40FEC30D24D38ADD00847C768526979A986446A
12016C031343DCB4005EE8D2DB056C073EE3026AEBBEF53FAD510E0E964787D5
7BF0F4EAC67714F0ABD869BF3E0BAD85CCF38336B50CEF2BE7F81B1C28350381
C88551706D66F8B619AA829AE8D5FC7F74B47C5A59D5E9A86B95806B32C9B867
06EB71D59ACD72AE56E34237C8832440A6640292AEDB38053D2734E9554C90E8
6C1D6269A31CC1FDC73FC10E968C31FE859F6F0392D746907AFC1E5842791755
FDB639BEEE38033382116E2A604DB043D6AA9EA25078B1BE2AF810E2D30B640E
37120D3C8D028C8A6B8E1001DD56D65B4CC710A0AC057352BF7A41395E6F1E97
1503AE04F011418312AC77F882EAA1906562E9175F0EC9DAC2734E1840EEBF98
D94C14D9B925884028D6A35CAF568A88D1B79C844446BC2D8CF52A2F3A54650D
2921E9827C79D5D9127521D5531852E47C58B88AB8533324C98332D21D445D6E
C0BA9084E89CA66DF2388DA38F16DB5A98B020C2CBBCD261469C6F50940CB552
4036A9F4DE8A68035152D14D70A65DD3DE6658DAEE6368A3597CAB86366578C2
6A7C8627F810303C25D3C16D2E8309852684901989E20F218488BD498C725A39
3E67142F84168582B3E9155F124C9810A4683EDAD9091DC0AA2A2E14AA62958E
C77C983E6418D302C35A043097E1B4508AB761F5C776AFCBD880AA865A3B7C82
BE7DAC073EAB078FF9FF409D1DB7F4CF593E5EDDE453F674F935579BF89F6F96
87315F76A1277DBD3CFC3A2CABAD2580B1FDCB5AECE860D972AFD753D069588A
6D675A449B0B58010BF82508433AADC7C58E2F44AA838DF457960218EE606C89
0D4381A3E1FCC0F64C6710FA00FE457B075CABB4E8B0CF0971E889FF22536399
125D064012EB8107FDFF0F1217899678240100
}
; Note that there are two menus in the following
; block. The "file" menu is indented and spread
; across several lines. The edit menu is all on
; one line. Notice that you can place action blocks
; after each menu item, to be performed whenever the
; menu item is selected - as with the [print "You
; chose Item 1"] block below:
menu-data: [
file: item "File"
menu [
new: item "Item 1" [print "You chose Item 1"]
open: item "Item 2" ; icons [1.png 2.png]
---
recent: item "Look In Here..."
menu [
item "WIN A PRIZE!"
item "Try door number two"
]
---
exit: item <Ctrl-Q> "Exit"
]
edit: item "Edit" menu [item "copy" item "paste"]
]
; Most of the style definition below is totally optional.
; It's designed to look like a native Microsoft menu. The
; example at
; http://www.rebol.org/library/scripts/menu-system-demo.r
; contains many more examples of menu styles and options.
; The only part that's required in the example below is
; the action block in the "item style" section. Everything
; else serves only to adjust the cosmetic appearance of the
; menu:
winxp-menu: layout-menu/style copy menu-data xp-style: [
menu style edge [size: 1x1 color: 178.180.191 effect: none]
color white
spacing 2x2
effect none
item style
font [name: "Tahoma" size: 11 colors: reduce [
black black silver silver]]
colors [none 187.183.199]
effects none
edge [size: 1x1 colors: reduce [none 178.180.191]
effects: []]
action [
; Change the lines below to fit your needs.
; You can use the action block of each item
; in the switch structure to run your own
; functions. "item/body/text" refers to the
; selected menu item. This does the exact same
; thing as including a code block for each item
; in the menu definition above (i.e., you can
; put the [quit] block after the "exit" item
; above, and it will perform the same way -
; just like the "[print "You chose Item 1"]"
; block after the "new" item above).
switch/default item/body/text [
"exit" [quit]
"WIN A PRIZE!" [alert "You win!"]
"Try door number two" [alert "Bad choice :("]
] [print item/body/text] ; default thing to do
]
]
; The following function traps the GUI close event. This
; must be included whenever the menu module is used, or a
; portion of the application will continue to run after being
; shut down.
evt-close: func [face event] [
either event/type = 'close [quit] [event]
]
insert-event-func :evt-close
; And finally, here's the user interface:
window: layout [
size 400x500
; The line below shows the winxp style menu:
at 2x2 app-menu: menu-bar menu menu-data menu-style xp-style
; THE LINE BELOW SHOWS THE SAME MENU, WHENEVER THE BUTTON
; IS CLICKED:
at 150x200 btn "Menu Button" [
show-menu/offset window winxp-menu
0x1 * face/size + face/offset - 1x0
]
]
view center-face window
The popular REBOL GUI tool called RebGUI (covered in a later section of this tutorial) also has a simple facility for creating basic menus, which can be useful.
16.21 Creating Multi Column GUI Text Lists (Data Grids) From Scratch
REBOL's built-in "text-list" GUI widget is very simple to use, but it can only display one column of data:
view layout [text-list data (system/locale/months)]
REBOL does have a built-in "list" widget for multiple column "data grid" displays, but it's a bit more complex to use than the text-list widget. Earlier in this text, Henrik Mikael Kristensen's listview module was introduced as a simple solution for creating multiple column data grid displays. It works well, but requires you to include a third party module. With a little knowledge and practice, you'll find that REBOL's built-in list widget can be very powerful and easy to use. In it's simplest form, the native list widget takes a size parameter, and 2 additional block parameters:
list (size) [GUI widget layout block] data [block(s) of data to display]
The "(size)" parameter is an XxY pair indicating the pixel size of the overall list widget. The "[GUI widget layout block]" is a layout of standard VID widgets used to display each row of data in the grid. The GUI elements in this block are replicated to display each consecutive row of data in the grid. The GUI layout block typically contains the word "across" (because these widgets are used to display rows of data), and it typically includes size parameters for each widget. The "data" block is made up of rows of information to be displayed in the grid. Each row of data is contained in a separate interior block:
view layout [
list 220x100 [across text 100 text 100] data [
["row 1, column 1" "row 1, column 2"]
["row 2, column 1" "row 2, column 2"]
["row 3, column 1" "row 3, column 2"]
["row 4, column 1" "row 4, column 2"]
]
]
The GUI block can contain other standard facet modifiers such as colors and spacing:
view layout [
list 200x100 [across space 0 text red 100 text blue 100] data [
["row 1, column 1" "row 1, column 2"]
["row 2, column 1" "row 2, column 2"]
["row 3, column 1" "row 3, column 2"]
["row 4, column 1" "row 4, column 2"]
]
]
The GUI block does not need to be comprised of only text fields. You can display the rows of data on widgets of any type:
view layout [
list 304x100 [across space 0 button 150 button 150] data [
["row 1, column 1" "row 1, column 2"]
["row 2, column 1" "row 2, column 2"]
["row 3, column 1" "row 3, column 2"]
["row 4, column 1" "row 4, column 2"]
]
]
IMPORTANT: You can make widgets in the list perform actions, just like in any other "view layout" code:
view layout [
list 304x100 [
across space 0
button 150 [alert face/text] ; When clicked, alert the text
button 150 [alert face/text] ; contained on the button's face.
] data [
["row 1, column 1" "row 1, column 2"]
["row 2, column 1" "row 2, column 2"]
["row 3, column 1" "row 3, column 2"]
["row 4, column 1" "row 4, column 2"]
]
]
This means that creating user editable cells is very simple - just reassign the text of the clicked face, then update the display:
view layout [
list 304x92 [
across space 0
btn 150 [face/text: request-text/default face/text show face]
btn 150 [face/text: request-text/default face/text show face]
] data [
["row 1, column 1" "row 1, column 2"]
["row 2, column 1" "row 2, column 2"]
["row 3, column 1" "row 3, column 2"]
["row 4, column 1" "row 4, column 2"]
]
]
Unintentional visual artifacts can be caused by the caret (cursor) in text widgets. To eliminate them, simply focus and unfocus the widget after updating the display:
view gui: layout [
list 304x84 [
across space 0
text 150 [
face/text: request-text/default face/text
show gui focus face unfocus face
]
text 150 [
face/text: request-text/default face/text
show gui focus face unfocus face
]
] data [
["row 1, column 1" "row 1, column 2"]
["row 2, column 1" "row 2, column 2"]
["row 3, column 1" "row 3, column 2"]
["row 4, column 1" "row 4, column 2"]
]
]
Notice that the number of rows contained in the data block does not affect the number of rows displayed. The list always shows as many rows as will fit in the overall pixel size of the widget (we'll attend to this issue later...):
view layout [
list 304x100 [across space 0 button 150 button 150] data [
["row 1, column 1" "row 1, column 2"]
]
]
view layout [
list 304x100 [across space 0 button 150 button 150] data [
["row 1, column 1" "row 1, column 2"]
["row 2, column 1" "row 2, column 2"]
["row 3, column 1" "row 3, column 2"]
["row 4, column 1" "row 4, column 2"]
["row 5, column 1" "row 5, column 2"]
["row 6, column 1" "row 6, column 2"]
["row 7, column 1" "row 7, column 2"]
]
]
You can resize lists to stretch and fit resizable GUI windows:
insert-event-func [
either event/type = 'resize [
li/size: gui/size - 40x40
t1/size: t2/size: as-pair (round (li/size/1 / 2)) 19
show li unview view gui
none
][event]
]
view/options gui: layout [
li: list 220x110 [across t1: text 100 t2: text 100] data [
["row 1, column 1" "row 1, column 2"]
["row 2, column 1" "row 2, column 2"]
["row 3, column 1" "row 3, column 2"]
["row 4, column 1" "row 4, column 2"]
]
] [resize]
Here's one way to stretch and/or shrink all the cells to fit inside a resizable list:
gui-size: 220x110 li-size: 100x19
gui-block: [
li: list li-size [
across
text first (li-size / 2) ; (1/2 the width of the list widget)
text first (li-size / 2)
] data [
["row 1, column 1" "row 1, column 2"]
["row 2, column 1" "row 2, column 2"]
["row 3, column 1" "row 3, column 2"]
["row 4, column 1" "row 4, column 2"]
]
]
insert-event-func [
either event/type = 'resize [
li-size: gui/size - 40x40
unview
view/options gui: layout gui-block [resize]
none
][event]
]
view/options gui: layout gui-block [resize]
Of course, you can assign a label to any properly formatted data block, and display it later in a list widget:
x: [
["row 1, column 1" "row 1, column 2"]
["row 2, column 1" "row 2, column 2"]
["row 3, column 1" "row 3, column 2"]
["row 4, column 1" "row 4, column 2"]
]
view layout [list 220x100 [across text 100 text 100] data x]
That allows you to build and display multi-column lists very easily:
x: copy []
for i 1 12 1 [
some-info: copy []
append some-info (pick system/locale/months i)
append some-info (pick system/locale/days i)
append/only x some-info
]
view layout [list 220x240 [across text 100 text 100] data x]
Here's a resizable version of the script above, which has user editing enabled for the first column only:
x: copy []
for i 1 12 1 [
some-info: copy []
append some-info (pick system/locale/months i)
append some-info (pick system/locale/days i)
append/only x some-info
]
gui-size: 220x110 li-size: 100x19
gui-block: [
li: list li-size [
across
text first (li-size / 2) [
face/text: request-text/default face/text ; enable user edit
show face focus face unfocus face
]
text first (li-size / 2)
] data x
]
insert-event-func [
either event/type = 'resize [
li-size: gui/size - 40x40
unview
view/options gui: layout gui-block [resize]
none
][event]
]
view/options gui: layout gui-block [resize]
You can collect the entire block of user-edited data using the following code:
editor second second get in (list-widget-label) 'subfunc
For example:
view layout [
the-list: list 304x100 [
across space 0
info 150 [face/text: request-text/default face/text show face]
info 150 [face/text: request-text/default face/text show face]
] data [
["row 1, column 1" "row 1, column 2"]
["row 2, column 1" "row 2, column 2"]
["row 3, column 1" "row 3, column 2"]
["row 4, column 1" "row 4, column 2"]
]
btn "Display Current Data" [
editor second second get in the-list 'subfunc
]
]
This can be used to save and load all data in the list to files, or otherwise put to use. That makes the widget very useful for data management of all types! Take a look at this script to see one way to save and load data:
x: copy [
["row 1, column 1" "row 1, column 2"]
["row 2, column 1" "row 2, column 2"]
["row 3, column 1" "row 3, column 2"]
["row 4, column 1" "row 4, column 2"]
]
do qq: [view layout [
the-list: list 304x100 [
across space 0
info 150 [face/text: request-text/default face/text show face]
info 150 [face/text: request-text/default face/text show face]
] data x
across
btn "Save" [
save to-file request-file/save
second second get in the-list 'subfunc
show the-list
]
btn "Load" [
x: copy load to-file request-file
unview do qq
]
]]
16.21.1 The "Supply" Function
To enable more versatile list displays, the "data" block can be replaced with a "supply" function. "Supply" works much like a "for" loop that iterates through each row of widgets in the displayed GUI list. The "supply" function automatically creates 2 new variables which are automatically incremented each time through the rows in the list:
- "count": the current ROW in the list
- "index": the current COLUMN in the current row
You can use the "count" and "index" variables to select sequential values from a block of data, using the "pick" function (in the same way as in a for loop). Typically, this is used to set the "/text" property of each widget in every row.
In the following example, every row in the list contains a single text widget. The supply function runs through each row, and sets the text property of the widget's face to be one item from the "x" block (a list of months). The loop automatically increments the "count" variable to display each of the months:
x: copy system/locale/months
view layout [
list 200x300 [text 200] supply [
face/text: pick x count
]
]
This example loops through a list of files read from the current directory:
x: read %.
view layout [
list 200x400 [text 200] supply [face/text: pick x count]
]
You can use the "count" variable to change properties of each widget face. In this example, the color property of alternate rows is changed (one color is assigned to even counted rows, another to odd rows):
x: read %.
view layout [
list 200x400 [text 200] supply [
either even? count [face/color: white][face/color: tan]
face/text: pick x count
]
]
You can apply actions to any widget in a list, just as you can with any other widget. Clicking on any file name in the list below will open that file in the editor:
x: read %.
view layout [
list 200x400 [
text 200 [editor to-file face/text]
] supply [
face/text: pick x count
]
]
You can use the "count" variable in the supply function to build multi-column lists from 2 or more separate data blocks (multi column grids are the whole point of learning to use the list widget):
x: copy system/locale/months
y: copy system/locale/days
view layout [
list 250x400 [across t1: text 50 t2: text 100 t3: text 100] supply [
t1/text: count
t2/text: pick x count
t3/text: pick y count
]
]
The next example uses both the "count" and "index" variables to loop through a block with 2 columns of data. Understanding this format is the basis for all the most complex list layouts you'll need. Take special notice of the first line in the supply block. Once all the data from the "x" block has been looped through, if there are more rows in the list display, the index value will go past the length of the data block, and cause an error. To avoid this, you simply check if the picked value is "none", and apply a value of none to the face/text, then exit the loop:
x: copy []
for i 1 12 1 [
append/only x reduce [
pick system/locale/months i
pick system/locale/days i
]
]
view layout [
list 400x400 [across text 200 text 200] supply [
if none? q: pick x count [face/text: none exit]
face/text: pick q index
]
]
To help clarify the above format, here's the same example with a third row added:
x: copy []
for i 1 12 1 [
append/only x reduce [
i
pick system/locale/months i
pick system/locale/days i
]
]
view layout [
list 250x300 [
across
text 50
text 100
text 100
] supply [
if none? q: pick x count [face/text: none exit]
face/text: pick q index
]
]
Here's an example of the directory reading example from earlier, but with two columns of data displayed (file name and size). Clicked file names still bring up the editor:
y: read %.
x: copy []
foreach i y [append/only x reduce [i (size? to-file i)]]
view layout [
list 300x400 [
across
text 200 [editor to-file face/text]
text 100
] supply [
if none? q: pick x count [face/text: none exit]
face/text: pick q index
]
]
The following example demonstrates how to add a slider to scroll through items in a large data block:
x: copy [] for i 1 397 1 [append x i]
slider-pos: 0
view layout [
across
the-list: list 240x400 [text 200] supply [
count: count + slider-pos
face/text: pick x count
]
slider 16x400 [
slider-pos: (length? x) * value
show the-list
]
]
Here's the above slider technique applied to the earlier directory reading example:
x: read %.
slider-pos: 0
view layout [
across
the-list: list 300x400 [
text 200 [editor to-file face/text]
] supply [
count: count + slider-pos
face/text: pick x count
]
slider 16x400 [
slider-pos: (length? x) * value
show the-list
]
]
Here's the 2 column version of the directory reading script, with a slider attached. Be aware that clicking on any file name still reads and edits that file:
y: read %.
x: copy []
foreach i y [append/only x reduce [i (size? to-file i)]]
slider-pos: 0
view layout [
across
the-list: list 300x400 [
across
text 200 [editor to-file face/text]
text 100
] supply [
count: count + slider-pos
if none? q: pick x count [face/text: none exit]
face/text: pick q index
]
slider 16x400 [
slider-pos: (length? x) * value
show the-list
]
]
Here's another refinement of the above script, with a third column added. The look of this display is changed by adding a line between each row (the line is drawn using a box widget), and by changing the color and font of the text:
y: read %.
c: 0
x: copy []
foreach i y [append/only x reduce [(c: c + 1) i (size? to-file i)]]
slider-pos: 0
view layout [
across space 0
the-list: list 400x400 [
across 0 space 0x0
text 50 purple
text 250 bold [editor read to-file face/text]
text 100 red italic
return box green 400x1
] supply [
count: count + slider-pos
if none? q: pick x count [face/text: none exit]
face/text: pick q index
]
slider 16x400 [
slider-pos: (length? x) * value
show the-list
]
]
This example by Carl Sassenrath demonstrates a basic 4 column display:
REBOL []
db: [
[ "000" "Ian Fleming" "ian" 31-Dec-2003 ]
[ "007" "James Bond" "jb" 1-Jan-2004 ]
[ "001" "M" "m" 2-Jan-2004 ]
[ "ABC" "Miss Moneypenny" "missm" 3-Jan-2004]
[ "008" "Pierce Brosnan" "pb" 4-Jan-2004 ]
[ "009" "George Lazenby" "gl" 5-Jan-2004 ]
[ "010" "Roger Moore" "rm" 6-Jan-2004 ]
]
sld-cnt: 0
view lst1: layout [across space 0x0
style text text [alert form face/user-data]
list 406x100 [
across space 0x0 text 36 text 100 text 120 text 150
] supply [
face/text: none face/user-data: none
count: count + sld-cnt
record: pick db count
if not record [exit]
n: pick [1 2 3 4] index
face/text: pick record n
face/user-data: record
]
scl1: scroller 16x100 [
value: to-integer value * length? db
if value <> sld-cnt [sld-cnt: value show lst1]
]
]
The following example demonstrates how to enable users to add and remove data from a list display. Notice that after adjusting the content of your original data block and then "show"ing the list, the displayed grid is automatically updated with the new data:
x: copy []
for i 1 10 1 [
append/only x reduce [form random 1000 form random 1000]
]
slider-pos: 0
view layout [
across
the-list: list 220x240 [across text 100 text 100] supply [
count: count + slider-pos
if none? q: pick x count [face/text: none exit]
face/text: pick q index
]
slider 16x240 [
slider-pos: (length? x) * value
show the-list
]
return
btn "Remove" [remove head x show the-list
]
btn "Add" [
insert/only head x reduce [form random 1000 form random 1000]
show the-list
]
]
To save user-edited contents of a GUI list created with the "supply" function, you need to use the following "set-it" code when iterating through the supply function with "count" and "index" (the "second second get in (list-widget-label) 'subfunc" trick only works for lists created using the "data" function):
x: copy [
["row 1, column 1" "row 1, column 2"]
["row 2, column 1" "row 2, column 2"]
["row 3, column 1" "row 3, column 2"]
["row 4, column 1" "row 4, column 2"]
]
do qq: [view gui: layout [
the-list: list 304x100 [
across space 0
info 150 [face/text: request-text/default face/text show gui]
info 150 [face/text: request-text/default face/text show gui]
] supply [
either count > length? x [face/text: "" face/image: none] [
the-list/set-it face x index count
]
]
across
btn "Save" [
save to-file request-file/save x
]
btn "Load" [
x: copy load to-file request-file
unview do qq
]
]]
In this example, columns can be sorted by clicking the headers. Individual values at any column/row position can be edited by the user (just click the current value). Entire rows can be added, removed, or moved to/from user-selected positions. The data block can be saved or loaded to/from file(s). Scrolling can be done with the mouse, arrow keys, or page-up/page-down keys. Several resizing concepts are also demonstrated:
REBOL [title: "List Widget Example"]
x: copy [] random/seed now/time ; generate 5000 rows of random data:
repeat i 5000 [
append/only x reduce [random "asdfqwertyiop" form random 1000 form i]
] y: copy x
Alert help-txt: {Be sure to try the following features: 1) Resize the GUI
window to see the list automatically adjust to fit 2) Click column
headers to sort by field 3) Use the arrow keys and page-up/page-down
keys to scroll 4) Use the Insert, Delete and "M" keys to add, remove
and move rows (by default, at the currently highlighted row) 5) Click
the small "r" header button in the top right corner to reset the list
back to its original values 6) Click any individual data cell to edit
the selected value.}
sort-column: func [field] [
either sort-order: not sort-order [
sort/compare x func [a b] [(at a field) > (at b field)]
] [
sort/compare x func [a b] [(at a field) < (at b field)]
]
show li
]
key-scroll: func [scroll-amount] [
s-pos: s-pos + scroll-amount
if s-pos > (length? x) [s-pos: length? x]
if s-pos < 0 [s-pos: 0]
sl/data: s-pos / (length? x)
show li show sl
]
resize-grid: func [percentage] [
gui-size: system/view/screen-face/pane/1/size ; - 10x0
list-size/1: list-size/1 * percentage
list-size/2: gui-size/2 - 95
t-size: round (list-size/1 / 3)
sl-size: as-pair 16 list-size/2
unview/only gui view/options center-face layout gui-block [resize]
]
resize-fit: does [
gui-size: system/view/screen-face/pane/1/size
resize-grid (gui-size/1 / list-size/1 - .1)
]
insert-event-func [either event/type = 'resize [resize-fit none] [event]]
gui-size: system/view/screen-face/size - 0x50
list-size: gui-size - 60x95
sl-size: as-pair 16 list-size/2
t-size: round (list-size/1 / 3)
s-pos: 0 sort-order: true ovr-cnt: none svv/vid-face/color: white
view/options center-face gui: layout gui-block: [
size gui-size across
btn "Smaller" [resize-grid .75]
btn "Bigger" [resize-grid 1.3333]
btn "Fit" [resize-fit]
btn #"^~" "Remove" [attempt [
indx: to-integer request-text/title/default "Row to remove:"
form to-integer ovr-cnt
if indx = 0 [return]
if true <> request rejoin ["Remove: " pick x indx "?"] [return]
remove (at x indx) show li
]]
insert-btn: btn "Add" [attempt [
indx: to-integer request-text/title/default "Add values at row #:"
form to-integer ovr-cnt
if indx = 0 [return]
new-values: reduce [
request-text request-text (form ((length? x) + 1))
]
insert/only (at x indx) new-values show li
]]
btn #"m" "Move" [
old-indx: to-integer request-text/title/default "Move from row #:"
form to-integer ovr-cnt
new-indx: to-integer request-text/title "Move to row #:"
if ((new-indx = 0) or (old-indx = 0)) [return]
if true <> request rejoin ["Move: " pick x old-indx "?"] [return]
move/to (at x old-indx) new-indx show li
]
btn "Save" [save to-file request-file/save x]
btn "Load" [y: copy x: copy load request-file/only show li]
btn "Read Me" [alert help-txt]
btn "View Data" [editor x]
return space 0x0
style header button as-pair t-size 20 black white bold
header "Random Text" [sort-column 1]
header "Random Number" [sort-column 2]
header "Unique Key" [sort-column 3]
button black "r" 17x20 [if true = request "Reset?"[x: copy y show li]]
return
li: list list-size [
style cell text t-size feel [
over: func [f o] [
if (o and (ovr-cnt <> f/data)) [ovr-cnt: f/data show li]
]
engage: func [f a e] [
if a = 'up [
f/text: request-text/default f/text show li
]
]
]
across space 0x0
col1: cell blue
col2: cell
col3: cell red
] supply [
either even? count [face/color: white] [face/color: 240.240.255]
count: count + s-pos
if none? q: pick x count [face/text: copy "" exit]
if ovr-cnt = count [face/color: 200.200.255]
face/data: count
face/text: pick q index
]
sl: scroller sl-size [s-pos: (length? x) * value show li]
key keycode [up] [key-scroll -1]
key keycode [down] [key-scroll 1]
key keycode [page-up] [key-scroll -20]
key keycode [page-down] [key-scroll 20]
key keycode [insert] [do-face insert-btn 1]
] [resize]
Be sure to see http://www.rebol.org/view-script.r?script=list-supply-how-to.r, http://www.rebol.org/view-script.r?script=vid-usage.r, http://www.rebol.org/view-script.r?script=list-scroll-demo.r, and http://www.pat665.free.fr/gtk/rebol-view.html#sect19. for more about lists.
16.21.2 Creating Home Made Multi Column Data Grids
As it turns out, it can actually be easier and more versatile to roll your own data grids using native VID components, than it is to use the "list" widget. The following examples are based on the concept at http://www.rebol.org/view-script.r?script=presenting-text-in-columns.r. In every example, a forskip loop is used to build a visual grid of GUI widgets. The loop inserts individual text items from a data block onto each widget's face. For large lists, these example run slowly, but they can be useful for creating reasonably small displays.
The first example creates a random block of two columns of data, labeled "x". Then, a forskip loop is used to assemble a layout block of field widgets, with each row of fields containing 2 consecutive text items from the data block. That GUI block is then displayed on a pane inside a box widget, which is itself displayed inside the layout of the main window. A scroller widget is added to scroll the visible portion of the grid layout. This is accomplished by adjusting the offset of the pane which contains the whole layout of field widgets. IMPORTANT: notice that each cell in this grid is user editable (simply because each cell is displayed using a standard VID field widget). Also notice that the data is converted to a string with the "form" function, because fields can only display text.
x: copy [] for i 1 179 1 [append x reduce [i random "abcd"]]
grid: copy [across space 0] ; the GUI block containing the grid of fields
forskip x 2 [append grid compose [field (form x/1)field (form x/2)return]]
view center-face layout [across
g: box 400x200 with [pane: layout/tight grid pane/offset: 0x0]
scroller [g/pane/offset/y: g/size/y - g/pane/size/y * value show g]
]
The next example demonstrates how to take two columns of data (blocks) and combine them into a single block that can be displayed using the layout above. First, the size of the longest block is determined using the "max" function, and a for loop is run to add consecutive items from each of the source blocks, in groups of 2, to the destination block. If either column runs out of data, blank strings are added to the rest of the destination block as column place holders.
x: copy []
block1: copy system/locale/months block2: copy system/locale/days
for i 1 (max length? block1 length? block2) 1 [
append x either g: pick block1 i [g] [""]
append x either g: pick block2 i [g] [""]
]
grid: copy [across space 0]
forskip x 2 [append grid compose [field (form x/1)field (form x/2)return]]
view center-face layout [across
g: box 400x200 with [pane: layout/tight grid pane/offset: 0x0]
scroller [g/pane/offset/y: g/size/y - g/pane/size/y * value show g]
]
The next example demonstrates how to change the look of the grid layout, and how to obtain a block containing all the data displayed in the grid, including user edits. To clarify visual separation of row data, an alternating color is assigned to each row in the grid. This is handled using a "remainder" function to check for even numbered rows. For every 4 pieces of text in the data block (every 2 displayed rows), the color is set to white. Otherwise, it's set to wheat. The most important part of this example is the line which collects all the data contained in each face of the displayed grid, and builds a block ("q") to store it.
x: copy [] for i 1 179 1 [append x reduce [i random "abcd"]]
grid: copy [origin 0x0 across space 0x0]
forskip x 2 [
color: either (remainder ((index? x) - 1) 4) = 0 [white][wheat]
append grid compose [
field 180 (form x/1) (color) edge none
field 180 (form x/2) (color) edge none return
]
]
view center-face layout [
across space 0
g: box 360x200 with [pane: layout grid pane/offset: 0x0]
scroller[g/pane/offset/y: g/size/y - g/pane/size/y * value / 2 show g]
return box 1x10 return ; just a spacer
btn "Get Data Block (INCLUDING USER EDITS)" [
q: copy [] foreach face g/pane/pane [append q face/text] editor q
]
]
The next example demonstrates a number of features that really make the grid malleable and useful for entering, editing, and storing columns of data. First, the look is adjusted by changing the edges of each field style. To enable all the new features, an "update" function is created to run the line of code from the previous example which creates the "q" block of data from text displayed in every cell of the grid. In every case, the data is collected and stored in the variable "q", and the desired operation is performed on that block (adding and removing rows or data, extracting vertical columns of data, saving and loading the data to/from files on the hard drive, etc.). After the data block has been changed by an operation, the entire layout is unviewed and rebuilt using the new data (i.e., the "q" data is reassigned to the initial "x" block). The code is rerun by labeling the entire script "qq" and using the "do" function to re-evaluate it. The final button demonstrates how to collect and display a history of user edits.
x: copy [] for i 1 179 1 [append x reduce [i random "abcd"]]
update: does [q: copy [] foreach face g/pane/pane [append q face/text]]
do qq: [grid: copy [across space 0]
forskip x 2 [append grid compose [
field (form x/1) 40 edge none
field (form x/2) 260 edge [size: 1x1] return
]]
view center-face gui: layout [across space 0
g: box 300x290 with [pane: layout/tight grid pane/offset: 0x0]
slider 16x290 [
g/pane/offset/y: g/size/y - g/pane/size/y * value show g
]
return btn "Add" [
row: (to-integer request-text/title "Insert at Row #:") * 2 - 1
update insert at q row ["" ""] x: copy q unview do qq
]
btn "Remove" [
row: (to-integer request-text/title "Row # to delete:") * 2 - 1
update remove/part (at q row) 2 x: copy q unview do qq
]
btn "Col 1" [update editor extract q 2]
btn "Col 2" [update editor extract/index q 2 2]
btn "Save" [update save to-file request-file/save q]
btn "Load" [x: load to-file request-file do qq]
btn "History" [
m: copy "ITEMS YOU'VE EDITED:^/^/" update for i 1 (length? q) 1 [
if (to-string pick x i) <> (to-string pick q i) [
append m rejoin [pick x i " " pick q i newline]
]
] editor m
]
]]
This final example clarifies how to add additional columns, how to use GUI widgets other than fields to display the data (text widgets, in this case), how to make the widgets perform any variety of actions, and how to get data from the grid when not every widget has text on its face. It also demonstrates some additional changes to the look of the grid.
x: copy [] for i 1 99 1 [append x reduce [i random 99x99 random "abcd"]]
grid: copy [origin 0x0 across space 0x0]
forskip x 3 [
append grid compose [
b: box 520x26 either (remainder((index? x)- 1)6)= 0 [white][beige]
origin b/offset
text bold 180 (form x/1)
text 120 center blue (form x/2) [alert face/text]
text 180 right purple (form x/3) [face/text: request-text] return
box 520x1 green return
]
]
view center-face layout [
across space 0
g: box 520x290 with [pane: layout grid pane/offset: 0x0]
scroller 16x290 [
g/pane/offset/y: g/size/y - g/pane/size/y * value / 2 show g
]
return box 1x10 return ; just a spacer
btn "Get Data Block" [
q: copy []
foreach face g/pane/pane [
if face/style = 'text [append q face/text]
]
editor q
]
]
These examples are useful for lists that contain ~1000 or fewer rows of data. For displays with grids larger than that, one of REBOL's other listview options should be used.
16.22 RebGUI
REBOL's VID dialect ("view layout []"), is one of the language's most attractive features. The ability to create GUI windows on multiple operating systems, with as little as 1 line of code, is practical for creating many sorts of applications. "RebGUI" is a third party GUI toolkit built on REBOL/View which replicates many of the basic components in VID, and upgrades/adds to the concept with many desirable features:
- Modern look and feel.
- Many powerful and useful new widgets and built-in functions: resizable tables (data grids) with automatic column sorting, trees, menus, tab and scroll panels, group boxes, tool-bars, spreadsheet, pie-chart and chat widgets, new requestors, native undo/redo, spellcheck, and translate functions (with many provided language dictionaries) for text widgets, etc.
- Simple and elegant syntax (similar to VID).
- Full documentation and demo code for all widgets.
- Super simple notation to handle automatic alignment and layout of widgets in resized windows.
- Config file to easily manage user settings for global UI sizes, colors, behaviors, and effects of all widgets. A built-in native requestor is also provided to adjust all these settings.
- Automatic handling of window close events.
- User assignable function key actions.
- Easy, automatic handling of multiple user languages.
- Well designed object structure to access every widget, function, and feature (and containing all necessary help information, built in).
- The entire system compresses to just over 30k.
VID is great for building quick scripts, and many of the features found in RebGUI have been created elsewhere as VID add-ons. The menu system and listview widget described earlier in this text, for example, are more powerful than those found in RebGUI. Close events and spell checking can also be handled in other ways described earlier in this text. But for most types of applications, RebGUI provides a single, simple, integrated way to build applications with all the most commonly needed user interface features. It uses a simple, consistent language structure, and provides a clean, modern looking visual design.
RebGUI is available at http://www.dobeash.com/download.html, and several tutorials are available at http://www.dobeash.com/rebgui.html. A mirror of the required files in version 117 is available at http://musiclessonz.com/rebol_tutorial/rebgui.zip. You can also download RebGUI directly within REBOL, using the built in Viewtop. To open the Viewtop, type "desktop" into the REBOL interpreter, then click REBOL -> Demos -> RebGUI. That will download the main "rebgui.r" include file, along with the "RebDoc.r" help program, the "tour.r" demo program, and some supporting graphic images. The downloaded package will automatically run tour.r, which demonstrates many of RebGUI's features. Be sure to click the "RebDOC" button to view all the documentation necessary to use RebGUI.
All you need to use RebGUI is %rebgui.r. Copy it to an accessible folder and include the line "do %rebgui.r" (with its path, if necessary), and then you can use all the built in widgets and functions in RebGUI. A quick and dirty way to do this in Windows is to run the "request-file" function in REBOL, then click Public -> www.Dobeash.com -> RebGUI, right click rebgui.r and paste it into a folder of your choice. You can also use the following script to copy it to any folder:
write to-file request-file/file/title/save %rebgui.r "Save As:" {
} read view-root/public/www.dobeash.com/RebGUI/rebgui.r
If you're going to use RebGUI regularly, it's a good idea to copy it directly into your main REBOL install directory (the default folder is c:\program files\rebol\view).
To build your first RebGUI interface, after running the RebGUI demo, try the following code:
do view-root/public/www.dobeash.com/RebGUI/rebgui.r
display "Test" [button "Click Me" [alert "Clicked"]]
do-events
Notice that "view layout" has been replaced with "display". This function always requires some title text. Notice also that "do-events" must be included after your RebGUI code to activate the GUI.
Once you've included %rebgui.r, you can try any of the built-in widgets and functions:
display "" [area] do-events ; the "area" widget
Notice that the area widget above has built-in undo/redo features using [CTRL]-Z and [CTRL]-Y (REBOL's native "view layout [area]" does not have any undo/redo capability). A built-in spellchecker can also be activated using [CTRL]-S! To use the spellchecker, you need to download a dictionary from http://www.dobeash.com/RebGUI/dictionary, and unzip it into %view-root/public/www.dobeash.com/RebGUI/dictionary/ (or in the /dictionary subdirectory of wherever rebgui.r is located).
Take a look at a few of the other great widgets built into RebGUI:
do %rebgui.r ; be sure to include the path, if necessary
display "Pie Chart" [pie-chart data ["VID" yellow 19 "RebGUI" red 81]]
do-events
display "Spreadsheet" [
sheet options [size 7x7] data [a1 "very " a2 "cool" a3 "=join a1 a2"]
]
do-events
display "Chat" [
chat data ["Nick" blue "I like RebGUI" yellow 20-sep-2009/1:00]]
do-events
display/maximize "Menu" [
menu data [
"File" [
"Open" [request-file]
"Save" [request-file]
]
"About" ["Info" [alert "RebGUI is great!"]]
]
]
do-events
You can run the RebDoc.r program to see the syntax required to use any of the other RebGUI widgets, requestors and functions.
The "/close" refinement of the "display" function lets you set any action(s) you want to run when a GUI window is shut down. This can help avoid data loss from accidental window closure, and provides a way to automatically process data or run other applications when a window is closed:
display/close "" [area] [question "Really Close?"] do-events
Be sure to try the "request-ui" requestor function. It lets you easily adjust the global settings for the overall look and feel of layouts created with RebGUI on your machine. Settings are saved in the file %ui.dat, in the current working directory.
request-ui
RebGUI includes a variety of "span directives" to easily automate resizing of widgets:
These directives automatically set the initial size of a widget:
#L - align the right hand edge of the widget with the adjacent edge
#V - align the base edge of the widget with the adjacent edge
#O - align the left hand edge of the widget with the adjacent edge
("adjacent edge" is the edge of the adjacent widget, or the edge of
the GUI, if there is no adjacent widget.)
These directives automatically adjust the size and position of
a widget when the GUI is resized:
#H - stretch or shrink the widget to fit the window height
#W - stretch or shrink the widget to fit the window width
#X x - move the widget x number of pixels to the right
#Y y - move the widget y number of pixels downward
Here's an example of an area widget that stretches and shrinks to fit a resized GUI window:
display "" [area #HW] do-events
Here's a fully functional, resizable text editor application, with built-in undo/redo, spell checking, and close event handling:
do %rebgui.r
display/maximize/close "Text Editor" [
menu #LHW data [
"File" [
"Open" [x/text: read to-file request-file show x]
"Save" [write to-file request-file/save x/text]
]
] return
x: area #LHW
] [question "Really Close?"] do-events
Now that's a lot of program for just a little code!
This example demonstrates how to use tab panels and a variety of useful techniques:
display "Tab Panel" [
main-screen: tab-panel data [
"Spreadsheet" [
x: sheet data [
A1 32 A2 12 A3 "=a1 + a2" A4 "=1.06 * to decimal! a3"
]
a: area
reverse
button -1 " Show Data " [x/save-data set-text a x/data]
button -1 " Quit! " [quit]
]
"VID style" [
style -1 data [text bold "Back to Spreadsheet" [
main-screen/select-tab 1
]]
]
action [wait .2 face/color: 230.230.230] "Text" [
text "Tabs are a great way to maximize screen real estate."
]
action [wait .2 set-focus z] "Fields" [
y: field
z: field "Edit me"
]
]
]
do-events
To really get to know RebGUI, explore its main object "ctx-rebgui":
? ctx-rebgui
The "ctx-rebgui" object is set up much like REBOL's built-in "system/view/vid" object. You can explore it using path notation. Notice that built-in help is included in the "tip" path of each widget:
? ctx-rebgui/widgets/tree/tip
Here's a quick and dirty way to view help for all the RebGUI widgets:
foreach i (find first ctx-rebgui/widgets 'anim) [
do compose/deep [print rejoin[i" - "(ctx-rebgui/widgets/(i)/tip)"^/"]]
]
Be sure to read the main RebGUI user guide at http://www.dobeash.com/RebGUI/user-guide.html, and the cookbook at http://www.dobeash.com/RebGUI/cookbook.html. Then read through all the info in RebDoc.r, examine the code in tour.r, and get to know your way around ctx-rebgui. You'll likely find that RebGUI is the best choice for GUI layout in many situations.
16.23 RebGUI Apps - Spreadsheet, Rolodex, Member Manager, Editor, POS system
RebGUI contains a spreadsheet widget. Here's a useful spreadsheet application the demonstrates how to use save, load, print and data view features:
do %rebgui.r
display "Spreadsheet" [
x: sheet options [size 3x3 widths [8 8 10]] data [
A1 32 A2 12 A3 "=a1 + a2" A4 "=1.06 * to-integer a3"
]
return
button "Save" [
x/save-data
save to-file request-file x/data
]
button "Load" [
x/load-data load to-file request-file
]
button "View" [
x/save-data
alert form x/data
]
button "Print" [
save/png %sheet.png to image! x
browse %sheet.png ; or call %sheet.png
]
]
do-events
The example below is similar to the "Parts Database" program presented earlier, using RebGUI instead of VID. It should provide a number of practical insights into how to use RebGUI. Notice that the GUI is resizable, the text fields have undo/redo and spellcheck capabilities, requestors are modal, and all the other features of RebGUI are available:
REBOL [title "RebGUI Card File"]
do load-thru http://re-bol.com/rebgui.r
write/append %data.txt ""
database: load %data.txt
display "RebGUI Card File" [
text 20 "Select:"
names: drop-list #LW data (sort extract copy database 4) [
marker: find database pick names/data names/picked
set-text n copy first marker
set-text a copy second marker
set-text p copy third marker
set-text o copy fourth marker
]
after 2
text 20 "Name:" n: field #LW ""
text 20 "Address:" a: field #LW ""
text 20 "Phone:" p: field #LW ""
after 1 text "Notes:" o: area #LW ""
after 3
button -1 "Save" [
if (n/text = "") [alert "You must enter a name." return]
if find (sort extract copy database 4) copy n/text [
either true = question "Overwrite existing record?" [
remove/part (find database n/text) 4
] [return]
]
database: repend database [
copy n/text copy a/text copy p/text copy o/text
]
save %data.txt database
set-data names (sort extract copy database 4)
set-text names copy n/text
]
button -1 "Delete" [
if true = question rejoin ["Delete " copy n/text "?"] [
remove/part (find database n/text) 4
save %data.txt database
set-data names (sort extract copy database 4)
set-values face/parent-face ["" "" "" "" ""]
]
]
button -1 "New" [
set-values face/parent-face ["" "" "" "" ""]
]
]
do-events
Here's a useful RebGUI text/code editor:
REBOL []
unless exists? %ui.dat [
write %ui.dat read http://re-bol.com/ui-editor.dat
]
do load-thru http://re-bol.com/rebgui.r ; Build#117
; do %rebgui.r
filename: %temp.txt
make-dir %./edit_history/
backup: does [
if ((length? x/text) > 0) [
write rejoin [
%./edit_history/
last split-path filename
"_" now/date "_"
replace/all form now/time ":" "-"
] x/text
]
]
ctx-rebgui/on-fkey/f5: does [
backup
write filename x/text
launch filename
]
display/maximize/close "RebGUI Editor" [
tight
menu #LW data [
"File" [
" New " [
if true = question "Erase Current Text?" [
backup
filename: %temp.txt set-text x copy ""
]
]
" Open " [
filetemp: to-file request-file/file filename
if filetemp = %none [return]
backup
set-text x read filename: filetemp
]
" Save " [
backup
write filename x/text
]
" Save As " [
filetemp: to-file request-file/save/file filename
if filetemp = %none [return]
backup
write filename: filetemp x/text
]
" Save and Run " [
backup
write filename x/text
launch filename
]
" Print " [
write %./edit_history/print-file.html rejoin [
{<}{pre}{>} x/text {<}{pre}{>}
]
browse %./edit_history/print-file.html
]
" Quit " [
if true = question "Really Close?" [backup quit]
]
]
"Options" [
" Appearance " [request-ui]
]
"Help" [
" Shortcut Keys " [
alert trim {
F5: Save and Run
Ctrl+Z: Undo
Ctrl+Y: Redo
Esc: Undo All
Ctrl+S: Spellcheck
}
]
]
] return
x: area #LHW
] [
if true = question "Really Close?" [backup quit]
]
do-events
Here's a user management script, inspired by the tutorial at http://snappmx.com:
REBOL [title: "RebGUI User List Demo"]
do load-thru http://re-bol.com/rebgui.r ; Build#117 ; do %rebgui.r
unless exists? %snappmx.txt [
save %snappmx.txt [
"user1" "pass1" "Bill Jones" "%bill--site--com" "Bill LLC"
"user2" "pass2" "John Smith" "%john--mail--com" "John LLC"
]
]
database: load %snappmx.txt
login: request-password
found: false
foreach [userid password name email company] database [
either (login/1 = userid) and (login/2 = password) [found: true] []
]
if found = false [alert "Incorrect Login." quit]
add-record: does [
display/dialog "User Info" [
text 20 "User:" f1: field return
text 20 "Pass:" f2: field return
text 20 "Name:" f3: field return
text 20 "Email:" f4: field return
text 20 "Company:" f5: field reverse
button -1 #XY " Clear " [clear-fields]
button -1 #XY " Add " [add-fields]
]
]
edit-record: does [
display/dialog "User Info" [
text 20 "User:" f1: field (form pick t/selected 1) return
text 20 "Pass:" f2: field (form pick t/selected 2) return
text 20 "Name:" f3: field (form pick t/selected 3) return
text 20 "Email:" f4: field (form pick t/selected 4) return
text 20 "Company:" f5: field (form pick t/selected 5) reverse
button -1 #XY " Delete " [
t/remove-row t/picked
save %snappmx.txt t/data
hide-popup
]
button -1 #XY " Save " [
t/remove-row t/picked
add-fields
save %snappmx.txt t/data
hide-popup
]
]
]
add-fields: does [
t/add-row reduce [
copy f1/text copy f2/text copy f3/text copy f4/text copy f5/text
]
save %snappmx.txt copy t/data
]
clear-fields: does [
foreach item [f1 f2 f3 f4 f5] [do rejoin [{set-text } item {""}]]
]
table-size: system/view/screen-face/size/1 / ctx-rebgui/sizes/cell
display/maximize "Users" [
t: table table-size #LWH options [
"" left .0 "" left .0 ; don't show the first 2 fields
"Name" center .33 "Email" center .34 "Company" center .34
] data database [edit-record]
reverse
button -1 #XY " Add " [add-record]
]
do-events
Here is a point of sale system (sales checkout, receipt printer, and data storage system) written using RebGUI. Note that the username and password info in the posp.db file should be created and read using a separate method, and encrypted. The example posp.db file is created here as a demonstration. Note also that the first field in the layout is designed to accept input from a keyboard wedge bar code scanner, with data in the format: item (space) booth (space) price (inserted [ENTER] key character). Using this format, and the automatic refocus upon entry, the user can continually scan multiple items into the system:
REBOL [title: "POINT OF SALE SYSTEM"]
write %posp.db {["username" "password"] ["username2" "password2"]} ; etc.
make-dir %./receipts/
write/append %./receipts/deleted.txt "" ; create file if not exists
unless exists? %scheme_has_changed [
write %ui.dat decompress #{
789C9552CB92A3300CBCE72BBCDCA18084995A7E652A078115F08EB129592C33
7FBFC24E32CC2387A5EC2A49ED6EB56C267845E5BB3FD8F32FF57250F2CD3060
ABEAA629E23E95B1CAF8C6AD7A3A1571A5D28813E6D60CA32055752AAAE67751
97CF3B5003BDB6EA5817CF821E9B8804067E484BE04F34BFB035EE4EACCB5371
DD9FE044AD8E4FC5751FCE6AFA3E648FD6B62A51516F035731BE78B7B9AAEF49
3EE2D5693A3CC02CCD63B8F5DB8CC464021A8CBB49066B3492901EB4879E8D77
B92C74BC1D7CD1E467992DB0D8319CA28B41ABE53D42583D691566E31C521438
7F9161E844241276780F84BCC117DF2F410E480E7BFCBDB7A697FA407E99F3CE
BF493787568511919588E631DF5146131F602FFA1F8645B1437D35A2BA85D93B
F5317A8C9810BF5DC240E6A1F0CF374CE4D790B31F507E45B9E10BD8801122D0
6633DEEC5E3CFB8BA4C14176AF6D936540066CA6B2DE2F649094C35532361386
EC0B270D18660B61CC355A78BFFD53ECBD6533DF8A655BCA4AD08A9D366E905E
4C4B72B71AA7FDDA2AE71D1ECEFF004BE40F38A0030000
}
]
do http://re-bol.com/rebgui.r
do login: [
userpass: request-password
if (length? userpass) < 2 [quit]
posp-database: to-block read %posp.db
logged-in: false
foreach user posp-database [
if (userpass/1 = user/1) and (userpass/2 = user/2) [
logged-in: true
]
]
either logged-in = true [] [
alert "Incorrect Username/Password"
do login
]
]
calculate-totals: does [
tax: .06
subtotal: 0
foreach [item booth price] pos-table/data [
subtotal: subtotal + to decimal! price
]
set-text subtotal-f subtotal
set-text tax-f (round/to (subtotal * tax) .01)
set-text total-f (round/to (subtotal + (subtotal * tax)) .01)
set-focus barcode
]
add-new-item: does [
if ("" = copy f1/text) or ("" = copy f2/text) or (error? try [
to-decimal copy f3/text
]) [
alert trim/lines {You must enter a proper Item Description,
Booth Number, and Price.}
return
]
pos-table/add-row/position reduce [
copy f1/text copy f2/text copy f3/text
] 1
calculate-totals
]
print-receipt: does [
if empty? pos-table/data [
alert "There's nothing to print." return
]
html: copy rejoin [
{<html><head><title>Receipt</title></head><body>
<table width=40% border=0 cellpadding=0><tr><td>
<h1>Business Name</h1>
123 Road St.<br>
City, State 98765<br>
123-456-7890
</td></tr></table><br><br>
<center><table width=80% border=1 cellpadding=5>
<tr>
<td width=60%><strong>Item</strong></td>
<td width=20%><strong>Booth</strong></td>
<td width=20%><strong>Price</strong></td></tr>}
]
foreach [item booth price] pos-table/data [
append html rejoin [
{<tr><td width=60%>} item
{</td><td width=20%>} booth
{</td><td width=20%>} price {</td></tr>}
]
]
append html rejoin [
{<tr><td width=60%></td><td width=20%><strong>SUBTOTAL:
</strong></td><td width=20%><strong>}
copy subtotal-f/text
{</strong></td></tr>}
]
append html rejoin [
{<tr><td width=60%></td><td width=20%><strong>TAX:
</strong></td><td width=20%><strong>}
copy tax-f/text
{</strong></td></tr>}
]
append html rejoin [
{<tr><td width=60%></td><td width=20%><strong>TOTAL:
</strong></td><td width=20%><strong>}
copy total-f/text
{</strong></td></tr>}
]
append html rejoin [
{</table><br>Date: <strong>} now/date
{</strong>, Time: <strong>} now/time
{</strong>, Salesperson: } userpass/1
{</center></body></html>}
]
write/append to-file saved-receipt: rejoin [
%./receipts/
now/date "_"
replace/all copy form now/time ":" "-"
"+" userpass/1
".html"
] html
browse saved-receipt
]
save-receipt: does [
if empty? pos-table/data [
alert "There's nothing to save." return
]
if allow-save = false [
unless true = resaving: question trim/lines {
This receipt has already been saved. Save again?
} [
if true = question "Print another copy of the receipt?" [
print-receipt
]
return
]
]
if resaving = true [
resave-file-to-delete: copy ""
display/dialog "Delete" compose [
text 150 (trim/lines {
IMPORTANT - DO NOT MAKE A MISTAKE HERE!
Since you've made changes to an existing receipt,
you MUST DELETE the original receipt. The original
receipt will be REPLACED by the new receipt (The
original data will be saved in an audit history file,
but will not appear in any future seaches or totals.)
Please CAREFULLY choose the original receipt to DELETE:
})
return
tl1: text-list 150 data [
"I'm making changes to a NEW receipt that I JUST SAVED"
"I'm making changes to an OLD receipt that I've RELOADED"
] [
resave-file-to-delete: tl1/selected
hide-popup
]
return
button -1 "Cancel" [
resave-file-to-delete: copy ""
hide-popup
]
]
if resave-file-to-delete = "" [
resaving: false
return
]
if resave-file-to-delete = trim/lines {
I'm making changes to a NEW receipt that I JUST SAVED
} [
the-file-to-delete: saved-file
]
if resave-file-to-delete = trim/lines {
I'm making changes to an OLD receipt that I've RELOADED
} [
the-file-to-delete: loaded-receipt
]
if not question to-string the-file-to-delete [return]
write %./receipts/deleted--backup.txt read %./receipts/deleted.txt
write/append %./receipts/deleted.txt rejoin [
newline newline newline
to-string the-file-to-delete
newline newline
read the-file-to-delete
]
delete the-file-to-delete
alert "Original receipt has been deleted, and new receipt saved."
resaving: false
]
if true = question "Print receipt?" [print-receipt]
saved-data: mold copy pos-table/data
write/append to-file saved-file: copy rejoin [
%./receipts/
now/date "_"
replace/all copy form now/time ":" "-"
"+" userpass/1
".txt"
] saved-data
splash compose [
size: 300x100
color: sky
text: (rejoin [{^/ *** SAVED ***^/^/ } saved-file {^/}])
font: ctx-rebgui/widgets/default-font
]
wait 1
unview
allow-save: false
if true = question "Clear and begin new receipt?" [clear-new]
]
load-receipt: does [
if error? try [
loaded-receipt: to-file request-file/file/filter %./receipts/
".txt" "*.txt"
] [
alert "Error selecting file"
return
]
if find form loaded-receipt "deleted" [
alert "Improper file selection"
return
]
if error? try [loaded-receipt-data: load loaded-receipt] [
alert "Error loading data"
return
]
insert clear pos-table/data loaded-receipt-data
pos-table/redraw
calculate-totals
allow-save: false
]
search-receipts: does [
search-word: copy request-value/title "Search word:" "Search"
; if search-word = none [return]
found-files: copy []
foreach file read %./receipts/ [
if find (read join %./receipts/ file) search-word [
if (%.txt = suffix? file) and (file <> %deleted.txt) [
append found-files file
]
]
]
if empty? found-files [alert "None found" return]
found-file: request-list "Pick a file to open" found-files
if found-file = none [return]
insert clear pos-table/data (
load loaded-receipt: copy to-file join %./receipts/ found-file
)
pos-table/redraw
calculate-totals
allow-save: false
]
clear-new: does [
if allow-save = true [
unless (true = question "Erase without saving?") [return]
]
foreach item [barcode f1 f2 f3 subtotal-f tax-f total-f] [
do rejoin [{clear } item {/text show } item]
]
clear head pos-table/data
pos-table/redraw
allow-save: true
]
change-appearance: does [
request-ui
if true = question "Restart now with new scheme?" [
if allow-save = true [
if false = question "Quit without saving?" [return]
]
write %scheme_has_changed ""
launch %pos.r ; EDIT
quit
]
]
title-text: "Point of Sale System"
if system/version/4 = 3 [
user32.dll: load/library %user32.dll
get-tb-focus: make routine! [return: [int]] user32.dll "GetFocus"
set-caption: make routine! [
hwnd [int]
a [string!]
return: [int]
] user32.dll "SetWindowTextA"
show-old: :show
show: func [face] [
show-old [face]
hwnd: get-tb-focus
set-caption hwnd title-text
]
]
allow-save: true
resaving: false
saved-file: ""
loaded-receipt: ""
screen-size: system/view/screen-face/size
cell-width: to-integer (screen-size/1) / (ctx-rebgui/sizes/cell)
cell-height: to-integer (screen-size/2) / (ctx-rebgui/sizes/cell)
table-size: as-pair cell-width (to-integer cell-height / 2.5)
current-margin: ctx-rebgui/sizes/margin
top-left: as-pair negate current-margin negate current-margin
display/maximize/close "POS" [
at top-left #L main-menu: menu data [
"File" [
" Print " [print-receipt]
" Save " [save-receipt]
" Load " [load-receipt]
" Search " [search-receipts]
]
"Options" [
" Appearance " [change-appearance]
]
"About" [
" Info " [
alert trim/lines {
Point of Sale System.
Copyright © 2010 Nick Antonaccio.
All rights reserved.
}
]
]
]
return
barcode: field #LW tip "Bar Code" [
parts: parse/all copy barcode/text " "
set-text f1 parts/1
set-text f2 parts/2
set-text f3 parts/3
clear barcode/text
add-new-item
]
return
f1: field tip "Item"
f2: field tip "Booth"
f3: field tip "Price (do NOT include '$' sign)" [
add-new-item
set-focus add-button
]
add-button: button -1 "Add Item" [
add-new-item
set-focus add-button
]
button -1 #OX "Delete Selected Item" [
remove/part find pos-table/data pos-table/selected 3
pos-table/redraw
calculate-totals
]
return
pos-table: table (table-size) #LWH options [
"Description" center .6
"Booth" center .2
"Price" center .2
] data []
reverse
panel sky #XY data [
after 2
text 20 "Subtotal:" subtotal-f: field
text 20 " Tax:" tax-f: field
text 20 " TOTAL:" total-f: field
]
reverse
button -1 #XY "Lock" [do login]
button -1 #XY "New" [clear-new]
button -1 #XY "SAVE and PRINT" [save-receipt]
do [set-focus barcode]
] [question "Really Close?"]
do-events
16.24 Creating PDF files using pdf-maker.r
PDF is a standard file format used to display and print document content in exactly the same way on different computer platforms. In Windows and other operating systems, the PDF reader by Adobe is often installed by default. Other free PDF readers such as Foxit, Sumatra, and PDF-Xchange allow you to view and print PDF documents. Openoffice can be used to create, convert, and save various document formats (i.e. MS Word and other word processor formats) to PDF, so that they are viewable/printable in the exact same visual layout, on any computer.
Gabriele Santilli has created a REBOL pdf-maker script that generates universally readable and printable PDF files directly from REBOL code. The official documentation is available at http://www.colellachiara.com/soft/Misc/pdf-maker-doc.pdf (the REBOL source used to create that PDF document is available at http://www.colellachiara.com/soft/Misc/pdf-maker-doc.r). Pdf-maker.r is a complete, self contained multi-platform solution for creating PDFs. No other software is required to create PDFs with REBOL.
The basic functionality of pdf-maker.r is very simple. Import pdf-maker.r with the "do" function (or simply include it directly in your code). Next, run the "layout-pdf" function, which takes one block as a parameter, and write its output to a .pdf file using the "write/binary" function. Inside the block passed to the "layout-pdf" function, a variety of formatting functions can be included to layout text, images, and manually generated graphics. Here's a basic example of the format, with one simple text layout function:
do http://www.colellachiara.com/soft/Misc/pdf-maker.r
write/binary %example.pdf layout-pdf [[textbox ["Hello PDF world!"]]]
; To open the created document in your default PDF viewer:
call %example.pdf
Here's a more complex example that creates a multi-page PDF file and demonstrates many of the basic capabilities of pdf-maker.r. Separate page content is contained in separate sub-blocks. All coordinates are written in MILLIMETER format:
REBOL [title: "pdf-maker example"]
do http://www.colellachiara.com/soft/Misc/pdf-maker.r
write/binary %example.pdf layout-pdf compose/deep [
[
page size 215.9 279.4 ; American Letter Size!!!
textbox ["Here is page 1. It just contains this text."]
]
[
textbox 55 55 90 100 [
{Here's page 2. This text box contains a starting
XxY position and an XxY size. Coordinates are in
millimeters and extend from the BOTTOM LEFT of the
page (this box extends from starting point 55x55
and is 90 mm wide, 100 mm tall).
*** NOTE ABOUT PAGE SIZES - IMPORTANT!!! ***
All the following page sizes are the default ISO A4,
or 211×297 mm. That is SLIGHTLY SMALLER than the
standard American Letter page size. If you are
printing on American Letter sized paper, be sure to
manually set your paper size, as is done on the first
page of this example.}
]
]
[
textbox 0 200 200 50 [
center font Helvetica 10.5
{This is page 3. The text inside this box is centered
and formatted using Helvetica font, with a character
size of 10.5 mm.}
]
]
[
apply rotation 20 translation 35 150 [
textbox 4 4 200 20 [
{This is page 4. The textbox is rotated 20 degrees
and translated (moved over) 35x150 mm. Graphics
and images can also be rotated and translated.}
]
]
]
[
textbox 5 200 200 40 [
{Here's page 5. It contains this textbox and several
images. The first image is placed at starting point
50x150 and is 10mm wide by 2.4mm tall. The second
image is 2x bigger and rotated 90 degrees. The last
image is placed at starting point 100x150 and is
streched to 10x its size. Notice that this ENTIRE
layout block has been evaluated with compose/deep to
evaluate the images in the following parentheses.}
]
image 50 150 10 2.4 (system/view/vid/image-stock/logo)
image 50 100 20 4.8 rotated 90
(system/view/vid/image-stock/logo)
image 100 150 100 24 (system/view/vid/image-stock/logo)
]
[
textbox [
{This page contains this textbox and several generated
graphics: a line, a colored and filled box with a
colored edge, and a circle.}
]
line width 3 20 20 100 50 ; starting and ending XxY positions
solid box edge width 0.2 edge 44.235.77 150.0.136 100 67 26 16
circle 75 50 40 ; starting point 75x50, radius 40mm
]
]
call %example.pdf
The compose/deep evaluation is very important when using computed values in PDF layouts. Take a look at the following example that uses computed coordinates and image values:
do http://www.colellachiara.com/soft/Misc/pdf-maker.r
xpos: 50 ypos: 200 offset: 5
size: 5 width: (10 * size) height: (2.4 * size)
page1: compose/deep [[
image
(xpos + offset) (ypos + offset)
(width) (height)
(system/view/vid/image-stock/logo)
]]
write/binary %example.pdf layout-pdf page1
call %example.pdf
Here is a program that I wrote for guitar students. It prints out fretboard note diagrams that can be cut out, wrapped around, and taped directly to guitar fretboards of specific varied sizes. The pdf-maker script is included in compressed, embedded format:
REBOL [title: "Guitar Fretboard Note Overlay Printer"]
chosen-scale: none
view center-face layout [
h1 "Fretboard length:"
text-list "25.5" "27.67" "30" [
chosen-scale: join "scale" value
unview
alert rejoin [
"Now printing "
value
" inch scale fretboard overlay to 'notes.pdf'"
]
]
]
notes: [
[{F}{C}{ }{ }{ }{F}]
[{ }{ }{A}{E}{B}{ }]
[{G}{D}{ }{F}{C}{G}]
[{ }{ }{B}{ }{ }{ }]
[{A}{E}{C}{G}{D}{A}]
[{ }{F}{ }{ }{ }{ }]
[{B}{ }{D}{A}{E}{B}]
[{C}{G}{ }{ }{F}{C}]
[{ }{ }{E}{B}{ }{ }]
[{D}{A}{F}{C}{G}{D}]
[{ }{ }{ }{ }{ }{ }]
[{E}{B}{G}{D}{A}{E}]
]
scale25.5: [
36.35 70.66 103.05 133.62 162.47 189.71 215.41 239.67 262.58 284.19
304.59 323.85 342.03 359.18 375.38 390.66 405.09 418.70 431.56 443.69
455.14 465.95 476.15 485.77
]
scale27.67: [
39.45 76.68 111.82 144.99 176.30 205.85 233.74 260.07 284.92 308.38
330.51 351.41 371.13 389.75 407.32 423.91 439.56 454.34 468.28 481.45
493.87 505.60 516.67 527.11
]
scale30: [
42.77 83.14 121.24 157.20 191.15 223.18 253.43 281.97 308.91 334.34
358.34 381.00 402.38 422.57 441.62 459.60 476.57 492.59 507.71 521.99
535.46 548.17 560.17 571.50
]
x: 40 line-width: 30 text-width: 4 height: 5
make-overlay: does [
page1: copy [
textbox 40 0 4 6 [center font Helvetica 5 "E"]
textbox 45 0 4 6 [center font Helvetica 5 "B"]
textbox 50 0 4 6 [center font Helvetica 5 "G"]
textbox 55 0 4 6 [center font Helvetica 5 "E"]
textbox 60 0 4 6 [center font Helvetica 5 "A"]
textbox 65 0 4 6 [center font Helvetica 5 "E"]
]
output: copy []
for i 1 10 1 [
y: do compose [pick (to-word chosen-scale) i]
notes-at-fret: pick notes i
append page1 compose/deep [
line width 4 (x) (y) (x + line-width) (y)
textbox (x) (y - 10) (text-width) (height + 1) [
center font Helvetica (height) (notes-at-fret/1)
]
textbox (x + 5) (y - 10) (text-width) (height + 1) [
center font Helvetica (height) (notes-at-fret/2)
]
textbox (x + 10) (y - 10) (text-width) (height + 1) [
center font Helvetica (height) (notes-at-fret/3)
]
textbox (x + 15) (y - 10) (text-width) (height + 1) [
center font Helvetica (height) (notes-at-fret/4)
]
textbox (x + 20) (y - 10) (text-width) (height + 1) [
center font Helvetica (height) (notes-at-fret/5)
]
textbox (x + 25) (y - 10) (text-width) (height + 1) [
center font Helvetica (height) (notes-at-fret/6)
]
]
]
append/only output page1
write/binary %notes.pdf layout-pdf output
alert "Done"
]
do to-string load to-binary decompress 64#{
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
n6eu9vNOGX7AVA9A3+GqM+D/NK66uAGAr77UFK0+q6KPfPo3+qO/Qf8gQOO3XU5B
8PO2hyaoTjZ8GaDVqaTeX7tfm1MQ383A25UErpG2AtNw1dpprq9obcWucTdDVaVX
l5DG1Ya5L1U+7mqkLpe8tJv3cF4nsNbq8a+mrEboOk8vJ+WK7ffG+ip8JkoN/nyT
L/JUgj4elr/ca/88rkecLmR5I/OXM2jka7357fbrS6PLs6qHXpeiX1tPtzs9XOdU
V//dO50uCP7+nU7/xeqS//iOZl6tdq61vsS/vqV7Vk02WSV+vlzn/b3NinV94Xx/
WH3b0Jvp/vhS5wuJr8L6MT7Cmy4Bw32/3nmYSaSLwA/ofAyzen25rPZfNnY8GEPg
LP14Y77aFtXE/nJfrf63BF5uw/ygp0cNed/V5V7uc28PVX/d3Usr9O6Dn4+XLlWv
C2Nf5+YdzvtyrvbH/8WTD3XuHxWRD8TjQTTeu6GKrXTfjy+VCnyPyS+3WH25V7gv
evzgitvmIG8m/7Z3pvXZV49RnjVurL21uLu79lZ8MSexfRnbCwsf34/8XHhzDl8+
OZv4J0iX6Xhwdc9T9PDATr837F/TG2k4vLf6zYvYqsb3p/Pf7vG9x6BJujgxASH0
t4T1I/3Ouyhe/Ot8Lwj7tKn4vaT4/VSf9xFebtV6MDJ14vuMw3sXtf58IOsZs9eJ
7+qSuy+vToF+rRc4EQzBrqelHzcwN8NN0CX4//8Dw8SJGQWrAAA=
}
make-overlay
Several articles about pdf-maker are available at rebolfrance.info: first translated by Google, second translated by Google.
16.25 Bar Codes
Bar codes are used in many business applications to speed data entry and to reduce input errors. It would be foolish to build a point of sale system or an inventory management system without using bar code technology.
Reading bar code data into the computer is extremely easy. USB scanners (hardware devices) work just like keyboards. Plug them into the computer, and anything they read from a bar code will be entered as plain text into your application, just as if the text was typed by hand.
Insert a field widget into your GUI, focus it automatically if necessary, and you're in business with bar codes. Add some error checks, save to a file, select from a data block, etc., as needed:
REBOL [title "Bar Code Reader"]
view layout [
text "Plug in your scanner and scan:"
f1: field [
if f1/text <> "" [
write/append %barcodes.txt rejoin [
copy f1/text
newline
]
]
if "recall" = select ["123456" "ok" "234567" "recall"] f1/text [
alert "That item has been recalled!"
]
f1/text: copy ""
focus f1
]
do [focus f1]
btn "View Scanned Bar Codes" [editor %barcodes.txt]
]
Printing bar codes requires some more code. The scripts at http://www.rebol.org/view-script.r?script=code39.r and http://www.rebol.org/view-script.r?script=ean13.r demonstrate how to print several common bar code formats to images.
The following program takes a given string and XxY coordinate (in millimeters), and outputs a PDF file containing a printable bar code at the given position. The bar code algorithm is derived directly from Bohdan Lechnowsky's "code39.r", and the PDF is generated using Gabriele Santilli's "pdf-maker.r". This script was created because images output by the original code39.r script would become blurred when inserted and resized by pdf-maker.r. Here, the bars are rendered as lines, directly in pdf-maker dialect. The images generated are crisp and easily scan-able:
REBOL [title: "PDF Bar Code Generator"]
text-string: "item2342"
x-offset: 10 ; millimeters from the left edge of the page
y-offset: 257 ; millimeters from the bottom edge of the page
create-pdf-barcode: func [barcode-string xshift yshift] [
barcode-width: .3 barcode-height: 12
code39: first to-block decompress #{
789C5D93490EC2400C04EF794514C10504D8EC1CD9F77D07F1FF6F30C9C4E3F6
200529E54EA91D866F92BA4FC699BB989828FF6277EB793BE7EE3EE69D322F03
E15D9F27629BEFA9DFE4FBEA377C103CC520F021F684FC087B0227EC037C2C9E
F209E113F1447C1AF6F503E1B3D2CF517E1EFC36BF087ECB97E221BBEF0A7B42
7E8D3D816FB00FF0AD7A8A89F09D7A0CDFC3BEF940F841FD267F847D317F827D
919FC3BE6C3C17E889F92BF4447E833EC8EFDE43A212FE28F2C4317F4A9EED79
7E95F9F83CBFD56FF21FF51BDE081EFBFB36B127E453EC09BC867D80578447E7
B3051CDF4F5DFB185ED5FF9DE7C9EF0F6518AA1B22040000
}
convfrom: rejoin ["*" barcode-string "*"]
pdf-dialect-out: copy []
x: 0
foreach char convfrom [
pattern: select code39 form char
foreach bit pattern [
x: x + 1
if bit = #"1" [
append pdf-dialect-out compose [
line width (barcode-width)
line
(
(x * barcode-width) + xshift
) (
yshift
)
(
(x * barcode-width) + xshift
) (
yshift + barcode-height
)
]
]
]
x: x + 1
]
return pdf-dialect-out
]
do http://www.colellachiara.com/soft/Misc/pdf-maker.r
barcode-layout: copy []
current-barcode-page: copy [page size 215.9 279.4 offset 0 0]
append current-barcode-page create-pdf-barcode
text-string x-offset y-offset
; The following block is not necessary. It just adds human readable text
; to the printout:
append current-barcode-page compose/deep [
textbox
(x-offset - 9.5) (y-offset - 8)
56 8
[
center font Helvetica 3
(mold text-string)
]
]
append/only barcode-layout current-barcode-page
write/binary %labels.pdf layout-pdf barcode-layout
call %labels.pdf
; This line is unnecessary - it just lets you read the pdf dialect output:
editor barcode-layout
Here's an function from the Merchants' Village software which prints vendor bar codes on pages of Avery Label paper (30 labels per page), with the option to start printing on any chosen label position on the first page inserted into a printer (to allow continued printing on partially used pages). This example is taken out of a larger RebGUI application, and requires the .PDF module. Even though it's taken out of context, the logic is useful for anyone who wants to print pages of bar code labels:
print-bar-codes: does [
bar-code-data-to-print: copy []
bar-code-human-readable-to-print: copy []
either true = question "Print bar codes from vendor order file?" [
if error? try [
barcode-order: request-file/only
barcode-order-data: load barcode-order
temporary-barcode-data: copy second barcode-order-data
] [alert "Not a valid bar code order file."]
] [
temporary-barcode-data: (copy pos-table/data)
]
temporary-barcode-data-build: copy []
foreach [booth item price] temporary-barcode-data [
new-price: to-decimal copy price
if new-price < .01 [
new-price: request-value/type rejoin [
"Enter Price for " booth ", " item ": "
] decimal!
]
append temporary-barcode-data-build reduce [booth item new-price]
]
display/maximize/dialog "Bar Codes" [
barcode-pos-table: table (table-size) #LWOH options [
"Booth" center .25
"Item" center .55
"Price" center .20
] data (copy temporary-barcode-data-build) [
; on-click, change the selected line price (request-text)
; probe barcode-pos-table/picked
new-price: request-value/type rejoin [
"Enter Price for " pick barcode-pos-table/selected 1
", " pick barcode-pos-table/selected 2
", " pick barcode-pos-table/selected 3 ": "
] decimal!
if new-price = none [return]
barcode-pos-table/alter-row barcode-pos-table/picked reduce [
pick barcode-pos-table/selected 1
pick barcode-pos-table/selected 2
pick barcode-pos-table/selected 3
new-price
]
barcode-pos-table/redraw
]
return
button "Print" #XYO [
all-the-barcode-data: copy barcode-pos-table/data
hide-popup
]
]
unless value? 'all-the-barcode-data [return]
if all-the-barcode-data = none [return]
foreach [booth item price] all-the-barcode-data [
bar-code-data: copy ""
bar-code-human-readable: copy ""
temp-booth: copy booth
booth-name-to-print: copy temp-booth
append bar-code-human-readable rejoin [
"booth " temp-booth ", item: "
]
if error? try [booth-index: to-integer copy temp-booth] [
booth-index: 0
]
append bar-code-data booth-index
append bar-code-data select [
1 " " 2 " " 3 ""
] (length? bar-code-data)
temp-item: copy item
append bar-code-human-readable join temp-item ", "
if error? try [item-index: index? find items temp-item] [
item-index: 0
]
append bar-code-data item-index
append bar-code-data select [
4 " " 5 " " 6 " " 7 " " 8 ""
] (length? bar-code-data)
temp-price: to-decimal price
temp-price-nodot: to-integer (price * 100)
append bar-code-human-readable rejoin ["$" temp-price]
append bar-code-data form temp-price-nodot
append bar-code-data-to-print bar-code-data
append bar-code-human-readable-to-print bar-code-human-readable
] ; probe bar-code-data
; Avery Address Label Edges: left - 5 75 145 right - 71 141 211
; tops/bottoms - 13 38.5 64 89.5 115 140 165.5 191 216.5 241.5 267
barcode-label-positions: [
5 267 75 267 145 267 5 241.5 75 241.5 145 241.5
5 216.5 75 216.5 145 216.5 5 191 75 191 145 191
5 165.5 75 165.5 145 165.5 5 140 75 140 145 140
5 115 75 115 145 115 5 89.5 75 89.5 145 89.5
5 64 75 64 145 64 5 38.5 75 38.5 145 38.5
]
current-barcode-pos: request-value/default/type
"Position on page to begin printing: " "1" integer!
if current-barcode-pos = none [return]
if current-barcode-pos = "" [return]
if current-barcode-pos = -1 [return]
current-barcode-pos: (to-integer current-barcode-pos) * 2 - 1
barcode-counter: 1
barcode-list-index: 1
barcode-layout: copy []
current-barcode-page: copy [page size 215.9 279.4 offset 2 -17 ]
loop (length? bar-code-data-to-print) [
append current-barcode-page compose/deep [
(create-pdf-barcode
(pick bar-code-data-to-print barcode-list-index)
(pick barcode-label-positions current-barcode-pos)
(pick barcode-label-positions (current-barcode-pos + 1))
)
textbox
(pick barcode-label-positions current-barcode-pos)
((pick barcode-label-positions
(current-barcode-pos + 1)) - 10)
56 8
[
font Helvetica 3
(mold pick bar-code-human-readable-to-print
barcode-list-index)
]
]
barcode-counter: barcode-counter + 1
barcode-list-index: barcode-list-index + 1
current-barcode-pos: current-barcode-pos + 2
if current-barcode-pos > 60 [
current-barcode-pos: 1
append/only barcode-layout current-barcode-page
current-barcode-page: copy [
page size 215.9 279.4 offset 2 -17
]
]
]
append/only barcode-layout current-barcode-page
make-dir %./barcodes/
write/binary %./barcodes/labels.pdf layout-pdf barcode-layout
current-barcode-file-name: to-file rejoin [
"./barcodes/labels--booth_" booth-name-to-print "_"
booth-number-login/text "--" now/date "_"
replace/all copy form now/time ":" "-"
".pdf"
]
write/binary current-barcode-file-name layout-pdf barcode-layout
either pdf-choice = "none" [
if error? try [call %./barcodes/labels.pdf] [
write %pdf-choice.txt "SumatraPDF.exe"
call rejoin [
(to-local-file %SumatraPDF.exe) " "
(to-local-file %./barcodes/labels.pdf)
]
]
] [
call rejoin [
(to-local-file read %pdf-choice.txt) " "
(to-local-file %./barcodes/labels.pdf)
]
]
]
Third party DLLs and other tools such as http://sourceforge.net/projects/openbarcodes/files/ provide useful solutions to printing bar codes of all types.
16.26 Creating .swf Files with REBOL/Flash
Flash is a ubiquitous multimedia format used to deliver graphics, sound, video, games, and entire web sites on the Internet. Flash is already installed on over 90% of all computers connected to the Internet. It is available as a small free plugin for every major web browser, at http://get.adobe.com/flashplayer. There are a variety of other flash players available which can display Flash formatted ".swf" files on mobile devices, on desktop operating systems, and on the web. Flash's ubiquity, power, and comprehensive multimedia features makes it a popular platform for rich media development, especially online.
Flash was originally created by the Macromedia company, and is now owned by Adobe. Adobe's Flash CS4 development package is an expensive and heavy development environment with a significant learning curve, and it requires proficiency in the "Actionscript" language. There are many other commercial and open source offerings that can be used to create flash .swf files, but many of those are oriented to creating simple animations with moving text effects, graphic sweeps, pans, fades, etc.
CS4 is a fantastically powerful tool (the industry standard), but for many Flash development needs, you'll be happy to learn that you don't need to venture outside the REBOL world. REBOLers have their own Flash creation tool available, which is freely downloadable and which does not require any additional languages or development tools to create rich multimedia .swf files. Just do the REBOL/flash script at http://box.lebeda.ws/~hmm/rswf/rswf_latest.r, and you've got a powerful Flash development system at your fingertips.
Using REBOL/flash is simple. The following 3 lines demonstrate the basic process of installing the dialect (DOing the REBOL/flash script file), compiling a downloaded REBOL/flash source code file, and then viewing the compiled .swf file in your browser:
do http://box.lebeda.ws/~hmm/rswf/rswf_latest.r ; install REBOL/flash
make-swf/save/html http://tinyurl.com/yhex2cf ; compile the source
browse %starfield1.html ; view the created .swf
To begin working with REBOL/flash in earnest, you'll want to save a copy of the REBOL/flash dialect to your hard drive:
write %rswf.r read http://box.lebeda.ws/~hmm/rswf/rswf_latest.r
The file above is a native REBOL program, created by David 'Oldes' Oliva, which takes input in the REBOL/flash dialect (a REBOL mini-language created by Oldes), and which directly outputs Flash .swf files. No other tools (besides the REBOL interpreter) are required to build very powerful Flash applications that you can use on your web site, in desktop presentations, etc.
Here are a few demo examples to download:
examples: [
http://box.lebeda.ws/~hmm/rswf/examples/swf5/swf5-eyeball.rswf
http://box.lebeda.ws/~hmm/rswf/examples/swf5/swf5-snow.rswf
http://box.lebeda.ws/~hmm/rswf/examples/swf5/swf5-starfield2.rswf
]
foreach file examples [write (to-file last split-path file) (read file)]
Those are just several of the 175 code examples at http://box.lebeda.ws/~hmm/rswf. That large collection of code examples represents the full existing documentation for the REBOL/flash dialect. Reading and experimenting with them will help you become familiar with the REBOL/flash API, and will demonstrate how to use the essential building blocks required to accomplish many necessary REBOL/flash development tasks. Note that Oldes' previous REBOL/flash web site is still available at http://oldes.multimedia.cz/swf. That site has a downloadable zip file of all the code examples and some additional older tools such as a swf-to-exe compiler (that site is no longer updated).
After downloading/doing the script at http://box.lebeda.ws/~hmm/rswf/rswf_latest.r, you can compile REBOL/flash source code files into working .swf files using the "make-swf" function. The /save and /html refinements of that function are most typically used, as they generate the HTML needed to insert the Flash object in your own web pages. The make-swf/save/html function creates a fully functional .swf Flash file and the necessary container HTML file in the same folder as rswf.r. Here's a simple script I use to compile REBOL/flash source files, and then view the compiled results in the browser:
REBOL [title: "Compile and View Flash Files"]
my-rswf-folder: %./
; my-rswf-folder: %/C/12-2-09/My_Docs/WORK/rswf/ ; choose your own
change-dir my-rswf-folder
do %rswf.r ; assuming you've already saved it to your hard drive
make-swf/save/html to-file request-file/filter "*.rswf"
browse to-file request-file/filter "*.html"
Go ahead and try it now - use the above script to compile the source files downloaded earlier. The compile-run process is unbelievably simple! This next script is a build tool that helps to automate the process of creating, editing, and compiling REBOL/flash source files, viewing the results, and then re-doing the entire process repeately to debug and complete projects quickly:
REBOL [title: "REBOL/flash Build Tool"]
; The following folder should be set to where you keep your REBOL/flash
; project files:
my-rswf-folder: %./
; my-rswf-folder: %/C/12-2-09/My_Docs/WORK/rswf/
change-dir my-rswf-folder
do %rswf.r
current-source: to-file request-file/filter/file "*.rswf" %test.rswf
unset 'output-html
do edit-compile-run: [
editor current-source
if error? err: try [make-swf/save/html current-source] [
err: disarm :err
alert reform [
"The following compile error occurred: "
err/id err/where err/arg1
]
either true = request "Edit/Compile/Run Again?" [
do edit-compile-run quit
] [
quit
]
]
unless value? 'output-html [
output-html: to-file request-file/filter "*.html"
]
browse output-html
if true = request "Edit/Compile/Run Again?" [do edit-compile-run]
]
Reading and adjusting the 175 online code examples is an essential part of learning to write your own REBOL/flash scripts. Try this example, derived from http://box.lebeda.ws/~hmm/rswf/examples/swf5/swf5-soundstream.rswf:
write %mp3.rswf {
REBOL [
type: 'swf5
file: %mp3.swf
background: 200.200.200
rate: 12
size: 1x1
]
mp3Stream http://re-bol.com/example.mp3
finish stream
end
}
Notice that I've adjusted the original file name, changed the .mp3 file to a URL link, changed the graphic size and background color, and trimmed off some of the header. I compiled this script with make-swf/save/html, added some text to the generated HTML file, and uploaded both files to http://re-bol.com/examples/mp3.html, all with the script below:
REBOL [title: "Generate from source and upload SWF and HTML to web site"]
; You can edit these file names and FTP info for your own use:
source-file: %mp3.rswf
output-html: %mp3.html
output-swf: %mp3.swf
inserted-html: {<center><h1>MP3 Example!</h1></center>}
insert-at: {<BODY bgcolor="#C8C8C8">}
my-ftp-info: ftp://username:password@site.com/public_html/folder/
destination-url: http://re-bol.com/examples/mp3.html
do http://box.lebeda.ws/~hmm/rswf/rswf_latest.r ; do %rswf.r
make-swf/save/html source-file
content: read output-html
insert (skip find content insert-at length? insert-at) inserted-html
write output-html content
write (join my-ftp-info form output-html) (read output-html)
write/binary (join my-ftp-info output-swf) (read/binary output-swf)
browse destination-url
That's a fully REBOL-based Flash development and deployment system in just a few lines of code, and it requires only a few hundred kilobytes of simple to use, stand-alone development tools. Incredible! Think for a moment about the possibility of automatically creating and deploying custom .swf files right on your web server, in real time, using scripts as simple as this...
Now take a look at http://box.lebeda.ws/~hmm/rswf/example/swf5-3dtext. Notice that the source code for this example (http://box.lebeda.ws/~hmm/rswf/examples/swf5/swf5-3dtext.rswf) imports some assets from a separate included file, stored on the web server (at %includes/fnt_euromode_b.swf). Files such as images and other binary resources are often included in REBOL/flash code, so they can be compiled into the final .swf file. If you want to compile this source code on your local machine, you must download the included files, as well as the source code (TRY THIS EXAMPLE!):
; Notice the folder which contains the included resource. By
; saving downloaded resources to the same folder structure, you
; don't need to alter the original code at all:
make-dir %./includes/
web-resource: http://box.lebeda.ws/~hmm/rswf/includes/fnt_euromode_b.swf
local-resource: %./includes/fnt_euromode_b.swf
write/binary local-resource read/binary web-resource
source: http://box.lebeda.ws/~hmm/rswf/examples/swf5/swf5-3dtext.rswf
do %rswf.r
make-swf/save/html source
; Notice the output file name in the header of the source code file:
browse %3dtext.html
The code example above is complex, but you should notice that much of it is in standard REBOL format (colons are used to create variables, code is contained in square bracketed blocks, functions and structures such as the "for" loop are formatted in standard REBOL syntax, etc.). You'll see many words and phrases in this example that are used in other examples: ImportAssets, EditText, sprite, place, at, "show 2 frames", doAction, "goto 1 and play", showFrame, end, etc. To understand how to use the dialect, start with simpler examples and look for the common words, study their syntax, and experiment with adjusting their parameters. Below are a few examples which demonstrate the basics of using text, images, graphics, sounds, and animation techniques in REBOL/flash. First, a rectangle:
REBOL [
type: 'swf
file: %shape.swf
background: 230.230.230
rate: 40
size: 320x240
]
a-rectangle: Shape [
Bounds 0x0 110x50
fill-style [color 255.0.0]
box 0x0 110x50
]
place [a-rectangle] at 105x100
showFrame
end
Here's some text:
REBOL [
type: 'swf
file: %text.swf
background: 255.255.255
rate: 40
size: 320x240
]
fnt_Arial: defineFont2 [name "Arial"]
some-text: EditText 'the-text 110x18 [
Color 0.0.0
ReadOnly
NoSelect
Font [fnt_Arial 12]
Layout [align: 'center]
]
place [some-text] at 105x100
doAction [the-text: "Hello world!"]
showFrame
end
Try altering all the above examples (copy/paste and edit them directly with the "REBOL/flash Build Tool" provided earlier). They should provide enough basic understanding to begin reading through the API examples at http://box.lebeda.ws/~hmm/rswf.
To see just how powerful REBOL/flash is, take a look at http://machinarium.net/demo/. Machinarium is an absolutely beautifully designed, commercially successful game (see the reviews), created using REBOL/flash. A number of complete, complex Flash web sites have also been created with REBOL/flash. See the links at http://oldes.multimedia.cz/swf and http://miss3.cz for a few examples.
16.27 Printing With REBOL
REBOL is a cross platform solution that runs on over 40 operating systems. As each operating system requires varied OS interfaces to printing (shared libraries, Dlls, and other frameworks), it's important to devise universal approaches that enable printing output onto paper hard copies.
16.27.1 Printing to Images
A quick way to layout simple printed page content is to save a GUI as an image, and then print the image using default or installed system software. REBOL's GUI capabilities provide quick and concise methods for positioning text and images, specifying fonts, etc. on screen. Simply layout the content you want to print, save it using the "save/png" and "to-image" functions, and use the "call/show" function to open it for printing using the default system viewer. This example resizes and positions an image, and sets the font size for a block of formatted text:
REBOL [title: "Simple Print Example"]
some-text: copy {}
repeat i 10 [append some-text rejoin ["This is line #: " i newline]]
save/png %printout.png to-image layout/tight [
backdrop white
area 640x480 white font-color red font-size 40 some-text
at 400x150 image logo.gif 200x100 pink
]
call/show %printout.png
You can also open the generated image for printing in the system browser:
browse %printout.png
Programs such as Irfanview enable batch processing, automated print sizing, and other features that can be helpful in automating repetitive printing tasks. Just Print the Image prints images files directly to printer without popping up any GUI interface.
16.27.2 Printing to HTML
Makedoc.r, explored earlier in this tutorial, provides a simple way to create HTML output for documentation content. Using any browser, that content can be printed, and often with a variety of layout choices and features. To force a printed page break in printed documentation, insert the following code:
<p style="page-break-before: always"></p>
Using free third party printer drivers such as CutePDF, HTML files can easily be saved to PDF and other formats. This is a quick and effective tool chain for creating high quality printable books and documentation that can be viewed across all common operating systems.
HTML can also be used to create more specific layouts for direct printing. Just concatenate together the HTML code and data you need, save it to an .html file, and open with the browser to print. HTML tables are especially useful for printing rows and colomns of data. To see a practical example, examine the print function in the Nano-Sheets spreadsheet app shown earlier in the tutorial.
To force a page to print automatically, append the Javascript "printthispage()" function to the head and body tags of your HTML document:
REBOL [title: "Automatic HTML Printing"]
write %autoprint.html {
<html>
<head>
<title>Automatically Printed Page</title>
<script type="text/javascript">
<!--
function printthispage() {
window.print();
}
//-->
</script>
</head>
<body onload="printthispage()">
This Page Will Print Automatically.
</body>
</html>
}
browse %autoprint.html
16.27.3 Guitar Chord Diagram Printer
The program below creates, saves, and prints collections of guitar chord fretboard diagrams. It was developed to help guitar students print chord diagram charts, in the author's music instruction business. The code demonstrates some common and useful file, data, and GUI manipulation techniques, including the drag-and-drop "feel" technique, used here to slide the pieces around the screen. It also demonstrates the technique of printing output to HTML, and then previewing in a browser (to be printed on paper, uploaded to a web site, etc.):
REBOL [Title: "Guitar Chord Diagram Printer"]
; Load some image files which have been embedded using the "binary
; resource embedder" script from earlier in the tutorial:
fretboard: load 64#{
iVBORw0KGgoAAAANSUhEUgAAAFUAAABkCAIAAAB4sesFAAAACXBIWXMAAAsTAAAL
EwEAmpwYAAAA2UlEQVR4nO3YQQqDQBAF0XTIwXtuNjfrLITs0rowGqbqbRWxEEL+
RFU9wJ53v8DN7Gezn81+NvvZXv3liLjmPX6n/4NL//72s9l/QGbWd5m53dbc8/kR
uv5RJ/QvzH42+9nsZ7OfzX62nfOPzZzzyNUxxh8+qhfVHo94/rM49y+b/Wz2s9nP
Zj+b/WzuX/cvmfuXzX42+9nsZ7OfzX4296/7l8z9y2Y/m/1s9rPZz2Y/m/vX/Uvm
/mWzn81+NvvZ7Gezn8396/4l2/n+y6N/f/vZ7Gezn81+tjenRWXD3TC8nAAAAABJ
RU5ErkJggg==
}
barimage: load 64#{
iVBORw0KGgoAAAANSUhEUgAAAEoAAAAFCAIAAABtvO2fAAAACXBIWXMAAAsTAAAL
EwEAmpwYAAAAHElEQVR4nGNsaGhgGL6AaaAdQFsw6r2hDIa59wCf/AGKgzU3RwAA
AABJRU5ErkJggg==
}
dot: load 64#{
iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAIAAAACUFjqAAAACXBIWXMAAAsTAAAL
EwEAmpwYAAAAFElEQVR4nGNsaGhgwA2Y8MiNYGkA22EBlPG3fjQAAAAASUVORK5C
YII=
}
; The following lines define the GUI design. The routine below was
; defined in the section about "feel":
movestyle: [
engage: func [f a e] [
if a = 'down [
initial-position: e/offset
remove find f/parent-face/pane f
append f/parent-face/pane f
]
if find [over away] a [
f/offset: f/offset + (e/offset - initial-position)
]
show f
]
]
; With that defined, adding "feel movestyle" to any widget makes it
; movable within the GUI. It's very useful for all sorts of graphic
; applications. If you want to pursue building graphic layouts that
; respond to user events, learning all about how "feel" works in REBOL
; is very important. See the URL above for more info.
gui: [
; Make the GUI background white:
backdrop white
; Show the fretboard image, and resize it (the saved image is
; actually only 85x100 pixels):
currentfretboard: image fretboard 255x300
; Show the bar image, resize it, and make it movable. Notice the
; "feel movestyle". Thats' what enables the dragging:
currentbar: image barimage 240x15 feel movestyle
; Some text instructions:
text "INSTRUCTIONS:" underline
text "Drag dots and other widgets onto the fretboard."
across
text "Resize the fretboard:"
; "tab" aligns the next GUI element with a predefined column spacer:
tab
; The rotary button below lets you select a size for the fretboard.
; In the action block, the fretboard image is resized, and then the
; bar image is also resized, according to the value chosen. This
; keeps the bar size proportioned correctly to the fretboard image.
; After each resize, the GUI is updated to actually display the
; changed image. The word "show" updates the GUI display. This
; needs to be done whenever a widget is changed within a GUI. Be
; aware of this - not "show"ing a changed GUI element is an easily
; overlooked source of errors:
rotary "255x300" "170x200" "85x100" [
currentfretboard/size: to-pair value show currentfretboard
switch value [
"255x300" [currentbar/size: 240x15 show currentbar]
"170x200" [currentbar/size: 160x10 show currentbar]
"85x100" [currentbar/size: 80x5 show currentbar]
]
]
return
; The action block of the button below requests a filename from the
; user, and then saves the current fretboard image to that filename:
button "Save Diagram" [
filename: to-file request-file/save/file "1.png"
save/png filename to-image currentfretboard
]
tab
; The action block of the button below prints out a user-
; selected set of images to an HTML page, where they can be
; viewed together, uploaded the Internet, sent to a printer,
; etc.
button "Print" [
; Get a list of files to print:
filelist: sort request-file/title "Select image(s) to print:"
; Start creating a block to hold the HTML layout to be printed,
; and give it the label "html":
html: copy "<html><body>"
; This foreach loop builds an HTML layout that displays each of
; the selected images:
foreach file filelist [
append html rejoin [
{<img src="file:///} to-local-file file {">}
]
]
; The following line finishes the HTML layout:
append html [</body></html>]
; Now the variable "html" contains a complete HTML document that
; can be written to the hard drive and opened in the default
; browser. The code below accomplishes that:
write %chords.html trim/auto html
browse %chords.html
]
]
; Each of the following loops puts 50 movable dots onto the GUI, all at
; the same locations. This creates three stacks of dots that the user
; can move around the screen and put onto the fretboard. There are three
; sizes to accommodate the resizing feature of the fretboard image.
; Notice the "feel movestyle" code at the end of each line. Again,
; that's what makes the each of the dots dragable:
loop 50 [append gui [at 275x50 image dot 30x30 feel movestyle]]
loop 50 [append gui [at 275x100 image dot 20x20 feel movestyle]]
loop 50 [append gui [at 275x140 image dot 10x10 feel movestyle]]
; The following loops add some additional dragable widgets to the GUI:
loop 6 [append gui [at 273x165 text "X" bold feel movestyle]]
loop 6 [append gui [at 273x185 text "O" bold feel movestyle]]
view layout gui
16.27.4 Directing Print to Either the Default System Browser or Installed Apps
As demonstrated earlier, you can open and print constructed HTML documents using your system default browser:
browse %example.html
Third party browsers such as Off By One provide consistent output and provide command line options such as automatic printing, so that files are printed without any apparent third party interface.
The code below enables users to print with either the system default browser, a chosen browser, or if there are errors using those applications, it falls back to using a browser app included via the "binary embedder" script (Off By One, in the example below). If your chosen browser supports it, you can add commands line options to the "call" function to suppress the third party application interface from appearing:
REBOL [title: "HTML printing"]
unless exists? %/OB1.exe [
write/binary %/OB1.exe to-binary decompress {compressed file}
]
unless exists? %html-choice.txt [
write %html-choice.txt "none" ; or for example "%OB1.exe"
]
browser-choice: read %browser-choice.txt
view layout [
btn "Choose HTML Browser Application" [
html-choice-file: to-file request-file/filter "*.exe" "*.exe"
write %html-choice.txt html-choice-file
html-choice: to-local-file to-file read %html-choice.txt
]
btn "Print" [
either browser-choice = "none" [
if error? try [browse current-file] [
write %browser-choice.txt "OB1.exe"
call/show rejoin [
"OB1.exe file:/// "
(to-local-file current-file)
]
]
] [
call/show browser-string: rejoin [
(to-local-file browser-choice)
" file:///C:/"
(replace current-file "./" "")
]
]
]
]
Be sure to examine the "POINT OF SALE SYSTEM" RebGUI application presented earlier to see another example of HTML printing. That example prints nicely formatted cash register receipts for customers.
16.27.5 Printing to PDF
You've already seen how PDF files can be used to layout printed documents. The PDF dialect explored earlier is primarily useful when positioning needs to be exact, upon a given size piece of paper. Millimeter measurements and exact root and relative positions on the paper can be specified with perfect certainty, for situations that require filling pre-printed third party forms (checks, government documents, etc.), for creating exactly sized paper cutout patterns, etc.
As demonstrated earlier, you can open and print created documents using your system default PDF viewer:
call %example.pdf
Third party readers such as Foxit reader are fast and provide command line options such as automatic printing, so that files are printed automatically. Sumatra PDF is a free, small, open source PDF reader that can be compressed and embedded directly in REBOL code. You can download it here.
The code below enables users to print with the system default PDF viewer, or if there are errors using that application, it falls back to using a PDF app included using the "binary embedder" (Sumatra, in the example below). Add command line automation options as needed:
REBOL [title: "PDF printing"]
current-file: %file.pdf
unless exists? %SumatraPDF.exe [
write/binary %SumatraPDF.exe to-binary decompress {compressed file}
]
unless exists? %pdf-choice.txt [
write %pdf-choice.txt "none" ; or for example "%SumatraPDF.exe"
]
pdf-choice: read %pdf-choice.txt
view layout [
btn "Choose PDF Application" [
pdf-choice-file: to-file request-file/filter "*.exe" "*.exe"
write %pdf-choice.txt pdf-choice-file
pdf-choice: to-local-file to-file read %pdf-choice.txt
]
btn "Print" [
either pdf-choice = "none" [
if error? try [call current-file] [
write %pdf-choice.txt "SumatraPDF.exe"
call/show rejoin [
(to-local-file %SumatraPDF.exe) " "
(to-local-file )
]
]
] [
call/show rejoin [
(to-local-file read %pdf-choice.txt) " "
(to-local-file current-file)
]
]
]
]
16.27.6 Printing to Text
When printing the simplest formatted text data, it's sometimes best to just rejoin the text, save it to a text file, and call a text editor program to send the file to a printer using a monospaced font. The following cash register program does just that. Pay attention to the "saveprint" function:
REBOL [title: "Simple POS"]
make-dir %./receipts/
make-dir %./inventory/
make-dir %./users/
write/append %./inventory/sales.txt ""
; Here's some sample data to use for testing the program.
; Edit the printed %header.txt file. Log in to the program using
; "user"/"user" as username/password. Enter "11111111" and "22222222"
; as bar code items.
; --------------------------------------------------------------------
write/binary %./users/users compress trim {"user" "user" "" ""}
save %./inventory/inventory.txt [
; barcode description price taxable cost date qty (8 blank fields)
"11111111" "Item 1" $1.00 "y" $5.00 "24-jun-2011" 40
"" "" "" "" "" "" "" ""
"22222222" "Item 2" $1.00 "y" $.50 "23-jun-2011" 999
"" "" "" "" "" "" "" ""
]
write %header.txt rejoin [
"Company Name" newline
"1234 Street Road" newline
"Townsville, PA 98765" newline
"800-833-7273"
]
; --------------------------------------------------------------------
users: load decompress read/binary %./users/users
if not find users user: request-pass [alert "Incorrect Login" quit]
database: load %./inventory/inventory.txt
divider: " | "
blank-line:
"___________________________________________________________________"
pad: func [strng lngth] [
insert/dup tail strng " " lngth
copy/part strng lngth
]
display-qty: does [
if error? try [idx: index? find database f1/text] [
alert "barcode error"
focus f1 return
]
qty: pick database (idx + 6)
alert join "Quantity: " qty
]
submit: does [
either f1/text <> "" [
if error? try [idx: index? find database f1/text] [
alert "barcode error"
focus f1 return
]
item: pick database (idx + 1)
price: pick database (idx + 2)
taxable: pick database (idx + 3)
f2/text: copy form item
f3/text: copy form price
] [
if ((f2/text = "") or
(f3/text = "") or
(error? try [to-money f3/text])) [
alert "Data Entry Error"
focus f1 return
]
item: f2/text
price: f3/text
taxable: either true = request "Taxable?" ["y"] ["n"]
]
insert head a1/text rejoin [
(pad form item 25) divider
(pad form f1/text 15) divider
(pad form price 10) divider
taxable newline
]
recalculate-totals
focus f1
show gui
]
recalculate-totals: does [
my-items: copy []
my-lines: parse/all (copy a1/text) "^/"
f7/text: copy form length? my-lines
foreach line my-lines [
current-line: parse/all line "|"
foreach item current-line [
append my-items trim item
]
]
subtotal: $0
tax: 0
total: $0
foreach [item barcode price taxable] my-items [
subtotal: subtotal + to-decimal (replace form price "$" "")
if taxable = "y" [
tax: tax + ((to-decimal replace form price "$" "") * .06)
]
total: to-money subtotal + tax
]
f4/text: form subtotal
f5/text: form tax
f6/text: form total
show gui
]
saveprint: does [
x: now y: now/time
write to-file filename: rejoin [
%./receipts/
x/year "-" x/month "-" x/day "_"
y/hour "-" y/minute "-" to-integer y/second "_"
user/1 ".txt"
] rejoin [
(read %header.txt) newline
newline newline
"Item: Barcode:"
" Price: Tax:"
newline
blank-line
newline newline
a1/text
newline
blank-line
newline newline
"Subtotal: " f4/text " (" f7/text " items) Tax: " f5/text
" TOTAL: " f6/text newline
blank-line
newline newline newline
"Receipt: " (replace copy filename "./receipts/" "")
" Payment Type: " d1/text " $" f8/text " -" f9/text
newline a2/text
]
write to-file rejoin [
%./inventory/sales--
x/year "-" x/month "-" x/day "_"
y/hour "-" y/minute "-" to-integer y/second "_"
user/1 ".txt"
] read %./inventory/sales.txt
write/append %./inventory/sales.txt rejoin [x newline a1/text]
f1/text: copy "" f2/text: copy "" f3/text: copy ""
f4/text: copy "" f5/text: copy "" f6/text: copy "" f7/text: copy ""
a1/text: copy " " a2/text: copy "" f8/text: copy "" f9/text: copy ""
show gui
replace a1/text " " "" show gui ; eliminate area bug
call filename ; call/show join "metapad " filename ; specify editor
]
delete-item: does [
my-items: copy []
my-lines: parse/all (copy a1/text) "^/"
foreach item my-lines [append item newline]
a1/text: form copy at my-lines 2
show a1
recalculate-totals
]
tend: does [
f9/text: form ((to-money f8/text) - (to-money f6/text)) show f9
if (to-money f9/text) < $0 [
alert "Not enough money tendered to pay for items!"
]
]
svv/vid-styles/area/colors: [240.240.240 255.255.255]
svv/vid-styles/field/colors: [240.240.240 255.255.255]
svv/vid-styles/area/font/name: "courier"
svv/vid-styles/area/font/size: 12
svv/vid-styles/field/font/size: 20
svv/vid-face/color: white
insert-event-func [
either event/type = 'close [
if true = request "Really close the program?" [quit]
] [event]
]
view center-face gui: layout [
size 1024x550
across
style txt text bold right 90x35 font-size 14 middle
style fld field 100x35
box black 984x2 return
txt 70 "Barcode:" [display-qty] f1: fld 120
txt "Description:" f2: fld 200
txt "Price:" 50 f3: fld 100
loc: at txt 10 "" btn 130x35 font-size 14 "Delete Item" [delete-item]
btn 145x35 font-size 14 "Save and Print" [saveprint]
return
box black 984x2 return
text "Description:" pad 280 text "Bar Code:" pad 180
text "Price:" pad 100
text "Taxable:" return
a1: area 984x300 [recalculate-totals]
return
loc2: at txt "" (loc/1 + 20) txt "SubTotal:" f4: fld 160 return
txt "" (loc/1 + 20) txt "Tax:" f5: fld 160 return
txt "" (loc/1 + 20) txt "Total:" f6: fld 160 return
at loc2 a2: area 400x70 "" font-size 12
text "" 75 loc3: at txt "Item count: " 100 f7: fld 50
at (loc3 + 20x50) d1: drop-down 138 "Cash" "Credit" "Check" "Other"
at (loc2 + 0x80) txt 70 "Tendered:" font-size 12 f8: field 120 [tend]
txt 60 "Change:" font-size 12 f9: field 120
key #"^M" [if form system/view/focal-face/var = "f1" [submit]]
do [focus f1]
]
One benefit to this printing option is that the data in the saved printable documents is structured, and can be used to create reports. This program prints sales reports based on receipts created with the system above:
REBOL [title: "Simple POS Sales Report Printer"]
nl: newline
start-date: request-date
if start-date = none [quit]
start-time: to-time request-text/ |