#!/usr/bin/perl
#
# app2.pl
#
# Autor: Marco Antonio Manzo <amnesiac@unixmonkeys.com>
#
# Descripción:
#   Programa3 de ejemplo - Tutorial Gtk2-Perl
#
# Licencia:
# Este programa puede ser redistribuido y/o modificado bajo
# los mismos términos de la licencia artística de Perl.
#

use strict;
use warnings;

use Glib qw( TRUE FALSE );
use Gtk2 '-init';
use Gtk2::SimpleMenu;
use Gtk2::SimpleList;

# Módulos necesarios para el funcionamiento de nuestra aplicación.
use DBI;
use YAML qw( LoadFile );
use UNIVERSAL 'isa';

sub _init {
    # Variable que utilizaremos para mantener el estado
    # entre algunos objetos de nuestra aplicación.
    my $args = { }; 

    # Creamos todos los widgets principales de nuestra aplicación,
    # utilizando nuevamente funciones fabricadas por nosotros para
    # encapsular dichos objetos.
    my $window = create_window( $args );
    my $query_frame = create_query_frame( $args );
    my $results_frame = create_results_frame( $args );
    my $menu = create_menu( $args );
    my $hbbox = create_app_buttons( $args );

    # Caja vertical que contiene todos nuestros widgets creados,
    # listos para ser mostrados.
    my $vbox = Gtk2::VBox->new( FALSE, 1 );
    $vbox->pack_start( $menu->{widget}, FALSE, FALSE, 1 );
    $vbox->pack_start( Gtk2::HSeparator->new, FALSE, FALSE, 1 );
    $vbox->pack_start( $query_frame, TRUE, TRUE, 3 );
    $vbox->pack_start( $results_frame, TRUE, TRUE, 3 );
    $vbox->pack_end( $hbbox, FALSE, FALSE, 1 );

    $window->add( $vbox );

    # Un AccelGroup es la clase que nos permite tener 'teclas rápidas'
    # de acceso, en nuestro caso, para el menú creado con Gtk2::SimpleMenu.
    # Automáticamente nuestro Gtk2::SimpleMenu contiene entre sus atributos
    # el accelgroup generado usando nuestro árbol de menú. Lo utilizamos como
    # parámetro en la ventana principal.
    $window->add_accel_group( $menu->{accel_group} );
    $window->show_all;
    return;
}

# Esta función nos regresa un objeto de tipo Gtk2::HButtonBox el cual contiene
# agrupados los botones principales de nuestra aplicación.
sub create_app_buttons {
    my $args = shift;

    # A diferencia de los otros constructores, new_from_stock() te permite crear
    # un botón utilizando imágenes y nombres del stock de GTk. Una vez creado
    # cada botón conectamos su señal de 'clicked' correspondiente.
    my $exit_btn = Gtk2::Button->new_from_stock( 'gtk-quit' );
    $exit_btn->signal_connect( clicked => \&on_window_destroy, $args );
    my $clear_btn = Gtk2::Button->new_from_stock( 'gtk-refresh' );
    $clear_btn->signal_connect( clicked => \&clear_results, $args );
    my $exe_btn = Gtk2::Button->new_from_stock( 'gtk-execute' );
    $exe_btn->signal_connect( clicked => \&exec_query, $args );

    # Gtk2::Tooltips nos permite generar algunos mensajes emergentes a la hora
    # de posicionarnos sobre el widget que establezcamos como parámetro, nuestro
    # caso los botones principales.
    my $tooltips = Gtk2::Tooltips->new;
    $tooltips->set_tip( $exit_btn, 'Salir de la aplicación' );
    $tooltips->set_tip( $clear_btn, 'Limpiar area de resultados' );
    $tooltips->set_tip( $exe_btn, 'Ejecutar Query' );
    $tooltips->enable;

    # Utilizamos un Gtl2::HButtonBox para agrupar un conjunto de botones los cuales
    # queremos que se dispersen a lo largo de la área de la aplicación( 'spread' ),
    # con un espaciado de 15px entre c/u de ellos.
    my $hbbox = Gtk2::HButtonBox->new;
    $hbbox->set_layout( 'spread' );
    $hbbox->set_spacing( 15 );

    $hbbox->add( $exe_btn );
    $hbbox->add( $clear_btn );
    $hbbox->add( $exit_btn );
    return $hbbox;
}

sub create_menu {
    my $args = shift;
    
    # Nuestro menú ahora será creado utilizando la clase Gtk2::SimpleMenu, la
    # cual require como parámetro un arbol con todos los nodos, callbacks,
    # aceleradores e hijos correspondientes.
    my $menu_tree = [
		  _Archivo =>
		  {
		   item_type => '<Branch>',
		   children => [
				'_Cargar Configuración' => {
							    callback => \&on_open_file,
							    callback_action => 0,
							    accelerator => '<ctrl>C',
							    callback_data => $args
							   },
				Separator => {
					      item_type => '<Separator>'
					     },
				'_Salir' => {
					     callback => sub {
						 $args->{db}->disconnect
						   if exists $args->{db};
						 Gtk2->main_quit
					     },
					     callback_action => 1,
					     accelerator => '<ctrl>S',
					    }
			       ]
		  },
		  A_yuda =>
		  {
		   item_type => '<Branch>',
		   children => [
				'Acerca de...' => {
						   callback => \&on_about,
						   callback_action => 2,
						   accelerator => '<ctrl>H'
						  }
			       ]
		  }
		 ];

    # Generamos nuestro menú.
    my $menu = Gtk2::SimpleMenu->new( menu_tree => $menu_tree );
    return $menu;
}

sub create_window {
    my $args = shift;
    my $window = Gtk2::Window->new( 'toplevel' );
    $window->set_title( 'Gtk2-Perl: Cliente SQL' );
    $window->set_default_size( 500, 400 ); # Tamaño inicial
    $window->set_border_width( 5 ); # Tamaño del borde
    $window->signal_connect( destroy => \&on_window_destroy, $args );
    return $window;
}

# Esta función se encargará de crear un frame con el área de texto donde
# podremos escribir nuestra sentencia de SQL.
sub create_query_frame {
    my $args = shift;
    # Un Gtk2::TextView nos servirá para poder almacenar un buffer con texto
    # el cual utilizaremos para escribir nuestra sentencia de SQL.
    my $textview = Gtk2::TextView->new;
    $textview->set_size_request( 0, 100 );

    # Es necesario guardar referencia sobre este objeto para poder manipularlo
    # mas delante.
    $args->{textwidget} = $textview;

    # nuestro Frame a retornar
    my $frame = Gtk2::Frame->new( '  SQL Query  ' );
    $frame->set_border_width( 5 );
    $frame->add( $textview );
    return $frame;
}

# Esta función retorna otro frame en el cual se podrán visualizar los resultados
# de la sentencia de SQL escrita.
sub create_results_frame {
    my $args = shift;
    my $frame = Gtk2::Frame->new( '  Resultados  ' );
    $frame->set_border_width( 5 );
    $frame->set_size_request( 0, 200 );

    # Gtk2::SimpleList nos permite generar una Lista de valores con tipos
    # de datos tales como texto, pixbufs, entre otros. Para nuestro propósito
    # Solamente utilizaremos campos de texto.
    $args->{results} = Gtk2::SimpleList->new(
					     name => 'text',
					     value => 'text'
					    );
    # Una ventana con barra de desplazamiento nos será de mucha ayuda en caso de
    # que la cantidad de filas retornadas por la base de datos sea muy grande.
    my $sc = Gtk2::ScrolledWindow->new;
    # La barra de desplazamiento aparecerá automáticamente, es decir solo cuando
    # sea necesaria.
    $sc->set_policy( 'automatic', 'automatic' ); 
    $sc->add( $args->{results} );
    $frame->add( $sc );
    # Guardamos referencia a nuestro widget para utilizarlo posteriormente
    $args->{scwin} = $sc;
    return $frame;
}

sub on_open_file {
    my $args = shift;
    my $fc = Gtk2::FileChooserDialog->new( 'Selecciona archivo de configuración', 
					   undef, 'open',
					   'gtk-ok' => 'ok',
					   'gtk-cancel' => 'cancel'
					 );
    if( $fc->run eq 'ok' ) {
	# Al seleccionar nuestro archivo de configuración, lo cargamos en una
	# estructura de datos.
	my $yaml = LoadFile( $fc->get_filename );
	my $dsn = make_dsn( $yaml ); # generamos nuestro DSN para la BD
	# Si todo sale bien, DBI->connect nos retorna una referencia a un objeto
	# de tipo DBI, caso contrario muere nuestra aplicación.
	$args->{db} = DBI->connect( $dsn, $yaml->{username}, $yaml->{password},
				    { RaiseError => 1 } ) or die $DBI::errstr;
    }
    $fc->destroy;
    return TRUE;
}

sub on_about {
    my $about = Gtk2::Dialog->new( 'Acerca de...', undef, 'modal',
				   'gtk-ok' => 'ok' );
    
    my $text =<<INFO;
Programa de prueba para
el Tutorial de Gtk2-Perl.

Autor: Marco Antonio Manzo

Consol 2005
INFO

    $about->vbox->pack_start( Gtk2::Label->new( $text ), FALSE, FALSE, 4 );
    $about->vbox->show_all;
    $about->destroy if $about->run;
    return TRUE;
}

# Manejador para el evento 'destroy'
sub on_window_destroy {
    my( $pwidget, $args ) = @_;
    # Desconecta nuestro manejador de la BD si este fué creado.
    $args->{db}->disconnect if exists $args->{db};
    Gtk2->main_quit;
    return;
}

# Esta función nos retorna una cadena con el DSN generado apartir del archivo
# de configuración.
sub make_dsn {
    my $yaml = shift;
    my $dsn = sprintf( "dbi:%s:dbname=%s;host=%s", $yaml->{dbdriver},
		       $yaml->{dbname}, $yaml->{dbhost} );
    return $dsn;
}

# Esta función limpiará el área de resultados cuando presionemos el botón
# Actualizar.
sub clear_results {
    my( $widget, $args ) = @_;
    # Si nuestra área de resultados se trata de un Gtk2::TextView, simplemente
    # borramos el buffer actual, sino, se trata obviamente de un Gtk2::SimpleList
    # entonces borramos su área de datos.
    unless( $args->{results}->isa( 'Gtk2::SimpleList' ) ) {
	$args->{results}->get_buffer->set_text( '' );
	return TRUE;
    }
    $args->{results}->set_data_array( [] );
    return TRUE;
}

# Nos despliega un cuadro de diálogo con el mensaje de error recibido.
sub error_window {
    my $msg = shift;
    my $dialog = Gtk2::Dialog->new( 'Error', undef, 'modal',
				    'gtk-ok' => 'ok' );
    $dialog->vbox->pack_start( Gtk2::Label->new( $msg ), FALSE, FALSE, 4 );
    $dialog->vbox->show_all;
    $dialog->destroy if $dialog->run;
    return TRUE;

}

# Esta función es la que, dependiendo del tipo de query, serán los resultados
# visualizados, y se manda llamar cuando presionamos el botón Ejecutar.
sub exec_query {
    my ( $widget, $args ) = @_;

    # Si el archivo de configuración no está cargado, despliega un mensaje
    # de error.
    error_window( "Faltar cargar configuración" ) and return
      unless $args->{db};

    # Gtk2::TextView contiene dentro un objeto de tipo Gtk2::TextBuffer, el cual
    # contiene  el texto que nosotros escribimos, es necesario obtenerlo para
    # poder ejecutar el query.
    my $buffer = $args->{textwidget}->get_buffer;
    my $query = $buffer->get_text( $buffer->get_start_iter, 
				   $buffer->get_end_iter, TRUE );

    # Si el buffer está vacío, significa que el usuario presionó ejecutar
    # sin escribir una sentencia de SQL.
    error_window( "No hay query a ejecutar" ) and return
      unless $query;

    my $sth = $args->{db}->prepare( $query );
    $sth->execute;

    my $newwidget; # Aqui almacenamos el widget a utilizar para los resultados
    if( $query =~ /^select/i ) {
	# Si se trata de un SELECT entonces utilizaremos un Gtk2::SimpleList
	# para mostrar los resultados.
	my( @ret, $row );
	push @ret, $row while $row = $sth->fetchrow_hashref;
	# Retornamos los nombres de las columnas
	my @columns = get_columns_ready( $ret[0] );
	$newwidget = Gtk2::SimpleList->new( @columns );
	# Establecemos el área de datos de nuestra lista.
	$newwidget->set_data_array( get_column_values( \@ret ) );
    } else {
	# Si se trata de cualquier otra cosa que no sea SELECT utilizaremos
	# un Gtk2::TextView para mostrar las filas afectadas.
	my $count = $sth->rows;
	my $msg = "$count fila(3Cs) afectada(s)";
	$newwidget = Gtk2::TextView->new;
	$buffer = Gtk2::TextBuffer->new;
	$buffer->set_text( $msg );
	$newwidget->set_buffer( $buffer );
    }
    $sth->finish;

    $args->{scwin}->remove( $args->{results} );
    $args->{scwin}->add( $newwidget );
    # Guardamos nuestro nuevo widget de resultados para su futura
    # utilización.
    $args->{results} = $newwidget;
    $args->{scwin}->show_all;
    return TRUE;
}

# Esta función nos retorna el nombre de las columnas utilizadas
# por la sentencia en SQL.
sub get_columns_ready {
    my $row = shift;
    my @ret;
    push( @ret, $_, 'text' ) for keys %{ $row };
    return @ret;
}

# Esta función nos retorna una referencia a un array con los valores
# retornados por la sentencia de SQL, el cual utilizaremos con nuestro
# Gtk2::SimpleList
sub get_column_values {
    my $rows = shift;
    my $ret;
    @{$ret} = map{ [ values %{ $_ } ] } @$rows;
    return $ret;
}

_init;

Gtk2->main;


syntax highlighted by Code2HTML, v. 0.9