Serpent by Example

Roger B. Dannenberg

Introduction

This guide to the Serpent programming language provides examples of many small programming tasks. Examples illustrate many aspects of Serpent. Source code for most examples is here.

If you are looking for an example of how to do something and it is not here, email the topic to rbd at cs.cmu.edu.

Applying Functions   Arrays   Button   Characters   Classes and Objects   Dialog Boxes   Dictionaries   Editing Hints   File Open Dialog   Inheritance   Interactive Drawing   Linear Search   Listbox   Message Boxes   MIDI   Multiple Source Files  On Quit/Close   Open Sound Control with O2   Open Sound Control   Optional Parameters   Parsing Text   Repr vs String   Resizing Windows   Slider   Sorting   Strings and Flatten   Scrolling   ZeroMQ

Arrays

Topics: the Array type

Primitives: subscript, subseq, last, insert, append, unappend, set_len, len, uninsert, reverse, clear, copy

Introduction

An array, as in most languages, is a sequence of values. In Serpent, arrays are dynamic, i.e. you can append data at the end efficiently. To be most efficient, you can create arrays with the capacity to expand to a certain length.

Note that if you assign an array to another variable, a reference to the array is assigned, so the variable becomes an alias for the array. You can make a shallow copy of the array using the copy method.

The Code

> a = []
-> []
> a = [5, 6, "hi", 'symb', 3.4]
-> [5, 6, "hi", 'symb', 3.4]
> a[3]
-> symb
> subseq(a, 3)
-> ['symb', 3.4]
> subseq(a, 1, 3)
-> [6, "hi"]
> a.last()
-> 3.4
> a.insert(1, "new")
-> [5, "new", 6, "hi", 'symb', 3.4]
> a.append("newer")
-> [5, "new", 6, "hi", 'symb', 3.4, "newer"]
> a.unappend()
-> "newer"
> a
-> [5, "new", 6, "hi", 'symb', 3.4]
> a.setlen(3)
-> [5, "new", 6]
> len(a)
-> 3
> a.uninsert(1)
-> [5, 6]
> a.reverse()
-> [6, 5]
> a
-> [6, 5]
> b = a
-> [6, 5]
> a is b
-> t
> a.append(4)
-> [6, 5, 4]
a is changed
> b
-> [6, 5, 4]
note that b was also changed
> b = a.copy()
-> [6, 5, 4]
> a is b
-> nil
now, b is a different array
> a.reverse()
-> [4, 5, 6]
changing a does not change b
> b
-> [6, 5, 4]

Dialog Boxes

Topics: File Open Dialog, File Browser, File Selector, Messages

Primitives: wxs_file_selector, wxs_message_box

Introduction

wxSerpent has some built-in dialog boxes for opening files, opening directories, getting numbers and text, and displaying messages. This example shows how to select a file and display a message. Note that wxs_file_selector returns a path to the file -- it does not open the file. wxs_message_box returns a return code, which is one of WXS_MSG_YES, WXS_MSG_NO, WXS_MSG_CANCEL, or WXS_MSG_OK.

The Code

# dialog example

require "strparse.srp"
require "debug.srp"
require "wxserpent.srp"

print "calling wxs_file_selector(\"This is an example call";
print " to wxs_file_selector.\", \".\", \"data\", \"txt\",";
print " \"*\", WXS_FILE_OPEN, WXS_DEFAULT_WINDOW)"
print

file_sel = wxs_file_selector(
"This is an example call to wxs_file_selector.",
".", "data", "txt", "*", WXS_FILE_OPEN,
WXS_DEFAULT_WINDOW)

print "wxs_file_selector returned this value:"
print " ", file_sel
print
print "calling wxs_message_box(\"This message box has style";
print " WXS_STYLE_INFORMATION.\", \"This is an example call";
print " to wxs_message_box.\", WXS_STYLE_INFORMATION,";
print " WXS_DEFAULT_WINDOW)"
print
msg_box = wxs_message_box(
"This message box has style WXS_STYLE_INFORMATION.",
"This is an example call to wxs_message_box.",
WXS_STYLE_INFORMATION, WXS_DEFAULT_WINDOW)

print "wxs_message_box returned this value:"
print " ", msg_box
display "Return values", WXS_MSG_YES, WXS_MSG_NO,
display WXS_MSG_CANCEL, WXS_MSG_OK

Dictionaries

Topics: the Dictionary type

Primitives: keys, get

Introduction

A dictionary is an associative array. Any value can serve as an index, in contrast to arrays where the index is a finite range of integers. To create a dictionary, you can just write {}, you can write a non-empty dictionary literal, such as {'address': "5000 Forbes Ave.", 'city': "Pittsburgh"}, or you can write dict(10), where 10 is the amount of pre-allocated space.

To access a dictionary, use array index notation, e.g. data['address'], or use get as in data.get('address'). Use get when it is not an error if there is no value for the given key.

The Code

> d = {}
-> {}
> d['address'] = "5000 Forbes Ave."
-> 5000 Forbes Ave.
> d['city'] = "Pittsburgh"
-> Pittsburgh
> d['state'] = "PA"
-> PA
> d['zip'] = "15213"
-> 15213
> print d['address']; "\n"; d['city']; ",", d['state'], d['zip']
5000 Forbes Ave.
Pittsburgh, PA 15213
-> nil
> d.keys()
-> ['address', 'city', 'state', 'zip']
> d['country']
runtime exception handler called
exception is: bad key
frame variables: {}
frame pc: 4
frame method: <immediate command 0>
frame class: nil

bad key, debugger invoked.
Method: <immediate command 0>, PC: 4, Line: 10, File: <stdin>
1> >
debugger reads >
Resume execution...
> >
> d.get('country', "USA") // provides default for attribute
-> USA
>

Strings and Characters

Topics: creating strings and characters, character codes, string searching, string to integer conversion, string to float conversion

Primitives: ord, +, toupper, tolower, find, int, real

See also: FlattenRepr vs String

Introduction

Serpent does not have a Character type. It uses single-character strings instead.

The Code

def chars()
# make characters from numbers
var char_a = ord("a") // ascii code for "a"
var alphabet = ""
for i = 0 to 26
alphabet = alphabet + chr(char_a + i)
print alphabet

# strings can be accessed as arrays
display "string access", "abcd"[2]

# some more string functions (apply to chars too of course)
display "string fns", toupper("abcd"), tolower("NeXT")

# find a substring
display "find", find(alphabet, "qrst"), find(alphabet, "qed")

# make a number from a string
display "convert to number", int("123"), real("123.456")

Linear Search

Topics: for loop, search

Primitives: for loop

Introduction

This is a simple search example, done in different ways. Note the different types of for loop.

The Code

data = ["This", "is", "an", "array", "of", "words", "to", "search."]

# linear search with for loop, return index of word in data or -1
#
def search_with_for(data, word)
for i = 0 to len(data)
if data[i] == word
return i
return -1

# for-in loop
#
def search_with_for_in(data, word)
var i = 0
for w in data
if w == word
return i
i = i + 1
return -1

# special form of for eliminates call to len
#
def search_with_for_at(data, word)
for w at i in data // i is the index of w in data
if w == word
return i
return -1

# for simple equality searching, use index:
#
def search_with_index(data, word)
return data.index(word)

# test it
#
display "test 1", search_with_for(data, "of")
display "test 2", search_with_for_in(data, "of")
display "test 3", search_with_for_at(data, "of")
display "test 4", search_with_index(data, "of")

Using Multiple Source Files

Topics: require, load, paths

Primitives: require, load

Introduction

Serpent loads init.srp by default when started from the command line with no arguments. It is recommended to use a mnemonic for your application, e.g. sequencer.srp, and create a tiny init.srp to automatically start the program, e.g.
# --- init.srp ---
load "debug" // the ".srp" extension is assumed
load "sequencer"
You can of course split your program into multiple modules and load multiple files from init.srp.

Dependencies

It is common for one module (i.e. a source file) to depend upon definitions in some other module. The recommended way to handle this is with require. The require command loads a file if and only if the file has not been previously loaded. So if sequencer.srp needs the String_parse class from strparse.srp, it should look like this:
# --- sequencer.srp ---
require "strparse" // the ".srp" extension is assumed
... other requires ...
... class and function definitions ...
If all modules use this convention, strparse.srp will be loaded the first time it is needed and no more, making initialization faster.

By convention, the ".srp" extension should be omitted since it is assumed. Naming files differently may circumvent the desired behavior of the require command.

Remember that Serpent searches directories on the SERPENTPATH environment variable so that it can find standard Serpent files. You can include your own directories on the path as well.

Classes and Objects

Topics: class, object, method, instance variable

Primitives: class, var, def

Introduction

Serpent has a single-inheritance object system. Instances are created by "calling" the class name as if it is a function. The parameters are passed automatically to the init method.

The Code

class Account:
var balance
# init() is called automatically when an Account is created
def init(initial_deposit)
balance = initial_deposit
def deposit(x)
balance = balance + x
# note: you could also write this.balance = ...
def withdraw(x)
if balance >= x
balance = balance - x
return balance
else
return false

# test
#
def account_test()
account = Account(0)
account.deposit(5)
display "account_test", account, account.balance
if not account.withdraw(10)
print "don't have $10"

# run it ...
> account_test()
account_test: account = <obj@0x205a48>, account.balance = 5
don't have $10
-> nil
>

Inheritance

Topics: superclass, subclass

Primitives: class, this, super

Introduction

Classes inherit instance variables and methods from super classes or parents in the class hierarchy. Methods declared in a class override those in a super class. To reference the current object, use "this." To invoke a method that is overridden in the object's class, use super.the_method(...). This is especially useful in init() if you want to invoke init in the parent class.

The Code

class Account:
var balance
def init(initial_deposit)
balance = initial_deposit
def deposit(x)
balance = balance + x
def withdraw(x)
if balance >= x
balance = balance - x
return balance
else
return false

# extend the Account class with a name and show method
class Named_account(Account)
var name
def init(a_name, initial_deposit)
# first call superclass's init
super.init(initial_deposit)
name = a_name
 def show()
print name, "has a balance of $"; balance

# test
#
def account_test()
account = Account(0)
account.deposit(5)
display "account_test", account, account.balance
if not account.withdraw(10)
print "don't have $10"

def named_account_test()
named_account = Named_account("Klaatu", 1000)
named_account.show()
named_account.withdraw(300) // inherited method
named_account.show()

# run it ...
> account_test()
account_test: account = <obj@0x205a48>, account.balance = 5
don't have $10
-> nil
> named_account_test()
Klaatu has a balance of $1000
Klaatu has a balance of $700

Editing Hints

Set your editor to insert 4 spaces when you type the tab key. Never type tabs into Serpent programs. It's such a bad idea that future compilers may not allow tabs as white space. For Emacs, you should set indent-tabs-mode to nil. My ~/Library/Preferences/Aquamacs Emacs/customizations.el contains the following string:
    '(indent-tabs-mode nil)
You can set this in Aquamacs Emacs by choosing : Options/Top-level Customization Group, go to the Editing group, go to the Indent group, and find Indent Tabs Mode.

Emacs has a Python mode that works pretty well for Serpent. Slightly better is Serpent mode. This is not a full and correct implementation but rather a modification to Python mode to make a new mode that is more friendly to Serpent. You can find the code in serpent/src/serpent-mode.el.

Under OS X, using Aquamacs Emacs, I copy serpent-mode.el to ~/Library/Application Support/Emacs/site-lisp and add the following to ~/Library/Preferences/Aquamacs Emacs/Preferences.el:

(setq auto-mode-alist (cons '("\\.srp$" . serpent-mode) auto-mode-alist))
(setq interpreter-mode-alist (cons '("serpent" . serpent-mode) interpreter-mode-alist))
(autoload 'serpent-mode "serpent-mode" "Serpent editing mode." t)

If anyone develops a better Serpent mode for Emacs or has suggestions or tips for other systems, let me know!

Sorting a File

Topics: file I/O, sorting

Primitives: open, sort, readlines, writelines

Introduction

To sort a file in memory: read the file, sort the data, and write the result. This program assumes the input consists of lines of text. These are sorted in the order determined by string comparison.

Notes: open() opens a file and returns a vlaue of type 'File' or nil if the file cannot be opened. The second parameter can be "r", "rb", "w", or "wb", where "b" means binary.

readlines() reads the file into an array of strings. Warning: long lines (> 255 characters) may be split.

sort() takes an optional function (denoted by a symbol, e.g. 'my_compare') to compare two items in the array. A second example shows how to sort the file by increasing line length.

The Code

def string_gtr(s1, s2)
s1 > s2

def filesort(in_file, out_file)
var inf = open(in_file, "r")
if not inf
return "Could not open " + in_file
var outf = open(out_file, "w")
if not outf
return "Could not open " + out_file
var content = inf.readlines()
inf.close()
content.sort('string_gtr')
outf.writelines(content)
outf.close()

# test it
#
filesort("unsorted.txt", "sorted.txt")

# another version that sorts by line length
#
def longer(s1, s2)
len(s1) > len(s2)

def filesort_by_len(in_file, out_file)
var inf = open(in_file, "r")
if not inf
return "Could not open " + in_file
var outf = open(out_file, "w")
if not outf
return "Could not open " + out_file
var content = inf.readlines()
inf.close()
content.sort('longer')
outf.writelines(content)
outf.close()

# test it
#
filesort_by_len("unsorted.txt", "by-length.txt")

Example input (unsorted.txt) and output files (sorted.txt and by-length.txt) are online.

Optional Parameters

Topics: optional parameters, keyword parameters, dictionary parameters, and rest parameters

Primitives: keys(), string literals

Introduction

Serpent allows flexible handling of artuments. Unlike C or Java, where you overload procedure or method definitions, in Serpent you make arguments optional. You can mix all forms of parameters: required, optional, keyword, dictionary, and rest.

The Code

# optargs.srp -- optional parameter examples
#
# Roger B. Dannenberg
# Jan, 2010

#---- example of "rest" parameter ----
#
# form sum of all arguments
#
def sum(rest a)
var s = 0
for x in a
s = s + x
s // just "s", or you can say "return s"


# test it
#
display "call sum", sum(1, 2, 3, 4, 5)


#---- example of optional parameter ----
#
# convert to string and print with arbitrary "quote" strings
#
def print_quoted(s, optional quote = "\"")
print quote + str(s) + quote


# test it
#
def print_quoted_tests()
print "test: print_quoted(23) prints ... ";
print_quoted(23)
print "test: print_quoted(23, \"'\") prints ... ";
print_quoted(23, "'")
print "test: print_quoted(23, \"\") prints ... ";
print_quoted(23, "")
print "test: print_quoted(\"hello world\", \"|\") prints ... ";
print_quoted("hello world", "|")


print_quoted_tests()


#---- example of keyword parameter ----
#
# print the time
#
def print_within(s, keyword prefix = "", keyword suffix = "")
print prefix; str(s); suffix


# test it
#
def print_within_tests()
print "test: print_within(23) prints ... ";
print_within(23)
print "test: print_within(\"My Paragraph\", prefix = \"<p>\") prints ... ";
print_within("My Paragraph", prefix = "<p>")
print "test: print_within(\"My Heading\", "
print " prefix = \"<h1>\", suffix = \"</h1>\") prints ... ";
print_within(23, prefix = "<h1>", suffix = "</h1>")


print_within_tests()


#---- example of dictionary parameter ----
#
# print keywords and their values
#
def print_args(required_parameter, dictionary d)
var keys = d.keys()
print "required_parameter =", required_parameter
for k in keys
print k, "=", d[k]

# test it
#
def print_args_test()
print "calling print_args(a = 1, b = 2, c = 3) ..."
print_args(123, a = 1, b = 2, c = 3)
print "... done"

print_args_test()

Parsing Text

Topics: parsing, scanning text, String_parse class

Primitives: creating an array, append

Introduction

Serpent does not have sophisticated pattern matching or regular expression support, so if you want to parse a string, the recommended way is to use the String_parse class in lib/strparse.srp. This class offers many methods to procedurally scan a string from beginning to end and extract fields. A simple thing to do is to call skip_spaces() to advance to the next field, and get_nonspace() to extract the field. There are many other methods. In general, these start looking at a current string position and advance the string position if the desired pattern is found.
get_alpha()
skip spaces and return sequence of letters
get_real()
skip space and return [-]<digits>[.digits] as a string; if there are no digits, just skip the spaces and return nil
get_integer()
skip space and return an integer as a string; if there are no digits, just skip the spaces and return nil
get_char()
return the next character or nil if no characters are left
get_alnum()
skip space and return a sequence of alphanumerics
get_csym()
skip space and return a legal C language identifier
get_nonspace()
skip space and return any sequence of non-space characters
get_delimited(s)
find string surrounded by s (e.g. s may be a double-quote character). If no s is found, return the remainder of the string
get_nonspace_quoted()
skip space and return either a non-space string or, if the first non-space character is a double-quote, search for a matching end double-quote. Quote characters within the string may be escaped with a backslash.
get_space()
return spaces up to the end of the string or to the end
skip_space()
skip over spaces; return nil
skip_over(s)
skip ahead beyond s, returning true for success
skip(s)
if s is at the current position, skip over s and return true
peek()
return the next character in the input string, but do not advance the current position
remainder()
return what is left of the string
remaining_len()
return the length of what is left in the string

The Code

// String_parser is not built-in, so you must load the
// code if it has not already been loaded...
require "strparse"

# return an array of fields from an input string
#
def fields(s)
var sp = String_parse(s) // create and initialize the
// String_parser object
var result = [] // create empty array to accumulate results
sp.skip_space() // this is not necessary -- just illustrating
var field = sp.get_nonspace() // parse out a field
while field != "" // get all the fields
result.append(field) // append each field to array
field = sp.get_nonspace()
return result

# test it
#
print "fields(\"This is a test 123 %$&!\") returns",
print fields("This is a test 123 %$&!")

Applying Functions

Topics: function names are symbols, functions as variables, applying functions, symbols

Primitives: funcall, apply

Introduction

Serpent functions can be referenced by symbols, which are unique strings. Unlike in Python, Serpent uses double quotes to surround string constants and single quotes to surround symbols. (Python does not have symbols.) Symbols are based on Lisp atoms. Symbols in Serpent are entered in a global symbol table (that's how Serpent makes sure they are unique -- while two strings can have the same characters, no two symbols have the same characters). Symbols are global variables. Every global variable is a symbol and every symbol is a global variable (but since symbols have other uses, many if not most symbols have no global variable value.) Symbols also name functions, methods, and classes.

Various functions allow you to call functions and methods with arguments, where the function or method is named by a symbol. In a normal function call, e.g. f(x), the symbol name is taken literally (in this case, it is 'f'), but you might also store a function name in a variable or pass a function name as a parameter.
apply(fn, args)
apply a function to a list of arguments. There are two parameters: a function name (of type Symbol) and a list of arguments (of type Array, no provision is made for keyword and dictionary parameters).
funcall(fn, p1, p2, ...)
apply a function to an argument list. The first parameter is the function to call (of type Symbol). The remaining parameters are evaluated as an ordinary parameter list.
send(object, method , arg1, arg2, ...)
invoke method (a Symbol) on object with the given arguments
sendapply(object, method, argarray)
invoke method (a Symbol) on object with the arguments given in an array (no provision is made for keyword and dictionary parameters)

The Code

> def adder(x, y) // define a simple function 
> x + y
> apply('adder', [5, 7]) // function is a Symbol, args are in an array
-> 12
> funcall('adder', 5, 7) // function is a Symbol, args in ordinary list
-> 12
> fn = 'adder' // you can of course use variables so that functions
-> 'adder'
> args = [4, 9] // and arguments depend on computation...
-> [4, 9]
> apply(fn, args)
-> 13
> require "strparse" // load a class
> s = String_parse("test string")
> meth = 'get_nonspace'
> send(s, meth)
-> test
> sendapply(s, meth, []) // pass an empty array if there are no arguments
-> string

String Processing: Flatten

Topics: appending strings, the flatten function

Primitives: flatten, append

Introduction

Serpent does not do anything clever when you append strings, either with "+" or append(). Appending small strings to the end of a string to build a long string requires both strings to be copied to newly allocated memory. Concatenating 2,000 characters requires 2,000,000 character copies. Serpent has an interesting alternate way to assemble long strings: you can build an array of strings and use the flatten function. In fact, you can build arrays of arrays of arrays, etc. Flatten will recursively find all the strings, append them together, and return one big string, and it does this efficiently.

The Code

# the fast way to build a big string

def flatten_example()
    var text = []
    for i = 0 to 1000
        text.append(str(i)) // appending to arrays is efficient
        text.append(",")
    // now "flatten" the strings in the array to one string
    return flatten(text)

# test it
#
print flatten_example()

# nested arrays can be flattened too
print flatten(["a", "b", ["c", "d"], "e"])

Repr vs Str

Topics: repr and str

Primitives: repr, str

Introduction

Serpent has two ways to convert values to strings. The repr function is used when you want a machine-readable string. In particular, repr prints strings in double quotes and symbols in quotes. The str function does not print the quotes.

A potential problem with repr is it does not escape embedded quotes. See the code example.

The Code

def str_repr()
display "str", str("abc"), len(str("abc"))
display "repr", repr("abc"), len(repr("abc"))
// the backslash is used to insert special characters in a string
// \n is newline, \t is tab, \\ is one backslash, \r is return
// \" is a double quote, \' is a single quote
var tricky = "quoted: \"abc\"" // the string is: quoted: "abc"
display "embedded quotes problem", repr(tricky)
// repr(tricky) does not escape the quotes, so the result is not
// machine readable
// print a quoted string with escaped quotes:
display "a solution", string_escape("quoted:\"abc\"", "\"")
// how to print a symbol with embedded escaped quotes:
display "symbol", string_escape(str('it\'s'), "'")
Try it out:
> load "str-repr"
str: str("abc") = abc, len(str("abc")) = 3
repr: repr("abc") = "abc", len(repr("abc")) = 5
embedded quotes problem: repr(tricky) = "quoted: "abc""
a solution: string_escape("quoted:\"abc\"", "\"") = "quoted:\"abc\""
symbol: string_escape(str('it\'s'), "'") = 'it\'s'
-> nil

Midi

Topics: create a MIDI stream, open a MIDI stream for output, create MIDI message, send MIDI message

Primitives: midi_open, chr, midi_create, midi_write, midi_out_default

Introduction

MIDI is covered in some detail in the Serpent manual. Perhaps the only tricky thing here is forming MIDI messages. MIDI messages are strings of characters (bytes), so we use the chr() function to convert an integer character code into a character type (actually a 1-element Serpent String), then use "+" to append characters and form a message.

The Code

def midi_example()
    var midi_out = midi_create() // create an unopened PortMidi stream
    var midi_dev = midi_out_default() // find default device number
    // Note: you can query and select available devices, but much
    // simpler to open the default device. Choose the default with the
    // PmDefault application in PortMidi
    if midi_open_output(midi_out, midi_dev, 100, 10) != 0
        print "Error opening default MIDI device for output"
        return
    display "sending first MIDI message", time_get()
    // play a note
    var now = int(time_get() * 1000) // integer milliseconds
    for i = 0 to 20
        now = now + 100
        var note_on = chr(0x90) + chr(60 + i) + chr(100)
        midi_write(midi_out, now, note_on)
        var note_off = chr(0x90) + chr(60 + i) + chr(0)
        midi_write(midi_out, now + 100, note_off)
    display "last MIDI message sent", time_get()
    time_sleep(1.0 + now / 1000) // allow time to play notes
    midi_close(midi_out)


# test it
#
midi_example()


Graphical Interfaces Overview

Graphical interfaces are simple to create, but you have to run wxserpent, not serpent.

Creating a Button

Topics: Button, Graphical Input Event Handling

Primitives: Button

Introduction

The Button class creates a button. You have to specify the size, ideally large enough for the label, which you also specify. The button has a parent window. The following example creates a button that calls a function when it is pressed.

The Code

require "wxserpent"

// WXS_DEFAULT_WINDOW is the initial window created automatically
// when you run wxserpent. It's the parent of my_button.
// Parameters are parent, label, x, y, width, height:
my_button = Button(WXS_DEFAULT_WINDOW, "Click Me", 25, 5, 70, 25)

// Tell button who to call when pressed:
my_button.method = 'my_button_handler' // function to call

// Every handler takes 4 parameters:
def my_button_handler(obj, event, x, y)
// but for buttons, parameters are not so useful
print "my_button was clicked. As if you didn't know."

Buttons can invoke methods as well as call functions:

The Code

require "wxserpent"

// declare a simple class and instantiate an object
class Counter
var count
def init()
count = 0
def increment(rest ignore) // ignore all parameters
count = count + 1
display "Counter", count

counter = Counter() // create an instance

// make a button to call the increment method
// notice that increment method must accept the 4 parameters
// passed to all graphical control handlers
obj_button = Button(WXS_DEFAULT_WINDOW, "Invoke a Method", 25, 75, 150, 25)
obj_button.target = counter
obj_button.method = 'increment'


Creating a Slider

Topics: Slider

Primitives: Slider

Introduction

The Slider class creates a horizontal slider. You have to specify the size and location. The slider has a parent window. The following example creates a slider that calls a function when it is pressed.

The Code

require "wxserpent"

// WXS_DEFAULT_WINDOW is the initial window created automatically
// when you run wxserpent. It's the parent of my_slider, but you
// could open another window and put the slider there instead.
// Parameters are parent, minimum value (an integer), maximum value
// (an integer), initial value (an integer), x, y, width, height
// (all coordinates and sizes are integers):
my_slider = Slider(WXS_DEFAULT_WINDOW, 0, 127, 0, 5, 150, 100, 20)
my_slider.method = 'my_slider_handler' // function to call

def my_slider_handler(obj, event, x, y)
    print "my_slider hit", event, "value:", x
    my_gauge.set_value(x) // forward reference to object created next...

// A Gauge is an "analog" value indicator something like one bar of a
// bar graph. The parameters are parent window, range (an integer), and
// x, y, width, height bounding box coordinates (all integers):
my_gauge = Gauge(WXS_DEFAULT_WINDOW, 127, 5, 175, 100, 10)
Notice that 'my_slider_handler' is used (assigned to my_slider.method) before the function is defined. This is valid because 'my_slider_handler' is just a symbol. It exists as soon as the compiler sees it. The slider cannot generate any events or handler calls until the file is fully loaded and compiled because Serpent is non-preemptive: there is no other thread to make the call. By the time a handler can be called, the function my_slider_handler() will be defined.

Similarly, my_gauge is used in my_slider_handler() before it is given a value in the last line. That is valid because Serpent does not check that the variable my_gauge referenced in my_slider_handler() has a value until the function is called. By then, my_gauge will have a value.

Creating a Listbox

Topics: Listbox

Primitives: Listbox, Statictext

Introduction

The Listbox class creates a box of selectable text. You have to specify the size and location. The listbox has a parent window. The listbox can allow multiple selections. The following example creates two listboxes: one with single and one with multiple selections. The listboxes call a function when pressed.

The Code

require "debug"
require "wxserpent"

// WXS_DEFAULT_WINDOW is the initial window created automatically
// when you run wxserpent. It's the parent of my_button.
Statictext(WXS_DEFAULT_WINDOW, "Single selection Listbox", 5, 5, 100, 20)
listbox = Listbox(WXS_DEFAULT_WINDOW, 5, 25, 100, 50, false)

def load_strings(lb):
# strings in listbox'es are added using append():
lb.append("cello")
lb.append("flute")
lb.append("trumpet")
lb.append("violin")

load_strings(listbox)
listbox.method = 'listbox_handler' // function to call for user actions

print "There are", listbox.get_count(), "items in the Listbox:"
for i = 0 to listbox.get_count():
print " ", listbox.get_string(i)


def listbox_handler(obj, event, x, y)
# handle listbox interaction events.
# Every handler takes 4 parameters:
# obj - the listbox
# event - the interaction event type
# event is WXS_LISTBOX_SELECTED
# when there is a new selection
# x, y - depend on the event
display "listbox event", obj, event, x, y, obj.get_count()
display "handler", obj.get_selections(), obj.value()



// Listbox'es can allow multiple selections. Here is another listbox
// to illustrate this feature and how to use it
Statictext(WXS_DEFAULT_WINDOW, "Multiple selection Listbox", 5, 125, 100, 20)
listbox_m = Listbox(WXS_DEFAULT_WINDOW, 5, 150, 100, 50, true)
load_strings(listbox_m) // put the same strings in it
listbox_m.method = 'listbox_handler' // function to call for user actions

print "There are", listbox_m.get_count(), "items in the Listbox:"
for i = 0 to listbox_m.get_count():
print " ", listbox_m.get_string(i)

// Listbox'es can allow multiple selections. Here is another listbox
// to illustrate this feature and how to use it
Statictext(WXS_DEFAULT_WINDOW, "Multiple selection Listbox", 5, 125, 100, 20)
listbox_m = Listbox(WXS_DEFAULT_WINDOW, 5, 150, 100, 50, true)
load_strings(listbox_m) // put the same strings in it
listbox_m.method = 'listbox_handler' // function to call for user actions
Notice that 'listbox_handler' is used (assigned to listbox.method and listbox_m.method) before the function is defined. This is valid because 'listbox_handler' is just a symbol. It exists as soon as the compiler sees it. The listbox cannot generate any events or handler calls until the file is fully loaded and compiled because Serpent is non-preemptive: there is no other thread to make the call. By the time a handler can be called, the function listbox_handler() will be defined.

In the last line of the handler, we print obj.get_selections(), which returns an array of everything selected. This will be an array of (at most) one element in the case of the first listbox, which does not allow multiple selections. In that case, notice that both the formal parameter x and the method value() provide the single selection.



Creating Menus

Topics: Menu

Primitives: Menu, separator, item, Statictext

Introduction

The Menu class creates a pull-down menu in the menu bar for a given window. You specify the window ID (an integer) and the name of the menu (a string). The following example creates a menu with various items and functions that handle menu item selection.

The Code

require "wxserpent"

// WXS_DEFAULT_WINDOW is the initial window created automatically
// when you run wxserpent. It is the parent of my_menu, but you
// could open another window and put the menu there instead.

Statictext(WXS_DEFAULT_WINDOW, "Please try the Command menu",
10, 10, 300, 30)

// Create a Menu; parameters are parent and menu name.
my_menu = Menu(WXS_DEFAULT_WINDOW, "Command")
my_menu.method = 'my_menu_handler' // function to call

// Add items to a menu using the item method. Parameters
// are name (string), help string, and checkable (boolean)
my_menu.item("Hello World", "print Hello World", false)

// The separator() method inserts a separator into the menu
my_menu.separator()

// If checkable is true, the menu item has a boolean state
// and will display a check mark when the state is true
my_menu.item("Trace", "print some trace info", true)
// this flag will be controlled by the Trace menu item
menu_trace_flag = false

def my_menu_handler(obj, event, x, y)
if menu_trace_flag
display "my_menu_handler", obj, event, x, y
if event != WXS_MENU_SELECTED
return // ignore anything other than menu item selection
if x == 0 // first menu item
print "Hello World"
elif x == 1 // the separator
print "This never prints because you cannot select a separator"
elif x == 2 // second menu item
// y == 0 => not checked; y == 1 => checked
menu_trace_flag = (y == 1)
print "trace"


Creating Static Text

You can use Statictext() to add labels, instructions, and other text to a window. Static text objects are not interactive, so there are no events generated and no event handler function is needed. See Creating Menus above for an example. There, the text "Please try the Command menu" is added using a call to Statictext().


Creating a Text Input Box

Topics: Textctrl, Graphical Input Event Handling

Primitives: Textctrl

Introduction

The Textctrl class creates a one-line text input box. You specify the size in pixels. The Textctrl has a parent window. The following example creates a Textctrl that calls a function and prints text when it is entered.

The Code

require "wxserpent"

// WXS_DEFAULT_WINDOW is the initial window created automatically
// when you run wxserpent. It's the parent of my_textctrl.
// Parameters are parent, initial text, x, y, width, height:
my_textctrl = Textctrl(WXS_DEFAULT_WINDOW, "Hello World", 25, 5, 200, 25)

// Tell Textctrl who to call when pressed:
my_textctrl.method = 'my_textctrl_handler' // function to call

// Every handler takes 4 parameters:
def my_textctrl_handler(obj, event, x, y)
// for Textctrl, parameters are not so useful, but get_value()
// can be used to get the text in the box. Not the use of "\""
// which is a string containing a quote character ("\" is the
// "escape" character that quotes the next character):
print "my_textctrl has \""; my_textctrl.value(); "\""


Creating a New Window


Creating a Panel (Subwindow)


Drawing on a Canvas


Creating a Scrolling Panel


Creating a Scrolling Canvas

Topics: Scrolling_canvas, Drawing, Button, Rectangle, Drawing Text

Primitives: Scrolling_canvas, handle method, paint method, scroll, set_brush_color, draw_rectangle, draw_text, set_virtualsize

Introduction

A scrolling canvas is similar to a canvas, but the area that you paint on can be larger than the display area. Scrollbars are automatically added to allow you to scroll the canvas, and the scroll() method lets your program move to a position in the scrollable canvas.

The Code

require "debug"
require "wxserpent"

MY_CANVAS_WIDTH = 500
MY_CANVAS_HEIGHT = 2000

// create a subclass of Scrolled_canvas
class My_canvas (Scrolled_canvas)
def init(parent, x, y, w, h)
super.init(parent, x, y, w, h)
set_virtualsize(MY_CANVAS_WIDTH, MY_CANVAS_HEIGHT)

def paint(flag):
for x = 50 to MY_CANVAS_WIDTH by 150:
for y = 50 to MY_CANVAS_HEIGHT by 100:
set_brush_color("YELLOW") // so text is visible
draw_rectangle(x, y, 100, 20)
draw_text(x + 3, y + 3, "(" + str(x) + ", " + str(y) + ")")

// implement the action of the button. Catch all parameters in "rest" so
// you can call do_jump() with no parameters, OR call it as an event
// handler that gets obj, event, x, and y parameters...
def do_jump(rest ignore):
scroll(int(MY_CANVAS_WIDTH / 2), int(MY_CANVAS_HEIGHT / 2))

my_canvas = My_canvas(WXS_DEFAULT_WINDOW, 0, 0, 200, 200)

// make a button that jumps to a middle location
jump = Button(WXS_DEFAULT_WINDOW, "Jump", 220, 20, 100, 30)
jump.method = 'do_jump'
jump.target = my_canvas


Interactive Drawing

Topics: Canvas, Drawing, Graphical Input Event Handling

Primitives: Canvas, handle method, paint method

Introduction

Interactive graphics combines mouse input, changing variables that control drawing, and updating the display to reflect new variable values. The values of variables are generally called the "state." The image that is displayed should always depend only on the state. In the event the image is hidden and then has to be redrawn, it can be completely reconstructed from the state. To update the image interactively, input is handled by updating the state. Then refresh is called to cause the display to be redrawn. I will change because the state has changed. Note that you do not draw directly on the screen or canvas from the input event handler.

This example program draws lines from the mouse-down location to the mouse-up location.

The Code

require "debug"
require "wxserpent"

// create a subclass of Canvas -- you need to subclass Canvas in
// order to override the paint() method, which is how you make
// custom graphics
//
class Drawing (Canvas)
// the state is an array of lines, where each line is an array
// of x1, y1, x2, y2 coordinates for the endpoints of lines
var lines
var mouse_is_down

// override init() in order to intialize the state of this
// subclass, but pass the usuall parent, x, y, w, h parameters
// to the init() method defined in the Canvas class so that
// the Canvas state is also initialized.
//
def init(parent, x, y, w, h)
super.init(parent, x, y, w, h)
lines = []

// override paint to do custom drawing
//
def paint(x)
for line in lines
draw_line(line[0], line[1], line[2], line[3])

// override handle to handle mouse events
//
def handle(event, x, y)
var line
if event == WXS_LEFT_DOWN
if mouse_is_down
return
line = [x, y, x, y]
lines.append(line)
mouse_is_down = true
elif event == WXS_MOVE and mouse_is_down
// change the end-point of the last line to follow the mouse
line = lines.last()
line[2] = x
line[3] = y
// It might be possible to receive a mouse up without a mouse down
// E.g. the mouse down might be captured by the window system to
// change the focus to this canvas, and the mouse down might not
// be delivered to the canvas. In that case, ignore the mouse-up
// event because we have not received a mouse down yet.
elif event == WXS_LEFT_UP and mouse_is_down
line = lines.last()
line[2] = x
line[3] = y
mouse_is_down = false
else // could be keyboard input
return // no state change, so return
// State has changed. Request redraw.
refresh(true)

// create a canvas to draw on
drawing = Drawing(WXS_DEFAULT_WINDOW, 0, 0, 200, 200)


Resizing Windows

Topics: Canvas, Drawing, Graphical Input Event Handling, Resize

Primitives: on_size, paint method, get_width, get_height

Introduction

You can make the layout within a window depend upon the window size. To do this, you need to subclass Window and override the on_size method, which normally does nothing. Alternatively, you could override the handler method in Window which redirects WXS_SIZE_CHANGED events to a call to the on_size method.

This example program places a canvas in a window. When the window is resized, the canvas is resized in order to fill the window. The canvas draws text that displays the canvas size.

The Code

require "debug"
require "wxserpent"

class My_window (Window):
var canvas

def init(title, x, y, w, h):
super.init(title, x, y, w, h)
canvas = My_canvas(this.id, 0, 0, w, h)
canvas.refresh(t)

def on_size(x, y)
display "on_size", x, y
canvas.set_size(x, y)
canvas.refresh(t)

// create a subclass of Canvas
class My_canvas (Canvas)
def paint(flag):
var x = get_width()
var y = get_height()
draw_text(30, 3, "(" + str(x) + ", " + str(y) + ")")
// draw an arrow pointing to lower right corner
draw_line(0, 0, x, y)
draw_line(x - 30, y - 15, x, y)
draw_line(x - 15, y - 30, x, y)

my_window = My_window("Resizable Window With Canvas", 100, 100, 400, 400)


Open Sound Control with O2

Topics: Open Sound Control, O2

Primitives: o2_initialize, o2_send_start, o2_add_float, o2_send_finish, o2_osc_port_new, o2_osc_delegate, o2_poll

Introduction

O2 is a replacement for Open Sound Control. O2 adds new features but retains compatibility. This example creates a client and server that are equivalent to those created below using Serpent's osc functions.

To use O2, initialize it by calling o2_initialize(). O2 messages are directed to services, so our client needs a service that delgates to an OSC server, and our server needs to create a port for incoming OSC messages. Note that if you really wanted to send from process to process, you'd be better off just using O2 and sending directly to the remote service, eliminating OSC altogether. But if you want to receive from an OSC sender or send to an OSC receiver, these examples show how.

The server is created using o2_osc_port_new() which can listen on a TCP or a UDP port. The client is created using o2_osc_delgate() which creates an O2 service that forwards incoming messages to an OSC server.

The following is a simple OSC client.

The Code

def startup()
    o2_initialize("o2osc", t) // string is the O2 application name, t for debug
    // parameters are: service name, IP address (string), port (int), use TCP flag
    o2_osc_delegate("oscsend", "localhost", 7770, false) // use UDP

def send_afloat()
    o2_send_start()
    o2_add_float(3.14159)
    // parameters are: timestamp (0 for now), address (note that it starts with
    //   the service name, and reliable (tcp) transmission flag (in this case,
    //   the ultimate transmission is controlled by the flag to o2_osc_delegate,
    //   which says send via UDP to the OSC server. The OSC server is probably
    //   using UDP; if so, you must delegate via UDP or you'll never connect.
    o2_send_finish(0.0, "/oscsend/afloat", false)

def run()
    startup()
    while true
        send_afloat()
        time_sleep(3.0)

run()


The following is a simple OSC server that will receive messages from this client. Note that the second parameter to osc_server_init(), which is t (true), turns on some debugging printouts so you can see when OSC messages are received.

The Code

def osc_handler(timestamp, path, types, rest parameters)
    display "osc_handler", timestamp, path, types, parameters

def startup()
    // note that the first parameter can be any application name -- it does
    //    not have to match that of the sender
    display "oscserver", o2_initialize("o2osc", t)
    display "oscserver", o2_service_new("oscrecv") // create service
    // attach a handler to the service, parameters are the path to handle,
    // the types expected, the handler (function name, a symbol), and coerce
    // flag that tells O2 to convert to the specified type(s) if possible
    display "oscserver", o2_method_new("/oscrecv/afloat", "f", 'osc_handler', true)
    // create receive port for OSC messages, which are forwarded to the service
    //    named by the first parameter. Port number is 7770, and false means UDP.
    display "oscserver", o2_osc_port_new("oscrecv", 7770, false)

def run()
    startup()
    while true
        o2_server_poll()
    time_sleep(0.1)

run()

Open Sound Control

Topics: Open Sound Control

Primitives: osc_create_address, osc_send_start, osc_add_float, osc_send, osc_server_init, osc_server_method, osc_server_poll

Introduction

Open Sound Control is a UDP-based protocol for sending messages containing a set of values and an address string. A process can act as a client or a server. A client sends messages and a server receives them. Before sending any messages, the client should construct an address using osc_create_address(). Then, to send each message, the client calls osc_send_start(), followed by calls to add values to the message, and ending with a call to osc_send(). The osc_send() call includes the network address as well as the address string.

The server prepares for messages by registering address strings using osc_server_method(). This associates an address string with a type string denoting the expected number and types of values, an object to handle the message, and a method to invoke.

The following is a simple OSC client.

The Code

def startup()
addr = osc_create_address("", "7770", false)

def send_afloat()
osc_send_start()
osc_add_float(3.14159)
osc_send(addr, "/afloat")

def run()
startup()
while true
send_afloat()
time_sleep(3.0)

run()


The following is a simple OSC server that will receive messages from this client. Note that the second parameter to osc_server_init(), which is t (true), turns on some debugging printouts so you can see when OSC messages are received.

The Code

def osc_handler(path, rest parameters)
display "osc_handler", path, parameters

def startup()
display "osctest", osc_server_init("7770", t)
display "osctest", osc_server_method("/afloat", "f", nil, 'osc_handler')


def run()
startup()
while true
osc_server_poll()
time_sleep(0.1)

run()


ZeroMQ

Introduction

ZeroMQ is a library for message passing. Using ZeroMQ is something like using network sockets (see Networks examples), but in general, ZeroMQ has some nicer features. For example, a client can create a socket and send a message to a server before the server is created. When the server is initialized, it receives the client's message. Also, if the server generates one reply to each client request, there is no need to manage client connections. Instead, the server just receives a request and sends a reply; ZeroMQ takes care of sending the reply to the client, cleaning up when clients terminate, etc.

Here is an example of a client and server, which you would normally run in separate processes. By changing the address from "localhost" to an Internet address in the client, e.g. "192.168.1.39", you can connect to a server over the Internet. Notice that the server uses zmq_open_reply() to get a socket, and the server generates one zmq_send() call for each zmq_receive(). The client uses zmq_open_request() to get a socket and follows each zmq_send() with a zmq_recv_block().

The server code:

def main()
zmq_init() // prepare to use zmq
var socket = zmq_open_reply()
display "bind step", zmq_bind(socket, "tcp", "*", 5555)
for i = 0 to 10
var req = zmq_recv_block(socket)
print "Server:", req, "-> World"
zmq_send(socket, "World")
zmq_close(socket)
zmq_term()

main()
exit()

The client code:

def main()
zmq_init() // prepare to use zmq
var socket = zmq_open_request()
display "connect step", zmq_connect(socket, "tcp", "localhost", 5555)
for i = 0 to 10
zmq_send(socket, "Hello")
var result = zmq_recv_block(socket)
print "Client: Hello ->", result
zmq_close(socket)
zmq_term()

main()
exit()

More ZeroMQ

ZeroMQ assumes that communication patterns are regular. The Request/Reply pattern illustrated by the client/server code above is one example. Other patterns supported by ZeroMQ are the Push/Pull pattern, where a sender produces messages that are consumed by a receiver, and the Publish/Subscribe pattern, where messages are "broadcast" to any number of receivers that can selectively receive messages by topic. Examples of these can be found in serpent/programs/zmq*.srp. Some details on the corresponding functions can be found in the Serpent documentation. Most of these functions correspond closely to the ZeroMQ API for the C programming language, and extensive ZeroMQ documentation is available online.



On Quit/Close

Introduction

When a program exits or a window closes, you may want to prompt the user to save data or perform other cleanup actions. In the following little program, a handler is installed for the File:Quit menu item and another handler is installed for the default window.

The quit_handler for the File:Quit menu is only called when Quit is selected. The handler pops up a dialog box to get confirmation from the user. If the user clicks "yes," we want to really quit. However, since Quit is "handled" in this code, wxserpent does not normally pass the event on to the built-in handler which will actually exit the application. To get wxserpent to invoke the built-in handler, we have to tell wxserpent explicitly that this event was not handled. This is accomplished by calling wxs_event_not_handled(). Note that we only call wxs_event_not_handled() if the user answers "yes" in the dialog box.

The win_handler handles every event to the window that is not first captured by some component within the window. We are only interested in the WXS_CLOSE_WINDOW event, so we test for it. When found, we use the same strategy as quit_handler: get confirmation with a dialog box, and if the user really wants to close, use wxs_event_not_handled() to tell wxserpent to pass the event on to the built-in close-window event handler.

The Code

require "debug"
require "wxserpent"

win = default_window
win.method = 'win_handler'

file_menu = win.get_menu("File")
// parameters are Menu item name, Help string, Checkable, Target, Method:
file_menu.item("Quit", "quit the application", nil, nil, 'quit_handler')

display "file quit", file_menu.map, file_menu.menu

def quit_handler(obj, event, x, y):
display "quit_handler", obj, event, x, y
var quit = wxs_message_box("Do you really want to quit?",
"Quit selected", WXS_STYLE_YES_NO, win.id)
display "message box returns", quit, WXS_MSG_NO, WXS_MSG_YES
// The default is that this handler performed the request, so no
// further action (e.g. really quit the application) is performed.
// We override this default by calling wxs_event_not_handled().
if quit == WXS_MSG_YES:
wxs_event_not_handled()

def win_handler(obj, event, x, y):
display "win_handler", obj, event, x, y, WXS_CLOSE_WINDOW
if event == WXS_CLOSE_WINDOW:
var close = wxs_message_box("Do you really want to close?",
"Close", WXS_STYLE_YES_NO, win.id)
if close == WXS_MSG_YES:
wxs_event_not_handled()

Animation Basics


Networks: Server Creation

--- in progress ---

Networks: Detecting Disconnects

--- in progress ---

Networks: Message Length

--- in progress ---