13.3. Gtk2::Helper - Simple Chat

13.3.1. Objective

This lesson will build on our newly acquired knowledge of Gtk2::Helper. We will create a simple chat program. We will introduce a few nice ticks to give the user a more pleasant time communicating.

Table 13-4. Gtk2 object classes used

Gtk2 Class Name
Gtk2::Window
Gtk2::VBox
Gtk2::Image
Gtk2::TextView
Gtk2::TextBuffer
Gtk2::TextIter
Gtk2::TextMark
Gtk2::Frame
Gtk2::HBox
Gtk2::Button
Gtk2::Entry
Gtk2::Helper
Gtk2::ScrolledWindow
Gtk2::Gdk::Pixbuf
Gtk2::Pango
Gtk2::Gdk::Keysyms

13.3.2. Requirements

The chat program needs the following requirements:

13.3.3. The Screenshot

Figure 13-2. Simple Chat

13.3.4. The Code

The program can be found here: 'Chat program'.

Note

How to chat

You need to run two instances of the program on the same machine. When calling the program, specify who you want to be, "shrek" or "donkey". To post a message, enter it on the Gtk2::Entry and click the "OK" button to send it.

	
  1 #!/usr/bin/perl -w
  2 
  3 use strict;
  4 use IO::Socket;
  5 
  6 use Gtk2 -init;
  7 use Glib qw/TRUE FALSE/;
  8 use Gtk2::Helper;
  9 use Gtk2::Pango;
 10 use Gtk2::Gdk::Keysyms;
 11 
 12 my ($who) = $ARGV[0];
 13 #check and warn if we received wrong argument
 14 unless(($who)&&($who =~ m/shrek|donkey/)){
 15 print "usage <program name> <shrek | donkey>\n";
 16 exit;
 17 }
 18 
 19 my($sock,$MAXLEN, $LISTEN_PORT, $SEND_PORT,$tag_send,$tag_receive,$img_big,$img_send,$img_rec);
 20 $MAXLEN = 1024;
 21 
 22 #set the variables up if you are shrek:
 23 if ($who =~ m/shrek/){
 24 
 25 	$LISTEN_PORT 	= 5151;
 26 	$SEND_PORT 	= 5152;
 27 	$tag_send	= 'shrek';
 28 	$tag_receive 	= 'donkey';
 29 	$img_big 	= './pix/shrek.gif';
 30 	$img_send 	= './pix/shrek_small.jpg';
 31 	$img_rec 	= './pix/donkey_small.gif';
 32 	
 33 }
 34 
 35 #set the variables up if you are donkey:
 36 if ($who =~ m/donkey/){
 37 
 38 	$LISTEN_PORT 	= 5152;
 39 	$SEND_PORT 	= 5151;
 40 	$tag_send	= 'donkey';
 41 	$tag_receive 	= 'shrek';
 42 	$img_big 	= './pix/donkey.gif';
 43 	$img_send 	= './pix/donkey_small.gif';
 44 	$img_rec 	= './pix/shrek_small.jpg';
 45 }
 46 
 47 my $tview;
 48 	
 49 #---------------------------------
 50 #set up a udp server waiting for incomming messages
 51 $sock = IO::Socket::INET->new(LocalPort => $LISTEN_PORT, Proto => 'udp')
 52 or die "socket: $@";
 53 
 54 #add a Gtk2::Helper watch on any incomming connections
 55 Gtk2::Helper->add_watch ( fileno $sock, 'in',sub{ 
 56 	my ($fd,$condition,$fh) = @_;
 57 	#call 'watch_callback' to handle the incomming data	
 58 	\&watch_callback($fh,$tview);
 59 	},$sock);
 60 	
 61 print "Awaiting UDP messages on port $LISTEN_PORT\n";
 62 #---------------------------------
 63 
 64 #Gtk2::Rc->parse ('/usr/share/themes/Anger/gtk/gtkrc');
 65 #this is parsing our theme file, giving a personal touch
 66 Gtk2::Rc->parse ('gtkrc');
 67 
 68 #standard window creation, placement, and signal connecting
 69 my $window = Gtk2::Window->new('toplevel');
 70 $window->signal_connect('delete_event' => sub { exit;});
 71 $window->set_border_width(5);
 72 $window->set_position('center_always');
 73 #again just some fine touches
 74 $window->set_title("I am $tag_send");
 75 $window->set_icon_from_file($img_send);
 76 
 77 #this vbox will geturn the bulk of the gui
 78 my ($vbox) = &ret_vbox();
 79 
 80 #add and show the vbox
 81 $window->add($vbox);
 82 $window->show();
 83 
 84 #our main event-loop
 85 Gtk2->main();
 86 
 87 sub ret_vbox {
 88 
 89 my $vbox = Gtk2::VBox->new(FALSE,0);
 90 	#add an image to indicate who you are
 91 	my $img_who = Gtk2::Image->new_from_file($img_big);
 92 
 93 $vbox->pack_start($img_who,TRUE,TRUE,0);
 94 
 95 	my $frame = Gtk2::Frame->new("Simple Chat - I am $tag_send");
 96 		
 97 	#method of Gtk2::Container
 98 	$frame->set_border_width(5);
 99 	
100 		my $sw = Gtk2::ScrolledWindow->new (undef, undef);
101     		$sw->set_shadow_type ('etched-out');
102 		$sw->set_policy ('automatic', 'automatic');
103 		#This is a method of the Gtk2::Widget class,it will force a minimum 
104 		#size on the widget. Handy to give intitial size to a 
105 		#Gtk2::ScrolledWindow class object
106 		$sw->set_size_request (300, 300);
107 		#method of Gtk2::Container
108 		$sw->set_border_width(5);
109 		
110 			$tview = Gtk2::TextView->new();
111 			#we do not want to edit anything
112 			$tview->set_editable(FALSE);
113 			$tview->set_cursor_visible (FALSE);
114 							
115   			my $buffer = $tview->get_buffer();
116 			
117 			#create a mark at the end of the buffer, and on each
118 			#'insert_text' we tell the textview to scroll to that mark
119 			$buffer->create_mark ('end', $buffer->get_end_iter, FALSE);
120 			$buffer->signal_connect (insert_text => sub {
121 				$tview->scroll_to_mark ($buffer->get_mark ('end'),
122 	                        0.0, TRUE, 0, 0.5);
123 			});
124 			
125 			#create a tag for the shreck		
126 			$buffer->create_tag ("shrek",
127 					style =>'italic',
128 					weight => PANGO_WEIGHT_ULTRALIGHT,
129 					family => 'flubber',
130 					foreground => "#189f3b",
131 					size => 20000,
132 					);
133 					
134 			#create a tag for the donkey		
135 			$buffer->create_tag ("donkey",
136 					style =>'italic',
137 					weight => PANGO_WEIGHT_ULTRALIGHT,
138 					family => 'davis',
139 					foreground => "blue",
140 					size => 20000,
141 					);
142 						
143 		$sw->add($tview);
144 	$frame->add($sw);
145 $vbox->pack_start($frame,TRUE,TRUE,4);
146 	#--------------------------------------
147 	my $hbox = Gtk2::HBox->new(FALSE,5);
148 	
149 		my $ent_send = Gtk2::Entry->new;
150 	$hbox->pack_start($ent_send,TRUE,TRUE,0);
151 	
152 		my $btn_send = Gtk2::Button->new_from_stock('gtk-ok');
153 		#connect the 'key_press_signal' to a handler that will
154 		#filter for 'Return'; if TRUE, trigger a button click
155 		$ent_send->signal_connect('key_press_event'=> sub {
156 			my ($widget,$event) = @_;
157 			if($event->keyval == $Gtk2::Gdk::Keysyms{Return}) {
158                  		$btn_send->clicked;
159                  		return 1;
160 			}
161 		
162 		});
163 		
164 		$btn_send->signal_connect("clicked" =>sub {
165 			#get the contents of the entry
166 			my $msg_send = $ent_send->get_text;
167 			#clear the entry
168 			$ent_send->set_text("");
169 			#grab focus again for the next round of talks
170 			$ent_send->grab_focus;
171 			#if there was bogus input, ignore it!
172 			if ($msg_send !~ m/^\s*$/){
173 				#open up a UDP client connection
174 				my $server_host = "127.0.0.1";
175 				my $sock = IO::Socket::INET->new(Proto => 'udp',
176                               	PeerPort  => $SEND_PORT,
177                               	PeerAddr  => $server_host)
178     				or die "Creating socket: $!\n";
179 				#send the message out on the socket
180 				$sock->send($msg_send."\n") or die "send: $!";
181 				#update the screen locally
182 				&update_buffer($buffer,$msg_send,TRUE);	
183 			}
184 		});
185 		
186 	$hbox->pack_end($btn_send,TRUE,TRUE,0);	
187 	#--------------------------------------
188 $vbox->pack_start($hbox,TRUE,TRUE,4);
189 	#set initial focus
190 	$vbox->set_focus_child($hbox);
191 	
192 $vbox->show_all();
193 return $vbox;
194 }
195 
196 	
197 sub watch_callback {
198 
199 	my ($fh,$tview) = @_;
200 	my $msg;
201 	$fh->recv($msg, $MAXLEN) or die "recv: $!";
202 		print $msg."\n";
203 		my $buffer = $tview->get_buffer();
204 		&update_buffer($buffer,$msg,FALSE);
205 		
206 	return 1;
207 }
208 
209 Gtk2->main;
210 
211 sub update_buffer {
212 
213 	my ($buffer,$msg,$send)= @_;
214 	
215 	$msg = $msg."\n";
216 	if ($send) {
217 	
218 		my $iter = $buffer->get_end_iter;
219 		$buffer->insert_pixbuf ($iter,  Gtk2::Gdk::Pixbuf->new_from_file ($img_send));
220 		$buffer->insert_with_tags_by_name($iter, $msg,$tag_send); 
221 	
222 	}else{
223 	
224 		my $iter = $buffer->get_end_iter;
225 		$buffer->insert_pixbuf ($iter,  Gtk2::Gdk::Pixbuf->new_from_file ($img_rec));
226 		$buffer->insert_with_tags_by_name($iter, $msg,$tag_receive); 
227 		
228 	}
229 }

13.3.5. Code to fulfill the requirements

We choose two famous characters from the movie "Shrek" to represent the chatters in our program.

Two people must be able to communicate with each other over a TCP/IP network

This is the core function of the program, the rest is just nice ways of presenting it. We use the IO::Socket Perl module. We use UPD instead of TCP because it is simpler and faster. When we specify "shrek" as an argument to the program, it will set up a certain set of variables, specific to "shrek". If we specify "donkey" as an argument to the program, it will set up a certain set of variables, specific to "donkey" [1]

Depending on the chosen variable values, it will set up a UDP server on a specified port and make use of Gtk2::Helper to wait for any incoming messages. Note that our filehandle ($fh) is now in the form of a network socket ($sock).

#---------------------------------
#set up a udp server waiting for incoming messages
$sock = IO::Socket::INET->new(LocalPort => $LISTEN_PORT, Proto => 'udp')
or die "socket: $@";

#add a Gtk2::Helper watch on any incoming connections
Gtk2::Helper->add_watch ( fileno $sock, 'in',sub{ 
	my ($fd,$condition,$fh) = @_;
	#call 'watch_callback' to handle the incoming data	
	\&watch_callback($fh,$tview);
	},$sock);
	
print "Awaiting UDP messages on port $LISTEN_PORT\n";
#---------------------------------

We use a Gtk2::Entry widget to type our posting into. When we want to send the message, we click on the "OK" button. This will set up a UDP connection to the listening server, and send the message out on that network socket.

Note

This chat demo is talking to IP "127.0.0.1", which is the IP of the local machine. Should you want to talk to a different IP, just change it in code, or as an exercise, add an Gtk2::Entry widget where you can specify who to talk to.

#open up a UDP client connection
my $server_host = "127.0.0.1";
my $sock = IO::Socket::INET->new(Proto => 'udp',
	PeerPort  => $SEND_PORT,
	PeerAddr  => $server_host)
    		or die "Creating socket: $!\n";
#send the message out on the socket
$sock->send($msg_send."\n") or die "send: $!";

To add an aesthetic touch, the GUI must be given a theme.

To do this, we make use of an "rc" file. [2] This is like a style sheet, which Gtk+ use to format the appearance of specified widgets. Every modern Linux distribution will have default "rc" files that is used by Gtk+. This will give programs an universal look and feel. In our program we specify an extra "rc" file to use:

#this is parsing our theme file, giving a personal touch
Gtk2::Rc->parse ('gtkrc');

The rc file we use was taken from the "Anger" theme and modified.

Tip

It is always helpful to look at existing "rc" files when creating your own.

Caution

Remember that rc files are not using Perl. The class names are the standard Gtk+ names. EG GtkButton VS Gtk2::Button.

We will look at a few snipets from the "rc" file and comment on them.
bg_pixmap[NORMAL] = "./pix/bg.png"
This will fill the background of the window with tiled images of the image file specified.
#create a style for the enrty where we 
#enter our text

style "ent"
{
	font_name = "Times Bold 15"
	base[NORMAL] = "blue"
	text[NORMAL] = "red"
		
}
#------------------------------------
#Bind this style to any GtkEnry class

class "GtkEntry" style "ent"
#-------------------------------------
The above piece will change the background and the text color of all the GtkEntry widgets. It will also change the font. "font_name" uses the more friendly Pango format. This is recomended above "font" which uses the more complex XLFD font description.
#create a style for the label inside a GtkFrame

style "frame_label" = "default" {

	font_name = "Times Bold 20"
	fg[NORMAL] = "#1d6629"
	
}

#------------------------------------
#Bind this style to any GtkFrame's GtkEnry.

widget_class "*.GtkFrame.GtkLabel" style "frame_label"
#-------------------------------------
When you want to change the text of a GtkFrame, remember, the text is actually part of a GtkLabel inside the GtkFrame widget! If you forget this, you may become frustrated, as nothing will happen when you bind the style to GtkFrame. Use the widget_class binding form for this to avoid changing ALL GtkLabel widgets by accident.

To change the GtkButton, we made use of the "image" theme engine. The theme engines can be quite cryptic and may not be well documented. The best advice is to investigate existing uses of them to gain a better understanding.

When the two persons chat, the program should be able to function without the use of a mouse.

To do this we give focus to Gtk2::Entry on initial startup. When the program starts we set the initial focus to the Gtk2::HBox. This will default to the Gtk2::Entry because it was packed into the Gtk2::HBox first. Gtk2::Entry is now ready to accept input from the keyboard.

#set initial focus
$vbox->set_focus_child($hbox);
When the button is clicked, the focus moves from the Gtk2::Entry to the button. In the signal handling code, we have to move the focus on the button back to the Gtk2::Entry. This will allow us to start entering our next message into the Gtk2::Entry after we clicked the button.
#clear the entry
$ent_send->set_text("");
#grab focus again for the next round of talks
$ent_send->grab_focus;

The keyboard can be used to trigger a "click" on the Gtk2::Button. Use of the key_press_event signal of Gtk2::Entry. Connect it to a closure that will cause the click method of Gtk2::Button to be called whenever the "return" key is pressed.

$ent_send->signal_connect('key_press_event'=> sub {
my ($widget,$event) = @_;
	if($event->keyval == $Gtk2::Gdk::Keysyms{Return}) {
                 $btn_send->clicked;
                 return 1;
	}
		
});

The latest postings must always be visible

To do this, we put a Gtk2::TextMark at the end of the Gtk2::TextBuffer and call the scroll_to_mark method of the Gtk2::TextView each time text is inserted into the Gtk2::TextBuffer.

#create a mark at the end of the buffer, and on each
	#'insert_text' we tell the textview to scroll to that mark
	$buffer->create_mark ('end', $buffer->get_end_iter, FALSE);
	$buffer->signal_connect (insert_text => sub {
	$tview->scroll_to_mark ($buffer->get_mark ('end'),
		0.0, TRUE, 0, 0.5);
	});

Notes

[1]

Networking falls outside the scope of this book. To get extra information, you can "google" for phrases like "perl socket demo" or "perl socket tutorial". "socket" can also be replaced by "network". There are quite a number of good tutorials available to gain background knowledge.

[2]

Should you require more info on the format of rc files: Appendix C