monolith
is an application framework which
generates web-based applications. It differs from existing web
tools because it doesn't work in terms of "pages" and "CGI
scripts". Instead it allows you to build applications using
reusable widgets (buttons, labels, etc.) which you arrange into
windows. This makes it much more like building a traditional
application in Windows/MFC, Java/JFC, Tcl/Tk, Motif, etc.
You can also use the basic widgets to build reusable "super-widgets" which you can then embed in other applications, give away or sell. An example might be a discussion system "widget" which can be placed anywhere in another application.
Of course, there are limitations in the web and browsers which means that you can't do everything you might do in a normal application. But you can do most things.
monolith
programs and widgets are written in C or
C++ (actually I've never used C++ with monolith
,
but you are welcome to try).
(This section is from the rws
documentation page).
Shared object scripts are the direct analogy to CGI scripts,
the only difference being that CGI scripts are usually written
in very high level languages like Perl and PHP, and shared
object scripts are loaded into the server process for efficiency.
(Perl CGI scripts can also be loaded into the Apache
server process using mod_perl
, and this is done
for similar reasons of efficiency).
monolith
programs are entire applications, the sort of
thing which normally would be written using dozens of
cooperating CGI scripts. In the case of monolith
, however,
the entire application compiles down to a single .so
file which happens to be (you guessed it) a shared object script.
Imagine that you are going to write yet another web-based email
client. For some reason you want to write this in C (please
don't try this at home: I wrote one in Perl at my last job and
that was hard enough). Here are three possible approaches
using C and rws
:
Write forty or so shared object scripts. Each displays a single frame of the application, one might generate the frameset, a couple of dozen to implement specific operations like emptying trash or moving a message between folders.
This is very much the normal way of writing CGI-based applications.
monolith
application. This will probably be
in lots of C files, but will compile down and be linked
into a single .so
file (eg. email.so
)
which is dropped into the so-bin
directory.
Write a monolith
email super-widget. This is going
to exist in a shared library called
/usr/lib/libmyemail.so
with a corresponding header file defining the interface
called myemail.h
.
Write a tiny monolith
application which just instantiates
a window and an email widget, and embeds the email widget
in the window. This will compile into email.so
(it'll be very tiny) which is dropped into so-bin
.
The advantage of this final approach is that you can
reuse the email widget in other places, or indeed sell
it to other monolith
users.
So monolith
is good when you want to build applications
from widgets as you would if you were building a
Java/Swing, Windows MFC, gtk, Tcl/Tk graphical application.
It's also good if code re-use is important to you.
Shared object scripts are good when you are familiar with
CGI-based techniques to build websites.
Of course, the same rws
server can serve
shared object scripts, multiple monolith
applications,
flat files, and directory listings, all at the same time.
monolith
programs are C programs which compile into
.so
files, called shared object scripts.
The rws
webserver runs these directly.
To compile, probably the simplest thing to do is write a Makefile which does:
gcc -Wall -Werror -c prog.c -o prog.o gcc -shared -Wl,-soname,prog.so prog.o \ -lmonolithcore -lpthrlib -lc2 -lm -o prog.so
The last step generates the actual binary. Copy this into
rws
's
/so-bin
directory, and make sure it is mode 0755
(-rwxr-xr-x
).
To create an /so-bin
directory, add this to your
rws
hosts file:
alias /so-bin/ path: /path/to/your/so-bin exec so: 1 end alias
Now test it out by going to
http://your.webserver/so-bin/prog.so
If it doesn't work, here is a checklist before you email me:
exec so
option is set?
rwsd
?
ldd -r script.so
), apart
from the rws_request_*
symbols which will be resolved
when the library is loaded into rws
?
I have quite successfully used gdb
on a running
server to debug and diagnose problems in monolith
programs.
However note that by default gdb
may have trouble
loading the symbol table for the monolithcore
library and your program. Use the sharedlibrary monolith;
sharedlibrary script.so
command to load symbols instead.
This tutorial begins with the basics of monolith
application design, and then goes through some of the examples
which you will find in the examples/
directory
in the source distribution.
The simple "hello, world" program is useful because it
lets us (a) make sure our development environment is really
working, and (b) sort out the boilerplate code that every
monolith
application needs. You can find
the full program in the source distribution as
doc/hello.c
.
Start by including some necessary headers:
#include <pool.h> #include <monolith.h> #include <ml_window.h> #include <ml_text_label.h>
Then there is some standard boilerplate code that needs to
go in (once only) into every monolith
application.
You don't need to worry about what it does: it's just
glue between rws
's shared object scripts and
the monolith
core code:
/*----- The following standard boilerplate code must appear -----*/ /* Main entry point to the app. */ static void app_main (ml_session); int handle_request (rws_request rq) { return ml_entry_point (rq, app_main); } /*----- End of standard boilerplate code -----*/
Then the "hello, world" program itself:
static void app_main (ml_session session) { pool pool = ml_session_pool (session); ml_window w; ml_text_label text; /* Create the top-level window. */ w = new_ml_window (session, pool); /* Create the text widget. */ text = new_ml_text_label (pool, "Hello, World!"); /* Pack the text widget into the window. */ ml_window_pack (w, text); }
app_main
is the entry point into the
application. It has one parameter, the opaque
ml_session
object, which is explained below. Every
monolith
program needs a top-level window, so we
create this using new_ml_window(3)
.
Now we need to put something into the window, otherwise
it'll appear completely blank. In this case we're
going to put a text label widget inside this window
containing the familiar greeting. We have to tell the
window that it contains the text widget (otherwise
they'd be just two completely separate variables),
so we call ml_window_pack(3)
to place the
text label inside the window.
Now you should compile and run this example
(in doc/hello.c
) using the instructions
above and verify that it runs.
The app_main
function has just one argument
passed to it, the opaque ml_session
object.
What is this used for?
Think of a normal GUI application written in C or C++ (or another language if you like). A user starts up the application. The application interacts exclusively with that one user. Global variables in the application belong entirely to that application and that user. The application continues to be used until the user closes it down. We will define this process of the user starting up the application, using the application and then finally closing it down, as a session.
Web-based applications are slightly different in that
the same code running in the same Unix process can be
used by many users at the same time. For the benefit
of programmers, monolith
automatically
keeps all of these users' sessions separate for you,
maintaining a different opaque ml_session
object for each session.
One implication of this is that app_main
is
called with a different ml_session
object
each time, because a session only starts once. (The same
user might come back later and use the same
application, but that would be a different session).
So a session is different from a user. A session is also
different from an HTTP request. Typically during a session
a user might fill in a few forms, press some buttons,
browse through tables and so on. Each of these operations
probably involves an HTTP request. So in monolith
HTTP requests are very short-lived (of the order of 1ms - 1s),
but sessions can be quite long (hours of activity).
monolith
allocates a separate session pool
for each session, and most applications are expected
to allocate most of their data on this session pool.
Of course when a user finishes their session, the session
pool will be deleted. This normally results in all of the
widgets and stuff created during the session being nuked,
and this is generally a good thing.
C static variables (ie. globals and variables in functions
declared using static
) are shared between
all sessions. This can be useful under some circumstances,
but if what you really want is persistent data,
then you are far better off using a database of some sort
at the back-end. This is because static variables will
obviously be trashed if the monolith
application
is unloaded or the webserver crashes.
c2lib
's global_pool
is also
shared between all sessions (but don't use it directly:
create a subpool in your _init
function
and delete the subpool in your _fini
function).
If C static variables are shared, how do we keep a separate
set of variables for each session? Generally the easiest
way to do this is to allocate a per-session structure
once in app_main
and pass it around. This
is called the session data structure. We'll
see this being done in example 01 below.
A short aside: what are widgets? If you have any experience of using a traditional GUI library or GUI-builder tool, like Tcl/Tk, gtk, Windows MFC, Motif, Java Swing, etc., then you'll probably already know what a widget (or "control" in MS-speak) is, so skip to the next section. This section is for people coming from a non-graphical or purely CGI background.
In traditional GUI environments, applications are not
built up using pages, forms, CGI scripts and so on,
but are instead built up using small reusable objects
called widgets. A typical basic widget might be a
push button, a label, or an image. There are also
compound widgets which store other widgets inside
themselves. In monolith
for example,
a table layout can be used to arrange other widgets
into a table. A table layout is itself a widget, and
you can use a table layout (populated with widgets inside)
any place you would use a basic widget. This is important
because complex layouts are often constructed from several
layers of compound and basic widgets (buttons and labels inside
table layouts inside other table layouts inside windows, etc.)
Here is an example form constructed using nested widgets:
Window Form Table layoutLabel | |
Label | Form input |
Label | Form input |
Empty | Form submit |
The first example, examples/01_label_and_button.c
,
is very simple. It displays a label and a button. Clicking
on the button increments the number on the label. Try this
now. Also try running it from two different browsers and
machines. Notice how the label starts counting up from 0
independently on each machine (demonstrating that each session
is really independent because this demo uses a session data
structure).
As before we begin by including some necessary headers and the same boilerplate code as in our "hello, world" example above. I won't repeat that here, because it's identical. Then we declare our session data structure and a few private functions:
struct data { ml_label lb; /* Label. */ int count; /* Count of number of button presses. */ }; static void increment (ml_session, void *); static void update_label (pool pool, ml_label lb, int count);
We keep (a pointer to) the label widget and the count of button presses in our session data structure. In theory we could keep more here, but in fact it's not necessary. These are the only two variables that we need to make "global" to the session, because these are the only two variables which our callback function will need when it comes to update the label. Our callback function is going to be called when the user clicks on the button, as we'll see in a moment.
The main entry point to our application is called
app_main
. It's called at the beginning
of the session:
static void app_main (ml_session session) { pool pool = ml_session_pool (session); struct data *data; ml_window w; ml_flow_layout lay; ml_label lb; ml_button b;
Notice that we use ml_session_pool(3)
to get the
current session pool, where we are going to make all of our
allocations.
data
will point to our session data structure, but
we have to allocate and initialise it first:
/* Create the private, per-session data area and save it in the * session object. */ data = pmalloc (pool, sizeof *data); data->count = 0;
Next we create the window, label and button. We're going to pack the label and button into a flow layout which is the simplest sort of compound widget: it just displays the widgets inside itself one after another.
/* Create the top-level window. */ w = new_ml_window (session, pool); /* Create the flow layout widget which will be packed into the window. */ lay = new_ml_flow_layout (pool); /* Create the label and button. */ data->lb = lb = new_ml_label (pool, 0); update_label (pool, data->lb, 0); b = new_ml_button (pool, "Push me!"); ml_button_set_callback (b, increment, session, data); /* Pack the label and button into the flow layout widget. */ ml_flow_layout_pack (lay, lb); ml_flow_layout_pack (lay, b); /* Pack the flow layout widget into the window. */ ml_window_pack (w, lay); }
Notice the call to ml_button_set_callback
. When the
button is pressed, the increment
function will
be called like this: increment (session, data)
.
(Recall that data
is our session data pointer).
This is the definition of increment
:
static void increment (ml_session session, void *vp) { struct data *data = (struct data *) vp; update_label (ml_session_pool (session), data->lb, ++data->count); }
It increments data->count
and calls
update_label
which is the function which
actually changes the message on the label.
update_label
is defined as:
static void update_label (pool pool, ml_label lb, int count) { ml_label_set_text (lb, psprintf (pool, "Button pressed: <b>%d</b><br>", count)); }
(If you are unfamiliar with the function psprintf
then you should read the c2lib
documentation).
That's the end of our first significant monolith
application! At around 31 lines of code, it's considerably smaller
than the equivalent CGI script.
This section talks about one of the more fundamental
design considerations you need to think about when first
designing a new monolith
application.
Namely when to build application code and when to
build reusable widgets.
The label and button example above is a complete
monolith
application. What happens
however if we needed to embed this label and button
combo in another program (not a very likely scenario,
I'll admit, but let's imagine that you've developed
a calendar program or something else quite substantial).
The answer is to turn your application into a reusable
widget. Widgets are often composed of many other
more fundamental widgets, and in this case it is possible
to turn the label and button combo into a full-blown
widget. This widget could be used just like any of the
core widgets which monolith
provides.
Turning an application into a widget isn't too hard, depending on the complexity of the application itself, but it's better when designing the application if you first of all work out whether the application as a whole -- or parts of the application -- can be designed as reusable widgets.
If you were designing a calendar program in monolith
then you might decompose the design like this:
Component | Specification | Can be used as a widget? |
---|---|---|
Whole application | Calendar: A tool for storing and tracking daily events, providing appointments, backed up in a database | Yes. By writing the whole calendar as a reusable widget, we can include the calendar widget in an Outlook-style personal information manager (PIM). |
New event form | Form which appears when the user adds a new event. | No. Quite specific to this calendar, so reuse doesn't make much sense. |
Date selector | Form input which allows the user to choose a date and automatically verifies it. | Yes. This is applicable on many different forms in other applications. |
Example 03 will demonstrate how to write a simple reusable
widget. In fact the reusable widget that we're going to write
is the same as the application in example 02 (not covered
here, but supplied with the source in the examples/
directory). So if you want you can study the process of
converting a whole application into a reusable widget.
The source code for example 03 is divided into three files:
03_many_toy_calculators.c
toy_calculator.h
toy_calculator
).
toy_calculator.c
It's helpful at this point if you run the example. You should see four calculators. Try doing some sums. Notice how each calculator acts completely independently of the others.
Let's start with the application code. This is very simple. It just creates four toy_calculator objects and populates a table layout widget with them:
static void app_main (ml_session session) { pool pool = ml_session_pool (session); ml_window w; ml_table_layout tbl; toy_calculator calcs[4]; /* Create the top-level window. */ w = new_ml_window (session, pool); /* Create a table layout widget to arrange the calculators. */ tbl = new_ml_table_layout (pool, 2, 2); /* Create the calculators and pack them into the table layout. */ calcs[0] = new_toy_calculator (pool, session); ml_table_layout_pack (tbl, calcs[0], 0, 0); calcs[1] = new_toy_calculator (pool, session); ml_table_layout_pack (tbl, calcs[1], 0, 1); calcs[2] = new_toy_calculator (pool, session); ml_table_layout_pack (tbl, calcs[2], 1, 0); calcs[3] = new_toy_calculator (pool, session); ml_table_layout_pack (tbl, calcs[3], 1, 1); /* Pack the table into the window. */ ml_window_pack (w, tbl); }
The header file toy_calculator.h
defines the
interface. It's very simple, because there's only one
thing you can do with a toy_calculator
at
the moment, and that's create one by using the
new_toy_calculator
function. Notice the
cdoc
documentation in the comments.
#ifndef TOY_CALCULATOR_H #define TOY_CALCULATOR_H #include <pool.h> #include <monolith.h> struct toy_calculator; typedef struct toy_calculator *toy_calculator; /* Function: new_toy_calculator - toy calculator widget * * @code{new_toy_calculator} creates a new reusable toy calculator * widget. */ extern toy_calculator new_toy_calculator (pool, ml_session); #endif /* TOY_CALCULATOR_H */
The actual implementation of the toy_calculator
widget is complicated, so we'll take it step by step here.
However remember that if all you want to do is to use
a toy_calculator
, then you needn't worry about
the implementation at all. You only need to look at the
header file and use it just like you would any other widget.
toy_calculator.c
begins by including many
header files.
#include <string.h> #include <pool.h> #include <pstring.h> #include <pthr_cgi.h> #include <monolith.h> #include <ml_window.h> #include <ml_table_layout.h> #include <ml_text_label.h> #include <ml_box.h> #include <ml_button.h> #include <ml_widget.h> #include "toy_calculator.h"
Every widget has an associated structure, which is where it
stores all its private data. For callers, the widget structure
is opaque (notice how it is defined in the
toy_calculator.h
header file above). The
toy_calculator
object we've been passing around
above is in fact a pointer, typedef'd to struct
toy_calculator *
. (This is a common coding convention in
c2lib
and pthrlib
). In the actual
implementation, obviously we need to see the private members.
Moreover, every widget structure must begin
with a pointer to struct ml_widget_operations
because the toy_calculator
object "inherits"
from the abstract base class ml_widget
.
Here is the definition of struct toy_calculator
:
static void repaint (void *, ml_session, const char *, io_handle); struct ml_widget_operations toy_calculator_ops = { repaint: repaint }; struct toy_calculator { struct ml_widget_operations *ops; pool pool; /* Pool for allocations. */ ml_text_label disp; /* The display. */ char digits[16]; /* Display digits (only 10+1 used). */ double reg; /* Hidden register. */ int op; /* Operation: PLUS, MINUS, TIMES, DIVIDE. */ ml_box box; /* The top-level box. */ };
repaint
is going to be the function which
actually draws our widget, but we'll see that a little bit
later.
The most important function we need to define is
new_toy_calculator
which creates new
toy_calculator
widgets. I won't show this
function in full because it's quite long (it needs to
create one ml_button
object for every
button on the calculator, and there are 18 of them in all!).
But here's the important outline.
We start by allocating and initialising a new struct
toy_calculator
. A pointer to this is stored in w
.
toy_calculator new_toy_calculator (pool pool, ml_session session) { toy_calculator w; ml_box box; ml_table_layout tbl; ml_button b[18]; ml_text_label disp; w = pmalloc (pool, sizeof *w); w->ops = &toy_calculator_ops; w->pool = pool; strcpy (w->digits, "0"); w->reg = 0; w->op = 0;
Then some code creates the actual widgets contained inside the calculator, the top level being a box widget:
/* Create the box surrounding the calculator. */ box = new_ml_box (pool); /* A table layout widget is used to arrange the buttons and the screen. * There are 6 rows, each with 4 columns. */ tbl = new_ml_table_layout (pool, 6, 4); /* Create the numeric buttons. */ b[0] = new_ml_button (pool, "0"); ml_button_set_callback (b[0], press_button_0, session, w); ml_button_set_key (b[0], 1); : : : : : : : : :
Finally we pack everything up and return the widget
pointer (w
):
: : : : : : : : : /* Pack the table into the box. */ ml_box_pack (box, tbl); /* Save the display widget in the widget structure so that the * callback functions can update it. */ w->disp = disp; /* Save the box, so we can repaint. */ w->box = box; return w; }
When a button is pressed, one of the appropriate callback
functions is called. Because there are 18 buttons, there are
18 separate callback functions, but we only reproduce a few
here. Notice how the toy_calculator
pointer
is passed as the second argument (the void *
),
so we need to cast this back before using it:
static void press_button_N (toy_calculator, int); static void press_button_0 (ml_session session, void *vw) { toy_calculator w = (toy_calculator) vw; press_button_N (w, 0); } : : : : : : : : : static void press_button_N (toy_calculator w, int n) { int len; if (strcmp (w->digits, "0") == 0) w->digits[0] = '\0'; len = strlen (w->digits); if ((strchr (w->digits, '.') && len < 11) || len < 10) { w->digits[len] = '0' + n; w->digits[len+1] = '\0'; ml_text_label_set_text (w->disp, w->digits); } }
Here's the callback function which runs when the [AC] button is pressed:
static void press_button_AC (ml_session session, void *vw) { toy_calculator w = (toy_calculator) vw; strcpy (w->digits, "0"); w->reg = 0; ml_text_label_set_text (w->disp, "0"); }
Finally our widget must know how to repaint (redisplay) itself.
monolith
will call the repaint function at
the appropriate moment, and it must generate the HTML corresponding
to the widget. In the case of this widget, it's a compound
widget built up entirely out of core monolith
widgets. The top-level widget inside the calculator is the
box (w->box
), so we just call the repaint
function for that:
static void repaint (void *vw, ml_session session, const char *windowid, io_handle io) { toy_calculator w = (toy_calculator) vw; ml_widget_repaint (w->box, session, windowid, io); }
That's all. Our reusable toy calculator is now complete.
Example 06 shows the various input controls available when using forms. The example is straightforward, albeit rather long because of the number of different controls demonstrated. We begin with an unusually long list of includes:
#include <string.h> #include <pool.h> #include <pstring.h> #include <pthr_cgi.h> #include <monolith.h> #include <ml_window.h> #include <ml_text_label.h> #include <ml_button.h> #include <ml_table_layout.h> #include <ml_flow_layout.h> #include <ml_form.h> #include <ml_form_submit.h> #include <ml_form_text.h> #include <ml_form_textarea.h> #include <ml_form_password.h> #include <ml_form_select.h> #include <ml_form_radio_group.h> #include <ml_form_radio.h> #include <ml_form_checkbox.h> #include <ml_form_textarea.h>
The private session data structure contains pointers to the form input widgets so that our callback functions are able to read their contents:
/* Private per-session data. */ struct data { ml_window win; /* The form input fields themselves. */ ml_form_text familyname, givenname; ml_form_password password; ml_form_select dd, mm, yyyy; /* Date of birth. */ ml_form_radio_group gender; ml_form_radio m, f; /* Gender. */ ml_form_checkbox eating, drinking, sleeping; /* Interests */ /*ml_form_file photo; File upload, not yet implemented. */ ml_form_select dept; ml_form_textarea comments; };
app_main
allocates the session data structure
and calls create_form
which actually creates
the initial form:
static void app_main (ml_session session) { pool pool = ml_session_pool (session); struct data *data; /* Create the private, per-session data area and save it in the * session object. */ data = pmalloc (pool, sizeof *data); /* Create the top-level window. */ data->win = new_ml_window (session, pool); create_form (session, data); }
create_form
is quite a long, but not very
complex function. It creates an input control of each
type. Notice first the visual structure of the form:
Label | Form input |
Label | Form input |
etc. | |
Empty | Form submit |
I will not reproduce all of the form inputs here:
static void create_form (ml_session session, void *vp) { pool pool = ml_session_pool (session); struct data *data = (struct data *) vp; ml_form form; ml_table_layout tbl; ml_text_label text; ml_form_submit submit; ml_flow_layout flow; int i; /* Create the form. */ form = new_ml_form (pool); ml_form_set_callback (form, submit_form, session, data); /* Create the table. */ tbl = new_ml_table_layout (pool, 10, 2); /* Create the contents of the form. */ text = new_ml_text_label (pool, "Family name / surname"); ml_table_layout_pack (tbl, text, 0, 0); data->familyname = new_ml_form_text (pool, form); ml_table_layout_pack (tbl, data->familyname, 0, 1); : : : : : : : : : /* Submit button. */ submit = new_ml_form_submit (pool, form, "Submit"); ml_table_layout_pack (tbl, submit, 9, 1); /* Pack it all up. */ ml_form_pack (form, tbl); ml_window_pack (data->win, form); }
Notice that we set a callback function on the form:
ml_form_set_callback (form, submit_form, session, data);
When the form is submitted by the user, submit_form
(session, data)
will be called. submit_form
,
reproduced next, can read the value that the user entered
into each form field by calling ml_form_input_get_value
:
static void submit_form (ml_session session, void *vp) { pool pool = ml_session_pool (session); struct data *data = (struct data *) vp; ml_text_label text; ml_table_layout tbl; ml_button button; const char *str; str = psprintf (pool, "Form submitted.\n" "\n" "Family name: %s\n" "Given name: %s\n" "Password: %s\n" "Date of birth: dd = %d, mm = %d, yyyy = %d\n" "Gender: %s\n" " M is checked: %d F is checked: %d\n" "Interests: Eating = %d, Drinking = %d, Sleeping = %d\n" "Dept fields checked: [ %s ]\n" "Comments:\n" "--start--\n" "%s\n" "--end--\n", ml_form_input_get_value (data->familyname), ml_form_input_get_value (data->givenname), ml_form_input_get_value (data->password), 1 + ml_form_select_get_selection (data->dd), 1 + ml_form_select_get_selection (data->mm), 1900 + ml_form_select_get_selection (data->yyyy), ml_form_input_get_value (data->gender), ml_form_radio_get_checked (data->m), ml_form_radio_get_checked (data->f), ml_form_input_get_value (data->eating) ? 1 : 0, ml_form_input_get_value (data->drinking) ? 1 : 0, ml_form_input_get_value (data->sleeping) ? 1 : 0, pjoin (pool, pvitostr (pool, ml_form_select_get_selections (data->dept)), ", "), ml_form_input_get_value (data->comments)); tbl = new_ml_table_layout (pool, 2, 1); text = new_ml_text_label (pool, str); ml_table_layout_pack (tbl, text, 0, 0); button = new_ml_button (pool, "Back to form"); ml_button_set_callback (button, create_form, session, data); ml_table_layout_pack (tbl, button, 1, 0); ml_window_pack (data->win, tbl); }
Notice how we have added a button called Back to form
which calls create_form
.
That's the end of this tutorial. You should be able to
go and write monolith
applications and
widgets now. Full manual pages are included below.
Of course monolith
is written in C, so there are no
classes per se, but this is the general class hierarchy of
monolith
widgets and windows:
ml_window: window or frameset ml_session: user session ml_widget | | +-- ml_box: draws a box around another widget | +-- ml_button: simple button | +-- ml_dialog: ask the user a question | +-- ml_flow_layout: layout widgets one after another | +-- ml_form: surrounds a collection of form inputs | +-- ml_form_input | | | | | +-- ml_form_checkbox: checkbox (tickbox) | | | +-- ml_form_file: file upload input [not implemented] | | | +-- ml_form_password: single line password input | | | +-- ml_form_radio_group: group of radio buttons | | | +-- ml_form_radio: radio button | | | +-- ml_form_select: drop-down list of options | | | +-- ml_form_submit: submit button | | | +-- ml_form_text: single line text input | | | +-- ml_form_textarea: multi-line text input | +-- ml_image: display an icon or image | +-- ml_label: display arbitrary HTML | +-- ml_table_layout: powerful table layouts of widgets | +-- ml_text_label: single line or paragraphs of plain text | +-- ml_toggle_button: toggle button
Inheritance is faked using a technique very similar to the vtables used by C++.
Yes, you can. At the moment, the easiest way to change the look
and feel is to edit the default stylesheet
(default.css
). This way you can make extensive
changes to how your application looks from a single file.
If you don't like the idea of editing default.css
,
then copy it and make your own. Call
ml_window_set_stylesheet
on all your application
windows to point to your new stylesheet.
You can even provide different themes to different users
(so-called "skinning" - ugh I hate that term). Once a user has
logged into the app, call ml_window_set_stylesheet
with the appropriate theme for that user.
Although monolith
supports frames and pop-up
windows, care should be taken because these do not work in
the way that most users expect.
The problem arises when one frame tries to update the state of another frame (the same problem applies to two separate windows, but I'll just use the generic term "frame" here). For example, imagine the following simple frameset:
Left frame [Button 1] [Button 2] [Button 3] |
Right frame |
It's common to want the buttons in the left hand frame to change the contents displayed in the right hand frame, and a naive way to do this would be to set the callback for each button to a function like this:
static void update_right_frame (ml_session session, void *vp) { /* Get private per-session data. */ struct data *data = (struct data *) vp; pool pool = ml_session_pool (session); ml_text_label label = new_ml_text_label (pool, updated content); /* Change the contents of the right hand frame. */ ml_window_pack (data->right_frame, label); }
The problem is that this doesn't work at all. Pressing the buttons will not update the right hand frame.
For seasoned HTML programmers, it will be obvious why this happens. For people used to traditional application development, it is confusing and seems like a bug.
In future, we will add features to Monolith to allow careful developers to use frames in situations such as above. However at the moment, the advice is:
You cannot nest forms. (This is a limitation of HTML.)
If you have several forms on the same page, you can run into problems. A common problem happens when you have two forms on the same dialog like this:
First form [ --- input #1 --- ] [submit] |
Second form [ --- input #2 --- ] [submit] |
If the user types something in input field #1, then types something in input field #2, and presses the second submit button, then the contents of input field #1 will disappear.
To avoid this, either only use one form on a dialog, or design your dialogs so that it is clear that the first submit button must be pressed after filling out the first form.
Another problem with forms (and again, a limitation of HTML) is that they are not interactive. The server cannot read the value of a form input field until the [Submit] button has been pressed.
If you are using forms, then only use form input widgets inside them. Toggle buttons and so on are not form input widgets, and cannot be part of a form.
ml_box_pack(3)
ml_button_get_text(3)
ml_button_set_callback(3)
ml_button_set_key(3)
ml_button_set_text(3)
ml_dialog_add_button(3)
ml_dialog_clear_buttons(3)
ml_dialog_get_icon(3)
ml_dialog_get_text(3)
ml_dialog_get_title(3)
ml_dialog_set_icon(3)
ml_dialog_set_text(3)
ml_dialog_set_title(3)
ml_entry_point(3)
ml_flow_layout_clear(3)
ml_flow_layout_erase(3)
ml_flow_layout_get(3)
ml_flow_layout_insert(3)
ml_flow_layout_pack(3)
ml_flow_layout_pop_back(3)
ml_flow_layout_pop_front(3)
ml_flow_layout_push_back(3)
ml_flow_layout_push_front(3)
ml_flow_layout_replace(3)
ml_flow_layout_size(3)
ml_form_input_clear_value(3)
ml_form_input_get_value(3)
ml_form_input_set_value(3)
ml_form_radio_get_checked(3)
ml_form_radio_group_pack(3)
ml_form_radio_set_checked(3)
ml_form_select_clear(3)
ml_form_select_erase(3)
ml_form_select_get(3)
ml_form_select_get_multiple(3)
ml_form_select_get_selection(3)
ml_form_select_get_selections(3)
ml_form_select_get_size(3)
ml_form_select_insert(3)
ml_form_select_pop_back(3)
ml_form_select_pop_front(3)
ml_form_select_push_back(3)
ml_form_select_push_front(3)
ml_form_select_replace(3)
ml_form_select_set_multiple(3)
ml_form_select_set_selection(3)
ml_form_select_set_selections(3)
ml_form_select_set_size(3)
ml_form_select_size(3)
ml_frameset_get_title(3)
ml_frameset_set_description(3)
ml_frameset_set_title(3)
ml_image_get_src(3)
ml_image_set_src(3)
ml_label_get_text(3)
ml_label_set_text(3)
ml_register_action(3)
ml_session_args(3)
ml_session_canonical_path(3)
ml_session_pool(3)
ml_session_script_name(3)
ml_session_sessionid(3)
ml_table_layout_pack(3)
ml_table_layout_set_align(3)
ml_table_layout_set_colspan(3)
ml_table_layout_set_rowspan(3)
ml_table_layout_set_valign(3)
ml_text_label_set_font_size(3)
ml_text_label_set_font_weight(3)
ml_text_label_set_text(3)
ml_text_label_set_text_align(3)
ml_unregister_action(3)
ml_widget_repaint(3)
ml_window_get_charset(3)
ml_window_get_stylesheet(3)
ml_window_get_title(3)
ml_window_pack(3)
ml_window_set_charset(3)
ml_window_set_stylesheet(3)
ml_window_set_title(3)
new_ml_box(3)
new_ml_button(3)
new_ml_dialog(3)
new_ml_flow_layout(3)
new_ml_form(3)
new_ml_form_checkbox(3)
new_ml_form_password(3)
new_ml_form_radio(3)
new_ml_form_radio_group(3)
new_ml_form_select(3)
new_ml_form_submit(3)
new_ml_form_text(3)
new_ml_form_textarea(3)
new_ml_frameset(3)
new_ml_image(3)
new_ml_label(3)
new_ml_table_layout(3)
new_ml_text_label(3)
new_ml_window(3)