Re: RFC: Gtk + XML chroming




***** REQUEST FOR COMMENTS:
***** Chroming Gtk applications with XML

Tuomas J. Lukka

Copyright(C) 1998 Tuomas J. Lukka
This document may be redistributed provided that this copyright notice
and paragraph remain in all versions and all changes in the file are 
clearly described below this notice.

This is version 1.0 of this document.
All comments and questions should be addressed to lukka@fas.harvard.edu for
inclusion into the next version.
Newer versions can be found in http://www.fas.harvard.edu/~lukka/gnome


*** Introduction

XML is an interesting new way of describing recursive data structures.
The language is simple and parsers exist in most languages.

Currently, Gtk user interfaces are usually described on a fairly
low level inside the program. This is not good since the functionality
of the program and the appearance of the user interface are quite
separate things. 

In this RFC I demonstrate a proof-of-principle system for using
XML to create Gtk widgets and bindings.


*** Interfacing Gtk and XML

I have implemented a simple system which takes XML input such as

	<UIObject id="Dir">
	    <TopLevel>
	      <HBox>
		<VBox>
			<Button label="Open"> 
				<Signal sig="clicked" method="open"/>
			</Button>
			<Button label="Goto"> 
				<Signal sig="clicked" method="goto"/>
			</Button>
			<Button label="Print"> 
				<Signal sig="clicked" method="print"/>
			</Button>
			<Button label="Reread"> 
				<Signal sig="clicked" method="reread"/>
			</Button>
			<Button>
				<Label text="Quit"/>
				<Signal sig="clicked" method="quit"/>
			</Button>
		</VBox>
		<CList id="list" usize="400,200">
			<Column label='C1' width="200"/>
			<Column label='C2'/>
		</CList>
	      </HBox>
	    </TopLevel>
	</UIObject>

and lets the user use this by creating an object C<$directory> which has the
appropriate methods (C<open>, C<goto>, ...) and 

	my $x = Gtk::XML->new($string); # $string contains the XML code
	my $w = $x->create($directory,"Dir");

This automatically creates the user interface object and
binds the appropriate signals. When an event occurs in the main loop,
the widget will signal by calling the named method of the object and
the object will respond in appropriate ways.

To update the contents of the listbox, the sample code uses the function

	sub update {
		my($this,$uiobj) = @_;
		my $list = $uiobj->widget("list");
		$list->clear();
		for(@{$this->{D}}) {
			$list->append($_, -s $_);
		}
	}

(here the C<$uiobj> refers to the same object returned from the C<create>
call). This shows how the C<id> attribute of an XML element can be used
to refer to particular identifiers in the widget set.

In this way we can achieve partial indepence between the program and the GUI
by defining the interface between the two layers, consisting in this case
the methods and the one listbox named C<list>.

Therefore, the XML code

	<UIObject id="Dir">
	    <TopLevel>
	      <VBox>
		<HBox>
			<Button label="Open"> 
				<Signal sig="clicked" method="open"/>
			</Button>
			<Button label="Goto"> 
				<Signal sig="clicked" method="goto"/>
			</Button>
			<Button label="Print"> 
				<Signal sig="clicked" method="print"/>
			</Button>
			<Button label="Reread"> 
				<Signal sig="clicked" method="reread"/>
			</Button>
		</HBox>
		<CList id="list" usize="400,200">
			<Column label='C1' width="200"/>
			<Column label='C2'/>
		</CList>
		<Button>
			<Label text="Quit"/>
			<Signal sig="clicked" method="quit"/>
		</Button>
	      </VBox>
	    </TopLevel>
	</UIObject>

works just as well with the same object but shows a quite different 
interface. It is even easy to put icons on the buttons without changing
any program code (although in the current proof-of-principle version
this is not yet implemented).


*** Discussion

The system described in this RFC is extremely simple but shows potential.
The benefits include that in a few years, several tools will exist
for creating XML as well as that by abstracting the interface, it is
easy to move into distributed objects where the signal from the
clicked button is transmitted through a distributed object interface
to the appropriate place.

In this way, a CD player might become a CORBA object that knows
how to play CDs and an XML file that describes the Gtk user interface
to show for this object.

What is needed is a discussion of ideas about how to extend the system
in a natural way to include various user-interface paradigms (e.g. 
drag&drop, dynamic menus etc). 


*** Side note

As an interesting aside, this system self-referentially arose 
from the process of writing an XML editor in Gtk. If we describe
the XML editor's user interface in XML, we can easily use it to edit 
itself.


*** Source code

The following source code uses the same class through two different
user interfaces. The code requires XML::Parser-2.07 and Gtk-0.3.
The code below is distributed under the GPL.

--- tui.pl

use Gtk;

require 'UI.pm';

my $x = Gtk::XML->new(
q{
<Elements>
<UIObject id="Foo">
    <TopLevel>
      <HBox>
	<VBox>
		<Button id="but1" label="Open"> 
			<Signal sig="clicked" method="open"/>
		</Button>
		<Button id="but1" label="Goto"> 
			<Signal sig="clicked" method="goto"/>
		</Button>
		<Button id="but1" label="Print"> 
			<Signal sig="clicked" method="print"/>
		</Button>
		<Button id="but1" label="Reread"> 
			<Signal sig="clicked" method="reread"/>
		</Button>
		<Button id="but2">
			<Label text="Quit"/>
			<Signal sig="clicked" method="quit"/>
		</Button>
	</VBox>
	<CList id="list" usize="400,200">
		<Column label='C1' width="200"/>
		<Column label='C2'/>
	</CList>
      </HBox>
    </TopLevel>
</UIObject>
<UIObject id="Dir">
    <TopLevel>
      <VBox>
	<HBox>
		<Button label="Open"> 
			<Signal sig="clicked" method="open"/>
		</Button>
		<Button label="Goto"> 
			<Signal sig="clicked" method="goto"/>
		</Button>
		<Button label="Print"> 
			<Signal sig="clicked" method="print"/>
		</Button>
		<Button label="Reread"> 
			<Signal sig="clicked" method="reread"/>
		</Button>
	</HBox>
	<CList id="list" usize="400,200">
		<Column label='C1' width="200"/>
		<Column label='C2'/>
	</CList>
	<Button>
		<Label text="Quit"/>
		<Signal sig="clicked" method="quit"/>
	</Button>
      </VBox>
    </TopLevel>
</UIObject>
</Elements>
}
);

use Data::Dumper;

print Dumper($x);

my $w = $x->create(Dir->new,"Foo");
my $w2 = $x->create(Dir->new,"Dir");

Gtk->main;


package Dir;
BEGIN{@ISA='Gtk::XML::Updater'}

sub new {
	my($type, $dir) = @_;
	my $this = bless {
		Dir => $dir,
	}, $_[0];
	$this->reread;
	$this;
}

sub reread {
	$_[0]{D} = [<$_[0]{Dir}/.*>,<$_[0]{Dir}/*>]

}

sub open {
	my($this, $uiobj) = @_;
	my $sel = $uiobj->widget("list")->selection;
	my $s = $this->{D}[$sel];
	return if !-d $s;
	my $nd = Dir->new($s);
	$uiobj->create($nd, "Foo");
}

sub goto {
	my($this, $uiobj) = @_;
	my $sel = $uiobj->widget("list")->selection;
	my $s = $this->{D}[$sel];
	return if !-d $s;
	my $nd = Dir->new($s);
	$uiobj->set_object($nd);
}

sub print {
	my($this, $uiobj) = @_;
}

sub quit {
	exit(0);
}

sub update {
	my($this,$uiobj) = @_;
	my $list = $uiobj->widget("list");
	$list->clear();
	for(@{$this->{D}}) {
		$list->append($_, -s $_);
	}
}

--- UI.pm


package Gtk::XML::Updater;

sub add_updated {
	push @{$_[0]{Updated}}, $_[1];
	$_[0]->update($_[1]);
}

sub remove_updated {
	my($this,$obj) = @_;
	my $i;
	for($i=0; $i<@{$this->{Updated}}; $i++) {
		if($this->{Updated}[$i] == $obj) {
			splice @{$this->{Updated}},$i,1;
			return;
		}
	}
	die("Couldn't find remove_updated object");
}

sub update_all {
	my $this = shift;
	for(@{$this->{Updated}}) {$_[0]->update($_,@_)}
}


package Gtk::XMLObject;

sub widget {
	my($this,$name) = @_;
	return $this->{W}{$name};
}

sub create {
	my $this = shift;
	$this->{XOS}->create(@_);
}

sub set_object {
	my($this,$o) = @_;
	$this->{O}->remove_updated($this);
	$this->{O} = $o;
	$this->{O}->add_updated($this);
}

package Gtk::XML;

use XML::Parser;

sub new {
	my($type, $string) = @_;
	$parser = XML::Parser->new;
	my @stack;
	my %objs;
	$parser->setHandlers(
		Start => sub {
			my($p, $e, %attrs) = @_;
			return if $e eq "Elements";
			print "Start; $e %attrs ($#stack)\n";
			my $o = [$e,\%attrs];
			if($e eq "UIObject") {
				$objs{$attrs{id}} = $o;
			} else {
				push @{$stack[-1]}, $o;
			}
			push @stack, $o;
		}, 
		End => sub {
			my($p, $e) = @_;
			return if $e eq "Elements";
			pop @stack;
		},
		Char => sub {
			my($p,$s) = @_;
			return if !@stack;
			push @{$stack[-1]}, $s;
		},
		Default => sub {
			my($str) = @_;
		}, 
	);
	$parser->parse($string);
	bless {
		Objs => \%objs
	}, $type;
}

sub create {
	my($this, $obj, $name) = @_;
	die "No UIobject '$name'" if !defined $this->{Objs}{$name};
	my %hash;
	my $os = $this->{Objs}{$name};
	my $uiobj = bless  {
		O => $obj,
		XOS => $this,
	}, Gtk::XMLObject;
	my @widgets = 
	 map {$this->_create($obj, $_, \%hash, sub{},
		undef, $uiobj) } @{$os}[2..$#$os], 
	$uiobj->{W} = \%hash;
	$obj->add_updated($uiobj);
	return $uiobj;
}

sub _create {
	my($this, $obj, $o, $n, $prevsub, $prev, $uiobj) = @_;
	return if(ref $o ne "ARRAY");
	my $t = $o->[0];
	my $a = $o->[1];
	print "_CR $t $a\n";
	my $w;
	if($t eq 'UIObject') {
		die("Invalid nesting");
	} elsif($t eq 'Pack') {
		my $exp = $a->{expand};
		my $fill = $a->{fill};
		my $pad = $a->{padding};
		for(@{$o[2..$#$o]}) {
			$this->_create($obj, $_, $n, sub {
				$prev->pack_start($_[0], $exp, $fill, $pad);
			}, $prev, $uiobj);
		}
		return;
	} elsif($t eq 'Signal') {
		my $sig = $a->{sig};
		my $m = $a->{method};
		$prev->signal_connect($sig, sub {
			$uiobj->{O}->$m($uiobj)
		});
		return;
	} elsif($t eq 'CList') {
		my @labels;
		my @widths;
		for(@{$o}[2..$#$o]) {
			next if ref $_ ne "ARRAY";
			die("Must be column") if $_->[0] ne "Column";
			push @labels, $_->[1]{label};
			push @widths, $_->[1]{width};
		}
		if(@labels) {
			$w = Gtk::CList->new_with_titles(@labels);
		} else {
			$w = Gtk::CList->new();
		}
		for(0..$#widths) {
			if($widths[$_]) {
				$w->set_column_width($_,$widths[$_]);
			}
		}
	} else {
		if($t eq 'VBox' or $t eq 'HBox') {
			$w = "Gtk::$t"->new($a->{homog},$a->{spacing});
		} elsif($t eq 'Button') {
			if(defined $a->{label}) {
				$w = Gtk::Button->new_with_label($a->{label});
			} else {
				$w = Gtk::Button->new();
			}
		} elsif($t eq 'Label') {
			$w = Gtk::Label->new($a->{text});
		} elsif($t eq 'TopLevel') {
			$w = Gtk::Window->new("-toplevel");
		} else {
			die("Invalid win type $t");
		}
		for(@{$o}[2..$#$o]) {
			$this->_create($obj, $_, $n, sub {
				$w->add($_[0]);
			}, $w, $uiobj);
		}
	}
	if($a->{usize}) {
		$w->set_usize(split ',',$a->{usize});
	}
	$prevsub->($w);
	$w->show;
	if(defined $a->{id}) {
		$n->{$a->{id}} = $w;
	}
	return $w;
}


1;






[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]